Creating a small Todos application

The objective of this exercise is to use learning material as read on the French OpenClassrooms tutorial and, too, on Premier projet Laravel 5.4.

Use and practice Laravel’s framework...

Table des matières

1. Put things in place

Create the model, router, controller, views and request. Simple way.

1.1. Create a TODOs app

1.1.1. Create the migration class

php artisan make:migration create_todos_table

1.1.2. Update the migration class

Edit /database/migrations/2018_08_01_000000_create_todos_table.php and add our fields.

The list of column’s type can be retrieved here

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateTodosTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('todos', function (Blueprint $table) {
            $table->increments('id');
            $table->timestamps();
            $table->string('title', 100);
            $table->boolean('completed')->default(0);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('todos');
    }
}

1.1.3. Execute and create the table

php artisan migrate

1.1.4. Create a model

php artisan make:model Todo

1.1.5. Add code to the model

Edit /app/Todo.php.

We don’t need to specify our table’s fields

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Todo extends Model
{
  protected $table = 'todos';

  // Only if $table->timestamps() was mentioned in the
  // CreateTodosTable class; set to False if not mentioned
  public $timestamps = true;
}

1.1.6. Create validation rules

php artisan make:request TodoRequest

1.1.7. Add rules

Edit /app/Http/Requests/TodoRequest.php and add somes rules:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class TodoRequest extends FormRequest
{
  /**
   * Determine if the user is authorized to make this request.
   *
   * @return bool
   */
  public function authorize()
  {
    return true;  // allow anyone to submit Todos
  }

  /**
   * Get the validation rules that apply to the request.
   *
   * @return array
   */
  public function rules()
  {
    return [
      'title' => 'required|string|max:100',
      'completed' => 'boolean'
      ];
  }
}

1.1.8. Add routes

Edit /routes/web.php, add a GET and a POST route and, for the exercise, give a name to the POST one.

Add also a route for showing all todos.

Route::get('form', 'TodoController@getForm');
Route::post('todo', [
  'uses' => 'TodoController@postForm',
  'as' => 'storeTodo'
]);

Route::get('todos', [
  'uses' => 'TodoController@index',
  'as' => 'showTodos'
]);

1.1.9. Create a controller

php artisan make:controller TodoController

1.1.10. Add code to the controller

Edit /app/Http/Controllers/TodoController.php and add this code:

<?php

namespace App\Http\Controllers;

use App\Todo;
use App\Http\Requests\TodoRequest;

class TodoController extends Controller
{
  public function index()
  {
    return Todo::all(); // Return JSON output
  }

  public function getForm()
  {
    return view('todo'); // Show the form
  }

  public function postForm(TodoRequest $request)
  {
    $todo = new Todo();

    $todo->title = $request->input('title');
    $todo->completed = $request->input('completed');

    $todo->save(); // Save the submitted data

    return view('todo_ok'); // And show a "successful" page
  }
}

1.1.11. Create the view

Manually create the file /resources/views/form.blade.php (there is no artisan command for this)

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Some stupid Todos application</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <h1>Some stupid Todos application</h1>

    <div class="col-sm-offset-4 col-sm-4">
    <div class="panel panel-info">
      <div class="panel-heading">Todos</div>
      <div class="panel-body">
        {!! Form::open(['route' => 'storeTodo']) !!}
          <div class="form-group {!! $errors->has('todo') ? 'has-error' : '' !!}">
            {!! Form::text('title', null, array('size' => '100', 'class' => 'form-control', 'placeholder' => 'Enter Todo\'s title')) !!}
            {!! $errors->first('title', '<div class="alert alert-danger">:message</div>') !!}
          </div>
          <div class="form-group ">
            {!! Form::checkbox('completed', 1, 0)  !!}
            {!! Form::label('completed', 'Completed'); !!}
          </div>
          {!! Form::submit('Submit !') !!}
        {!! Form::close() !!}
      </div>
    </div>
  </div>
</body>
</html>

1.1.12. Test the view

The route, the controller and the view are now in place, we can therefore access to http://127.0.0.1:8000/todo and check if our form is well displayed.

Showing the Todo form

1.1.13. Create a successful view

Create /resources/views/todo_ok.blade.php

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Some stupid Todos application</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <h1>Some stupid Todos application</h1>
    <div class="panel-body">
        Successfully stored in the database
    </div>
</body>
</html>

1.1.14. Use the form

Go to http://127.0.0.1:8000/todo and add a few items. If everything goes fine, each of them will be now immediately stored in your database.

You can open it with, f.i. phpMyAdmin to check how they’re saved.

But remember: we’ve created a route called /todos so open http://127.0.0.1:8000/todos and you should see this:

Show index

That output is made by the controller, function index():

public function index()
{
  return Todo::all(); // Return JSON output
}

(The JSON formatting as displayed on the screen capture has been automatically made thanks the JSON Formatter Chrome extension)

2. Improve the output

Once everything is in place, improve the interface with CSS

2.1. Improving outputs

Our Todos’s application is now up and running (we can add new items and display all of them). We can now improve the look&feel of the interface.

2.1.1. Update the controller

Edit /app/Http/Controllers/TodoController.php and replace the index function with the one below.

Every record of the Todo table will be retrieved and pass to the todos view (will be created below) a variable called $data.

public function index()
{
    $data = Todo::all();

    return view('todos', compact('data'));
}

2.1.2. Create a todos view

Create /resources/views/todos.blade.php for displaying all todos.

Use the Blade language for checking if there are todos to show and if it’s the case, loop and display every title.

Add a link below the list to add new items.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Some stupid Todos application</title>
    <link media="screen" rel="stylesheet" type="text/css" href="/css/app.css" />
</head>
<body>
    <main role="main">
      <div class="jumbotron">
        <div class="container">
          <h1 class="display-3">Some stupid Todos application</h1>
          <p>A simple Laravel application</p>
        </div>
      </div>
    </main>
    <div class="container">
        @isset($data)
            <ol>
                @foreach($data as $todo)
                    <li>{{ $todo->title }}</li>
                @endforeach
            </ol>
        @endisset
        <hr/>
        <a href="/todo" >Add new item</a>
    </div>

</body>
</html>

Show index

2.1.3. Edit the todo_ok view

Improve the look and feel of /resources/views/todo_ok.blade.php, replace the view’s content by:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Some stupid Todos application</title>
    <link media="screen" rel="stylesheet" type="text/css" href="/css/app.css" />
</head>
<body>
    <main role="main">
      <div class="jumbotron">
        <div class="container">
          <h1 class="display-3">Some stupid Todos application</h1>
          <p>A simple Laravel application</p>
        </div>
      </div>
    </main>
    <div class="container">
        <div class="panel-body">
        Successfully stored in the database
        </div>
        <hr/>
        <a href="/todo" >Add new item</a> - <a href="/todos">Show all</a>
    </div>

</body>
</html>

Successfully stored in the database

2.1.4. Improve the fom

Improve the look and feel of the input form by setting a new content in /resources/views/form.blade.php:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Some stupid Todos application</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link media="screen" rel="stylesheet" type="text/css" href="/css/app.css" />
</head>
<body>
  <main role="main">
    <div class="jumbotron">
      <div class="container">
        <h1 class="display-3">Some stupid Todos application</h1>
        <p>A simple Laravel application</p>
      </div>
    </div>
  </main>
  <div class="container">
    <div class="panel-heading">Add a new Todo</div>
    <div class="panel-body">
      {!! Form::open(['route' => 'storeTodo']) !!}
        <div class="form-group {!! $errors->has('todo') ? 'has-error' : '' !!}">
          {!! $errors->first('title', '<small class="help-block">:message</small>') !!}
          {!! Form::text('title', null, array('size' => '100', 'class' => 'form-control', 'placeholder' => 'Enter Todo\'s title')) !!}
        </div>
        <div class="form-group ">
          {!! Form::checkbox('completed', 1, 0)  !!}
          {!! Form::label('completed', 'Completed'); !!}
        </div>
        {!! Form::submit('Submit !') !!}
      {!! Form::close() !!}
    </div>
    <hr/>
    <a href="/todos">Show all</a>
  </div>
</body>
</html>

Get Todo

2.1.5. Test the application

Go to http://127.0.0.1:8000/todo and

The list of todos is accessible at http://127.0.0.1:8000/todos, click on the Add new item link for getting the input form.

By submitting a new todo, the feedback screen will display the two hyperlinks: Add new item and Show all

The application is working with a basic Bootstrap theme.

3. Update the structure

Having only a title and a completed flag for a todo is working but too limited.

By adding a comment field and, also, the ID of the author will be useful and will allow to make the tool more useful.

3.1. Follow evolution

Idea: add extra fields to our Todos application, add dummy records by the way of a migration plan and update views to reflect these changes.

3.1.1. Add new fields

Edit the migration /database/migrations/2018_08_01_000000_create_todos_table.php and add extra columns in the up() function.

We’ll add a description field (that can stay empty) and the ID of the user who has filled in the Todo. We’ll also add a foreign key to the users table and, we can too, add a cascade delete trigger.

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateTodosTable extends Migration
{
  /**
   * Run the migrations.
   *
   * @return void
   */
  public function up()
  {
    Schema::create('todos', function (Blueprint $table) {
      $table->increments('id');
      $table->timestamps();
      $table->string('title', 100);
      $table->boolean('completed');
      $table->text('description', 1000)->nullable();
      $table->integer('user_id')->unsigned();
      $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
    });
  }

  /**
   * Reverse the migrations.
   *
   * @return void
   */
  public function down()
  {
    Schema::dropIfExists('todos');
  }
}

Perhaps is it a bad idea to remove all todo’s when the user removes his account. If so, just use $table->foreign('user_id')->references('id')->on('users'); i.e. without the trigger.

New structure of the Todos table

3.1.2. Refresh the table’s structure

Run the following instruction for refreshing the structure of all tables. Be careful: tables will be cleared first.

php artisan migrate:refresh

By looking at the table’s structure we’ll see the changes:

3.1.3. Populate the table

To add a few items in our table, we can use a script:

php artisan make:migration PopulateTestingDatas

Just like a table, Laravel will create a php file in the folder /database/migrations/; here, for this test, the file will be called 2018_08_01_000000_populate_testing_datas.php.

<?php

use Illuminate\Database\Migrations\Migration;

class PopulateTestingDatas extends Migration
{
  /**
   * Run the migrations.
   *
   * @return void
   */
  public function up()
  {
    // Create user Christophe
    DB::table('users')->insert([[
      'name' => 'Christophe',
      'email' => 'christophe@todos.com',
      'password' => bcrypt('admin')
    ]
    ]);

    // Getting the ID of the user Christophe
    $user_id = DB::table('users')->where('name', 'Christophe')->take(1)->value('id');

    // Insert a few items for him
    for ($i = 0; $i < 20; $i++) {
      DB::table('todos')->insert([
        [
          'title' => 'Todo #' . ($i + 1),
          'description' => 'Some important content for the Todo #' . ($i + 1),
          'user_id' => $user_id,
          'completed' => rand(0, 1),
          'created_at' => now(),
          'updated_at' => now()
        ]
      ]);
    }
  }

  /**
   * Reverse the migrations.
   *
   * @return void
   */
  public function down()
  {
    // Getting the ID of the user Christophe
    $user_id = DB::table('users')->where('name', 'Christophe')->take(1)->value('id');

    // Delete todos added by him
    DB::table('todos')->where('user_id', $user_id)->delete();

    // And remove the user
    DB::table('users')->where('name', '=', 'Christophe')->delete();
  }
}
3.1.3.1. Using Faker

Just for the fun, use Faker (see https://github.com/fzaninotto/Faker). So, we’ll have random dummy text.

First install Faker

composer require fzaninotto/faker

Then edit the up() function:

public function up()
    {
        // Create user Christophe
        DB::table('users')->insert([[
            'name' => 'Christophe',
            'email' => 'christophe@todos.com',
            'password' => bcrypt('admin')
        ]]);

        // Getting the ID of the user Christophe
        $user_id = DB::table('users')->where('name', 'Christophe')->take(1)->value('id');

        // Use faker to get french dummy text
        // If needed, just run "composer require fzaninotto/faker" in
        // a DOS prompt
        $faker = Faker\Factory::create('fr_FR');

        // Insert a few items for him
        for ($i = 0; $i < 20; $i++) {
            DB::table('todos')->insert([
                [
                    'title' => $faker->sentence($nbWords = 6, $variableNbWords = true) .
                        ' (todo #' . ($i + 1) . ')',
                    'description' => $faker->realText($maxNbChars = 1000),
                    'user_id' => $user_id,
                    'completed' => $faker->boolean(),
                    'created_at' => now(),
                    'updated_at' => now()
                ]
            ]);
        }
  }

3.1.4. Edit the todos view

Now that we’ve a description field and we know the author, we can use these infos in our todos view.

Edit /resources/views/todos.blade.php:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Some stupid Todos application</title>
  <link media="screen" rel="stylesheet" type="text/css" href="/css/app.css" />
</head>
<body>
  <main role="main">
      <div class="jumbotron">
        <div class="container">
          <h1 class="display-3">Some stupid Todos application</h1>
          <p>A simple Laravel application</p>
        </div>
      </div>
  </main>
  <div class="container">
    @isset($data)
      @foreach($data as $post)
         <h3><a href="todo/{{ $post->id }}">{{ $post->title }}</a></h3>
         <p>{{ $post->description }}</p>
         <small>Author: {{ $post->user_id }}</small>
         <hr/>
      @endforeach
    @endisset
    <hr/>
    <a href="/todo" >Add new item</a>
  </div>

</body>
</html>

This will result into this blog view. The author is here displayed with only his ID; we’ll change this.

Show index

As you can see, we’ve added an hyperlink for the todo’s title:

<h3><a href="todo/{{ $post->id }}">{{ $post->title }}</a></h3>

So, we need to create a route that will show the selected Todo.

3.1.5. Retrieve user’s information

Edit /app\Todo.php and since we’ve a foreign key between the user_id of the todos table and the field id of the table users; it’s really use to retrieve informations.

Just add the user() function like below:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Todo extends Model
{
    protected $table = 'todos';

    // Only if $table->timestamps() was mentioned in the
    // CreateTodosTable class; set to False if not mentioned
    public $timestamps = true;

    public function user()
    {
        return $this->belongsTo('App\User', 'user_id', 'id');
    }
}

This works since, in our up() function for the CreateTodosTable we’ve:

$table->foreign('user_id')->references('id')->on('users');

So, now, we can use every columns of the users table by just referencing the ´user()` function.

3.1.6. Edit the todos view once more

Edit /resources/views/todos.blade.php and replace

<small>Author: {{ $post->user_id }}</small>

by

<small>Author: {{ $post->user->name }}</small>

And now, we’ve the name...

Show author

3.1.7. Add a detail todo route

Edit /app/Http/Controllers/TodoController.php and add this function:

public function show($id)
{
    $data = Todo::where('id', $id)->firstOrFail();

    return view('show', compact('data'));
}

We’ll use the Todo model (as defined in /app/Todo.php) and the where method will retrieved the specified item.

3.1.8. Add a detail todo view

Create this file /resources/views/show.blade.php:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Some stupid Todos application</title>
  <link media="screen" rel="stylesheet" type="text/css" href="/css/app.css" />
</head>
<body>
  <main role="main">
      <div class="jumbotron">
        <div class="container">
          <h1 class="display-3">Some stupid Todos application</h1>
          <p>A simple Laravel application</p>
        </div>
      </div>
  </main>
  <div class="container">
    @isset($data)
      <h3>{{ $data->title }}</h3>
      <p>{{ $data->description }}</p>
      <small>
        Created at: {{ $data->created_at }}
        <br/>
        Last updated: {{ $data->updated_at }}
        <br/>
        Author: {{ $data->user->name }}
      </small>
      <hr/>
    @endisset
    <a href="/todo" >Add new item</a> - <a href="/todos">Show all</a>
  </div>
</body>
</html>

3.1.9. Edit the form

We need to add a description field.

Edit /resources/views/form.blade.php and add the two lines below:

{!! Form::label('description', 'Description (optional)'); !!}
{!! Form::textarea('description', null, array('class' => 'form-control')) !!}

Then form will thus become:

{!! Form::open(['route' => 'storeTodo']) !!}
<div class="form-group {!! $errors->has('todo') ? 'has-error' : '' !!}">
  {!! Form::text('title', null, array('size' => '100', 'class' => 'form-control', 'placeholder' => 'Enter Todo\'s title')) !!}
  {!! $errors->first('title', '<div class="alert alert-danger">:message</div>') !!}
</div>  
<div class="form-group ">
  {!! Form::checkbox('completed', 1, 0)  !!}
  {!! Form::label('completed', 'Completed'); !!}
</div>
<div class="form-group ">
  {!! Form::label('description', 'Description (optional)'); !!}
  {!! Form::textarea('description', null, array('class' => 'form-control')) !!}
</div>
{!! Form::submit('Submit !') !!}
{!! Form::close() !!}

3.1.10. Store the description

Edit /app/Http/Controllers/TodoController.php and add in the postForm() function:

$todo->description = $request->input('description');

The function will become:

public function postForm(TodoRequest $request)
{
  $todo = new Todo();

  $todo->title = $request->input('title');
  $todo->description = $request->input('description');
  $todo->completed = $request->input('completed');

  $todo->save(); // Save the submitted data

  return view('todo_ok'); // And show a "successful" page
}

4. Use Blade and make life easier

Using Blade, we can use views and inject variable contents in them. With a master view f.i., we’ll draw the look & feel of the page only once. We don’t need anymore to repeat again and again the structure of the page but just concentrate on the content.

4.1. DRY thanks to Blade framework

Don’t repeat yourself thanks to Blade framework

So far, we’ve already create a few views and each time we’re coding again and again the same content: the header block f.i. is always the same.

4.1.1. The master template

The look & feel of our pages will be:

<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <meta name="csrf-token" content="{{ csrf_token() }}">
  <title>Some stupid Todos application</title>
  <link media="screen" rel="stylesheet" type="text/css" href="/css/app.css" />
</head>
<body>
  <main role="main">
    <div class="jumbotron">
      <div class="container">
        <h1 class="display-3">Some stupid Todos application</h1>
        <p>A simple Laravel application</p>
      </div>
    </div>
  </main>
  <div class="container">
    @yield('content')
    <hr/>
    @yield('navigation')
  </div>
</body>
</html>

Use app()->getLocale() to retrieve the langage for the .env.

We’ll have two variables part: the content of the page and the navigation.

Already add the Laravel token in the template to protect form’s submissions.

4.1.2. The todos view

List all todos (http://127.0.0.1:8000/todos)

Edit /resources/views/todos.blade.php:

@extends('master')

@section('content')
  @isset($data)
    @foreach($data as $post)
      <h3><a href="todo/{{ $post->id }}">{{ $post->title }}</a></h3>
      <p>{{ $post->description }}</p>
      <small>Author: {{ $post->user->name }}</small>
      <hr/>
    @endforeach
  @endisset
@endsection

@section('navigation')
  <a href="/todo" >Add new item</a>
@endsection

We’ll use the master template then inject our two contents.

The content will be the list of todos, one h3 by Todo followed by his description and the name of the todo’s author.

4.1.3. The detail view

Show the detail of a given todo (http://127.0.0.1:8000/todo/1)

Edit /resources/views/show.blade.php:

@extends('master')

@section('content')
  @isset($data)
    <h3>{{ $data->title }}</h3>
    <p>{{ $data->description }}</p>
    <small>
      Created at: {{ $data->created_at }}
      <br/>
      Last updated: {{ $data->updated_at }}
      <br/>
      Author: {{ $data->user->name }}
    </small>
    <hr/>
  @endisset
@endsection

@section('navigation')
  <a href="/todo" >Add new item</a> - <a href="/todos">Show all</a>
@endsection

4.1.4. The todo_ok view

Displayed after the submission of a new Todo

Edit /resources/views/todo_ok.blade.php:

@extends('master')

@section('content')
  <div class="alert alert-success" role="alert">Successfully stored in the database</div>
@endsection

@section('navigation')
  <a href="/todo" >Add new item</a> - <a href="/todos">Show all</a>
@endsection

4.1.5. The form view

Edit /resources/views/form.blade.php:

@extends('master')

@section('content')
  <div class="panel-heading">Hi {{ Auth::user()->name }}, please add your new Todo below</div>  
  <div class="panel-body">
    {!! Form::open(['route' => 'storeTodo']) !!}
      <div class="form-group {!! $errors->has('todo') ? 'has-error' : '' !!}">
        {!! Form::text('title', null, array('size' => '100', 'class' => 'form-control', 'placeholder' => 'Enter Todo\'s title')) !!}
        {!! $errors->first('title', '<div class="alert alert-danger">:message</div>') !!}
      </div>
      <div class="form-group ">
        {!! Form::checkbox('completed', 1, 0)  !!}
        {!! Form::label('completed', 'Completed'); !!}
      </div>
      <div class="form-group ">
        {!! Form::label('description', 'Description (optional)'); !!}
        {!! Form::textarea('description', null, array('class' => 'form-control')) !!}
      </div>
      {!! Form::submit('Submit !') !!}
    {!! Form::close() !!}
  </div>
@endsection

@section('navigation')
  <a href="/todos">Show all</a>
@endsection
4.1.5.1. Tips:

https://laravel-news.com/five-useful-laravel-blade-directives

We can replace

@if(auth()->user())
  // The user is authenticated.
@endif

by the @auth directive; more powerful.

@auth
  // The user is authenticated.
@endauth

Same for @guest.

4.1.6. Test

Now, you can test each view, no change will be visible by visiting the site.

But views are now easier to manage; we can update the master view and the change will be visible everywhere.

5. Protect our form

Use the auth middleware of Laravel, display a login form and retrieve the connected user.

5.1. Enable auth middleware

5.1.1. Enable the authentification layer

This is easy, just run artisan:

php artisan make:auth

A new controller will be automatically created: /app/Http/Controllers/HomeController.php, routes will be also added into /routes/web.php.

Auth::routes();

Route::get('home', 'HomeController@index')->name('home');
Route::get('logout', ['as' => 'logout', 'uses' => 'Auth\LoginController@logout']);

The logout route isn’t mandatory but make easy to disconnect by visiting http://127.0.0.1:8000/logout.

5.1.2. Update routes

We’ll protect our application so, we don’t want to protect the URL /home but the root /.

Edit /routes/web.php and update like this:

<?php

Auth::routes();
Route::get('/', 'HomeController@index')->name('home');

Route::get('form', 'TodoController@getForm');
Route::post('todo', ['uses' => 'TodoController@postForm', 'as' => 'storeTodo']);
Route::get('todos', ['uses' => 'TodoController@index','as' => 'showTodos']);

The last three routes remain, for this moment, unchanged.

If everything is going fine, now, we’ll have an authentication screen when visiting http://127.0.0.1:8000/.

Authentication

We’ve already created a dummy user previously:

Let’s try

Logged in

The look&feel of the views and the management of the authentication’s process is entirely managed by Laravel.

Note: by registering a new user, Laravel will redirect to the /home URL. We can change this easily by updating the file /app/Http/Controllers/Auth/RegisterController.php and change the $redirectTo variable to /:

protected $redirectTo = '/';

5.1.3. Update the HomeController

Once connected, it would be great that http://127.0.0.1:8000/ display our views and not the You are logged in message so, when connected, the / route shouldn’t more redirect to the login feature but to one or our page.

Edit the /app/Http/Controllers/HomeController.php controller and add the following Facadest the top of the file (below the namespace).

use Illuminate\Support\Facades\Auth;

Then update the index() function like this:

/**
  * Show the application dashboard.
  *
  * @return \Illuminate\Http\Response
  */
public function index()
{
  if (Auth::check()) {
    return view('todos');
  } else {
    return view('home');
  }
}

So, when check() is true (meaning that the user is connected), the controller will show the todos view otherwhise the home view (login form).

5.1.4. Display the logged-in username

Edit file /resources/views/form.blade.php and replace the line

<div class="panel-heading">Add a new Todo</div>

With this one:

<div class="panel-heading">Hi {{ Auth::user()->name }}, please add your new Todo below</div>

We’ll just show the name of the connected user.

5.1.5. Store the user_id

Edit /app/Http/Controllers/TodoController.php and add in the postForm() function:

$todo->user_id = Auth::user()->id;

The function will become:

public function postForm(TodoRequest $request)
{
  if (Auth::check()) {
    $todo = new Todo();

    $todo->title = $request->input('title');
    $todo->description = $request->input('description');
    $todo->completed = $request->input('completed');
    $todo->user_id = Auth::user()->id;

    $todo->save(); // Save the submitted data

    return view('todo_ok'); // And show a "successful" page
  }

  return redirect()->back();
}

5.1.6. Add a new view for displaying message

This simple view will be used for, only, displaying a message like an error.

Create file /resources/views/message.blade.php

@extends('master')

@section('content')
@if(!empty($message))
  <div class="alert alert-{{ $status }}">{!! $message !!}</div>
@endif
@endsection

Note: use and not so, we can use HTML output in the message.

This view is using two variables: status and message. In the controller, we’ll use:

return view('message')
  ->with('message', 'The message that needs to be displayed')
  ->with('status', 'status');  // can be warning, error, success, info, ...

5.1.7. Don’t show the form unless logged-in

Since the /resources/views/form.blade.php view now show the connected username, we need to block the access to http://127.0.0.1:8000/todo (the submission form) and show it only if logged-in.

One way is to edit the controller, add a check in the getForm() function.

public function getForm()
{
  if (Auth::check()) {
    return view('todo'); // Show the form
  } else {
    return view('message')
      ->with('message', 'Session expired, please <a href="/login">reconnect</a>')
      ->with('status', 'warning');
  }
}

Message

But there is a much better way to do this

Because we need to check if the user is authenticated for methods like DELETE, PUT or POST, it’ll be more efficient to use the middleware() notion:

// Protect these actions, a valid user should be connected
Route::get('form', 'TodoController@getForm')->middleware('auth');
Route::post('todo', ['uses' => 'TodoController@postForm', 'as' => 'storeTodo']);

Laravel will make sure that a user is connected before running the controller. So, prevent to display the form and to post data for a guest.

6. Make the app working

Add actions to our application so, we can edit and delete items.

6.1. Add update and delete verbs

6.1.1. Add a route for edit and delete

Edit `/routes/web.php’ and add the new routes:

Route::get('todo/{id}/edit', 'TodoController@edit')->where('id', '[0-9]+')->middleware('auth');
Route::put('todo', 'TodoController@put')->middleware('auth');
Route::delete('todo/{id}', 'TodoController@delete')->where('id', '[0-9]+')->middleware('auth');

These routes are using HTTP methods and thus use Route::delete() and Route::put() to be ready for REST approach.

Route::get('todo/{id}/edit', 'TodoController@edit')->where('id', '[0-9]+')->middleware('auth'); is for displaying the form with, already filled in, the data coming from the todo that should be edited.

So, two actions but three new routes.

6.1.2. Update master view and add script

Edit `/resources/views/master.blade.php’ and add jquery and a yield variable to make possible to inject js statements:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="csrf-token" content="{{ csrf_token() }}">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Some stupid Todos application</title>
  <link media="screen" rel="stylesheet" type="text/css" href="/css/app.css" />
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
</head>
<body>
  <main role="main">
    <div class="jumbotron">
      <div class="container">
        <h1 class="display-3">Some stupid Todos application</h1>
        <p>A simple Laravel application</p>
      </div>
    </div>
  </main>
  <div class="container">
    @yield('content')
    <hr/>
    @yield('navigation')
  </div>
  @yield('script')
</body>
</html>

6.1.3. Add buttons to the detail view

Edit `/resources/views/show.blade.php’ and add jquery and a yield variable.

The HTTP method (PUT or DELETE) will be derived from the text of the button.

The Ajax part will send the request to the server then remove the two buttons for displaying a text.

@extends('master')

@section('content')
  @isset($data)
    <h3>{{ $data->title }}</h3>
    <p>{{ $data->description }}</p>
    <small>
      Created at: {{ $data->created_at }}
      <br/>
      Last updated: {{ $data->updated_at }}
      <br/>
      Author: {{ $data->user->name }}
    </small>
    <hr/>
    <span class="buttons">
      <input type="button" value="Update" class="edit"/> - <input type="button" value="Delete" class="delete"/>
    </span>  
  @endisset
@endsection

@section('navigation')
  <a href="/todo" >Add new item</a> - <a href="/todos">Show all</a>
@endsection

@section('script')
<script defer="defer">
  $('.delete, .edit').click(function(){

    if (this.value.toUpperCase() === 'DELETE') {

      // Add the csrf-token protection but only when the request is
      // made on the same site (no cross-domain).
      // Don't share the token outside
      $.ajaxSetup({
        beforeSend: function(xhr, type) {
          if (!type.crossDomain) {
              xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'));
          }
      }
      });

      $.ajax({
        url: '/todo/{{ $data->id }}',
        type: 'DELETE',
        contentType: 'application/json',
        success: function (data) {
          if (data.hasOwnProperty("message")) {
            // Replace buttons and display the feedback message
            $('.buttons').html(data.message);
          }
        },
        error: function (data, textStatus, errorThrown) {
          console.log(data);
        }
      });
    } else {
      window.location.replace('/todo/{{ $data->id }}/edit');
    }
});
</script>
@endsection

6.1.4. Add the delete action

Edit /app/Http/Controllers/TodoController.php and add a delete() function and add the Response facade:

use Response;

public function delete($id)
{
  Todo::destroy($id);

  return Response::json([
    'status' => true,
    'message' => '<div class="alert alert-success" role="alert">Successfully deleted</div>'
  ]);
}

So, the delete() function will return a JSON output and the Ajax success() anonymous function will use the message and output it:

Feedback

6.1.5. Add the put action

Edit /app/Http/Controllers/TodoController.php and add a put() function:

public function put(TodoRequest $request)
{
  // Retrieve the record
  $todo = Todo::where('id', $request->input('id'))->firstOrFail();

  // List of fields that we'll update
  $todo->update($request->only(['title', 'completed', 'description']));

  // Redirect to the edit form
  return redirect()->back()
    ->with('message', '<div class="alert alert-success" role="alert">Successfully updated</div>');
}

We’ll reuse our todo view i.e. the one that is displaying the form.

6.1.6. Update the Todo model

Since the update() method requires the use of the $fillable property, we need to update our model. $fillable lists the fields that can be updated so not mentioning user_id f.i. will make the update() method fails when trying to modifying that specific field.

Edit /App/Todo.php and add the property:

public $fillable = ['title', 'completed', 'description'];

6.1.7. Edit the form and show the record

Edit /resources/views/form.blade.php and add $data variable like below. Add also extra informations like the completed flag and the ID in a hidden field.

@extends('master')

@section('content')

  <div class="panel-heading">Hi {{ Auth::user()->name }}, please add your new Todo below</div>  
  <div class="panel-body">
    @if(Session::has('message'))
      {!! Session::get('message') !!}
    @endif
    {!! Form::open(['route' => 'storeTodo', 'method' => (isset($data) ? 'PUT' : 'POST')]) !!}
      {!! Form::hidden('id', $data->id ?? 0) !!}
      <div class="form-group {!! $errors->has('todo') ? 'has-error' : '' !!}">
        {!! Form::text('title', $data->title ?? '', array('size' => '100', 'class' => 'form-control', 'placeholder' => 'Enter Todo\'s title')) !!}
        {!! $errors->first('title', '<div class="alert alert-danger">:message</div>') !!}
      </div>
      <div class="form-group ">
        {!! Form::checkbox('completed', 1, (isset($data) ? ($data->completed==1) : 0))  !!}
        {!! Form::label('completed', 'Completed'); !!}
      </div>
      <div class="form-group">
        {!! Form::label('description', 'Description (optional)'); !!}
        {!! Form::textarea('description', $data->description ?? '', array('class' => 'form-control')) !!}
      </div>
      {!! Form::submit('Submit !') !!}
    {!! Form::close() !!}
  </div>
@endsection

@section('navigation')
  <a href="/todos">Show all</a>
@endsection
6.1.7.1. Tips:

We can use the @auth directive; more powerful.

Note: HTML::Forms doesn’t support the PUT method so the trick is to add a _method field. This can be done with the line below.

If we’ve a record (so, we’ve an ID), we’ll use the PUT method, if not, this is a new record so use POST.

Form::open(['route' => 'storeTodo', 'method' => (isset($data) ? 'PUT' : 'POST')])

Since Laravel 5.6, we can also just use @method('put').

@section('content')
  <form>
    @method('put')
    ...

The three lines below will allow to output message sent by the controller.

@if(Session::has('message'))
  {!! Session::get('message') !!}
@endif

The put() in the controller indeed redirect with a message. Laravel use the Session object for storing the message.

return redirect()->back()
  ->with('message', '<div class="alert alert-success" role="alert">Successfully updated</div>');

6.1.8. Test

Everything is now in place.

Go to http://127.0.0.1:8000/todos, display a Todo by clicking on his title, the detail of the todo will be displayed with two buttons : Update and Delete.

By clicking on Update, the form will be displayed with the todo’s info loaded.

Put

Clicking on the Submit ! button will send a PUT request to the server and the put() function of the controller will be called; make the changes and show the form back.

By clicking on Delete, an Ajax call will be made with a DELETE request, the delete() function of the controller will be called and a alert message will then be displayed.

Pay attention, in the database, to the updated_at column: Laravel will automatically update that column. Just because our model (/app/Todo.php) has mentionned $timestamp = true;.

7. Review code organization

Make code quality better.

7.1. Better coding

https://openclassrooms.com/fr/courses/3613341-decouvrez-le-framework-php-laravel/3617473-migrations-et-modeles#/id/r-3617459

7.1.1. Context

In our /app/Http/Controllers/TodoController.php controller, we’ve this:

use App\Todo;

public function postForm(TodoRequest $request)
{
  $todo = new Todo();

  $todo->title = $request->input('title');
  $todo->description = $request->input('description');
  $todo->completed = $request->input('completed');
  $todo->user_id = Auth::user()->id;

  $todo->save();

  return view('todo_ok');
}

The problem: what will happen if, f.i., the description field is removed / renamed in the model /app/Todo.php? The controller will fail.

An updated version of the code can be the one below where the instantiation of the Todo() class has been removed in the function since the object is now part of the parameters.

use App\Todo;

public function postForm(TodoRequest $request, Todo $todo)
{
  $todo->title = $request->input('title');
  $todo->description = $request->input('description');
  $todo->completed = $request->input('completed');
  $todo->user_id = Auth::user()->id;

  $todo->save();

  return view('todo_ok');
}

But we still have our fields in our controller... The idea is to just call a save() method and without having a use App\Todo. Just save() without knowing what and where.

public function postForm(...)
{
  $todo->save();

  return view('todo_ok');
}

7.1.2. Using a repository

Put the data logic into the repository and remove it from the controller.

7.1.2.1. Edit the controller

The controller should only know that the method is called save() and nothing else. The controller will become agnostic: no need to known which fields are in the model, which ones needs to be saved, ... just call save() and that’s it.

Edit /app/Http/Controllers/TodoController.php, add a use statement for the repository and change the postForm() function:

use App\Repositories\TodoRepository;

public function postForm(
    TodoRequest $request,
    TodoRepository $todoRepository
) {
    $todoRepository->save($request);

    return view('todo_ok');
}
7.1.2.2. Create a repository

Create the /App/Repositories/TodoRepository.php file.

<?php

namespace App\Repositories;

use App\Todo;
use App\Http\Requests\TodoRequest;
use Auth;

class TodoRepository
{
    protected $todo;

    public function __construct(Todo $todo)
    {
        $this->todo = $todo;
    }

    public function save(TodoRequest $todo)
    {
        $this->todo->title = $todo->input('title');
        $this->todo->description = $todo->input('description');
        $this->todo->completed = $todo->input('completed');
        $this->todo->user_id = Auth::user()->id;

        $this->todo->save(); // Save the submitted data
    }
}
7.1.2.3. Test the repository

Once the controller has been updated and is referencing the repository, the save() method will be the one of the repository.

By going to http://127.0.0.1:8000/todo, if correctly implemented, the form will still work and the record stored in our database;

We can go one step further: by using an interface.

7.1.3. Using an interface

The interface will only inform other classes about the existing methods in the repository.

7.1.3.1. Create the interface

Create the /App/Repositories folder if needed and create the TodoRepositoryInterface.php file.

<?php

namespace App\Repositories;

use App\Http\Requests\TodoRequest;

interface TodoRepositoryInterface
{
    public function save(TodoRequest $todo);
}
7.1.3.2. Edit the repository

Edit /App/Repositories/TodoRepository.php and change

class TodoRepository

by

class TodoRepository implements TodoRepositoryInterface

The class extends the interface so, we need to implement the save() function and, too, we need to strictly respect the function declaration (so if the function in the interface call his parameter $todo, the repository should call it $todo too).

7.1.3.3. Edit the controller and use the interface

Edit /app/Http/Controllers/TodoController.php and change the postForm() function:

public function postForm(
    TodoRequest $request,
    TodoRepository $todoRepository
)

to

public function postForm(
    TodoRequest $request,
    TodoRepositoryInterface $todoRepository
)

So ask to use the interface and not the repository directly.

And ... this won’t work.

If you test to submit a new record, you’ll get this error:

Illuminate\Contracts\Container\BindingResolutionException
Target [App\Repositories\TodoRepositoryInterface] is not instantiable.

This because and interface can’t be instantiating.

Laravel comes with a little trick: bind the interface and the class using it.

Edit /app/Http/Providers/AppServiceProvider.php and add these lines to the register() function:

public function register()
{
    ...
    $this->app->bind(
        'App\Repositories\TodoRepositoryInterface',
        'App\Repositories\TodoRepository'
    );
}

Now, when Laravel will see TodoRepositoryInterface he’ll knows that he should use TodoRepository.

7.1.3.4. Test the interface

Once the controller has been updated and is referencing the interface, the save() method will be the one of the repository (which is an extension of the interface).

By going to http://127.0.0.1:8000/todo, if correctly implemented, the form will still work and the record stored in our database;

8. Final

Final code

8.1. Final code

Here is the final code of this lab: todos.zip

A few changes have been made like showing the list of todos as the homepage. So the code proposed here is a little different (also documented).

8.1.1. Install a fresh copy

To use this code, please follow these steps:

  1. Start a DOS prompt
  2. Make sure Laravel is correctly install: type Laravel -V, if you see the Laravel Installer’s version number (f.i. 2.0.1), it’s fine.
  3. Install a fresh Laravel website: go to your public_html folder (i.e. where you wish create your website, can be c:\development\my_sites too if configured like this on your machine).
  4. At the prompt level, run laravel new a_folder_name (f.i. laravel new app_todos)
  5. Once the installation is done, go in that folder: type cd app_todos (i.e. your folder)
  6. Grab a copy of todos.zip and unzip the file (under DOS "C:\Program Files\7-Zip\7z.exe" x todos.zip -aoa i.e. extract with full path and overwrite existing files)
  7. Retrieve the name of the database to create:
    1. Open with a text editor the file called .env present in the root of app_todos
    2. Look to the DB_DATABASE variable, you’ll find the name of the database for the application (by default, it’ll be todos)
  8. Create a database with that name (todos) in your mySQL
    1. At the prompt level, type mysql -u root -p (where root is the username)
    2. When asking the password, just press enter (there is no password)
    3. In the MySQL prompt, type CREATE DATABASE todos; and press Enter
    4. Type quit for leaving MySQL
  9. Start the migration: php artisan migrate:install
  10. Create tables: php artisan migrate:fresh
  11. Add the authentication layer: php artisan make:auth
  12. Run composer require "laravelcollective/html"
  13. This done, start artisan by starting php artisan serve on the DOS prompt.
  14. With your browser go to http://127.0.0.1:8000/login and make a login with christophe@todos.com for the email while admin is the password.
  15. Then we can use the application: http://127.0.0.1:8000

8.1.2. Code source

8.1.2.1. routes/web.php

Definition of our routes and how to manage them.

<?php

// Enable authentication routes
Auth::routes();

// Register http://127.0.0.1:8000/login and display the login screen
Route::get('home', 'HomeController@index')->name('home');

// Register the http://127.0.0.1:8000/logout and logout
Route::get('logout', ['as' => 'logout', 'uses' => 'Auth\LoginController@logout']);

// -------------------

// GET {URL} - Default URL : Show the list of todos, as a blog
Route::get('/', [
    'uses' => 'TodoController@index',
    'as' => 'showTodos'
]);

// GET {URL}/todo/{id} -> show the todo #{id}
Route::get('todo/{id}', 'TodoController@show')->where('id', '[0-9]+');

// GET {URL}/todo -> show a form -> only for logged in users
Route::get('todo', 'TodoController@getForm')->middleware('auth');

// POST {URL}/todo -> the form is being submitted -> only for logged in users
Route::post('todo', [
    'uses' => 'TodoController@postForm',
    'as' => 'storeTodo'
])->middleware('auth');

// GET {URL}/todo/{id}/edit -> edit an existing item in a form -> only for logged in users
Route::get('todo/{id}/edit', 'TodoController@edit')->where('id', '[0-9]+')->middleware('auth');

// DELETE {URL}/todo/{id} -> delete an existing item -> only for logged in users
Route::delete('todo/{id}', 'TodoController@delete')->where('id', '[0-9]+')->middleware('auth');

// PUT {URL}/todo -> update an existing item -> only for logged in users
Route::put('todo', 'TodoController@put')->middleware('auth');
8.1.2.2. app/Todo.php

Our model, layer for our todos table.

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Todo extends Model
{
    // Name of our table in the database
    protected $table = 'todos';

    // Only if $table->timestamps() was mentioned in the
    // CreateTodosTable class; set to False if not mentioned
    public $timestamps = true;

    // List of columns that we can update
    public $fillable = ['title', 'completed', 'description'];

    /**
     * Extend the model and offer a convenient way to retrieve the
     * user linked to our todo (thanks the user_id foreign key and
     * his link with the users table)
     *
     * @return void
     */
    public function user()
    {
        return $this->belongsTo('App\User', 'user_id', 'id');
    }
}
8.1.2.3. database/migrations/create_todos_table.php

Define the structure of the todos table.

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateTodosTable extends Migration
{
    /**
     * The migration is running
     * Define a foreign key between user_id and the users table
     *
     * @return void
     */
    public function up()
    {
        Schema::create('todos', function (Blueprint $table) {
            // Our primary key
            $table->increments('id');

            // Allow Eloquent to add two fields and managed them:
            // created_at and updated_at
            $table->timestamps();

            $table->string('title', 100);
            $table->boolean('completed')->default(0);
            $table->text('description', 1000)->nullable();

            // The author of the record
            $table->integer('user_id')->unsigned();
            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('todos');
    }
}
8.1.2.4. database/migrations/populate_testing_datas.php

Populate the table with fake records.

<?php

use Illuminate\Database\Migrations\Migration;

class PopulateTestingDatas extends Migration
{
    /**
     * The migration is running
     *
     * @return void
     */
    public function up()
    {
        // Create user Christophe
        DB::table('users')->insert([[
            'name' => 'Christophe',
            'email' => 'christophe@todos.com',
            'password' => bcrypt('admin')
        ]]);

        // Getting the ID of the user Christophe
        $user_id = DB::table('users')->where('name', 'Christophe')->take(1)->value('id');

        // Use faker to get french dummy text
        // If needed, just run "composer require fzaninotto/faker" in
        // a DOS prompt
        $faker = Faker\Factory::create('fr_FR');

        // Insert a few items for him
        for ($i = 0; $i < 20; $i++) {
            DB::table('todos')->insert([
                [
                    'title' => $faker->sentence($nbWords = 6, $variableNbWords = true) .
                        ' (todo #' . ($i + 1) . ')',
                    'description' => $faker->realText($maxNbChars = 1000),
                    'user_id' => $user_id,
                    'completed' => $faker->boolean(),
                    'created_at' => now(),
                    'updated_at' => now()
                ]
            ]);
        }
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        // Getting the ID of the user Christophe
        $user_id = DB::table('users')->where('name', 'Christophe')->take(1)->value('id');

        // Delete todos added by him
        DB::table('todos')->where('user_id', $user_id)->delete();

        // And remove the user
        DB::table('users')->where('name', '=', 'Christophe')->delete();
    }
}
8.1.2.5. app/Repositories/TodoRepositoryInterface.php

Defines the functions that are supported by the TodoRepository i.e. the data logic.

<?php

namespace App\Repositories;

use App\Todo;
use Illuminate\Database\Eloquent\Collection;
use App\Http\Requests\TodoRequest;

/**
 * Define the function that should be available through the
 * TodoRepository
 */
interface TodoRepositoryInterface
{
    /**
     * Get the list of records of the table
     *
     * @return Collection
     */
    public function index() : Collection;

    /**
     * Get a specific todo; identified by his ID
     *
     * @param  integer $id
     * @return Todo
     */
    public function get(int $id) : Todo;

    /**
     * Save the todo
     *
     * @param  TodoRequest $todo Submitted data
     * @return void
     */
    public function save(TodoRequest $todo);

    /**
     * Remove the specified todo
     *
     * @param  integer $id
     * @return void
     */
    public function delete(int $id);

    /**
     * Update the specified todo, update columns
     *
     * @param  TodoRequest $request
     * @return void
     */
    public function put(TodoRequest $request);
}
8.1.2.6. app/Repositories/TodoRepository.php

Implements functions for our data logic.

<?php

namespace App\Repositories;

use App\Http\Requests\TodoRequest;
use Illuminate\Database\Eloquent\Collection;
use App\Todo;
use Auth;

/**
 * Implements functions for working with the todos table
 */
class TodoRepository implements TodoRepositoryInterface
{
    protected $todo;

    public function __construct(Todo $todo)
    {
        $this->todo = $todo;
    }

    /**
     * Get the list of records of the table
     *
     * @return Collection
     */
    public function index() : Collection
    {
        return $this->todo->all();
    }

    /**
     * Get a specific todo; identified by his ID
     *
     * @param  integer $id
     * @return Todo
     */
    public function get(int $id) : Todo
    {
        return $this->todo->where('id', $id)->firstOrFail();
    }

    /**
     * Save the todo
     *
     * @param  TodoRequest $todo Submitted data
     * @return void
     */
    public function save(TodoRequest $todo)
    {
        $this->todo->title = $todo->input('title');
        $this->todo->description = $todo->input('description');
        $this->todo->completed = false;
        $this->todo->user_id = Auth::user()->id;

        $this->todo->save(); // Save the submitted data
    }

    /**
     * Remove the specified todo
     *
     * @param  integer $id
     * @return void
     */
    public function delete(int $id)
    {
        $this->todo->destroy($id);
    }

    /**
     * Update the specified todo, update columns
     *
     * @param  TodoRequest $request
     * @return void
     */
    public function put(TodoRequest $request)
    {
        // Retrieve the record
        $data = self::get($request->input('id'));

        // List of fields that we'll update
        $data->update($request->only(['title', 'completed', 'description']));
    }
}
8.1.2.7. app/Http/Controllers/TodoController.php

Implements the code to run for each route.

<?php

namespace App\Http\Controllers;

use App\Http\Requests\TodoRequest;
use App\Repositories\TodoRepositoryInterface;
use Response;

class TodoController extends Controller
{
    /**
     * Show the list of todos
     *
     * @param  TodoRepositoryInterface $todoRepository
     * @return void
     */
    public function index(
        TodoRepositoryInterface $todoRepository
    ) {
        // Retrieve all todos
        $datas = $todoRepository->index();

        // call the list.blade.php view and pass the data
        return view('list', compact('datas'));
    }

    /**
     * Show the detail of a todo
     *
     * @param  integer                 $id
     * @param  TodoRepositoryInterface $todoRepository
     * @return void
     */
    public function show(int $id, TodoRepositoryInterface $todoRepository)
    {
        $data = $todoRepository->get($id);

        return view('show', compact('data'));
    }

    /**
     * Show the submission form
     *
     * @return void
     */
    public function getForm()
    {
        return view('form');
    }

    /**
     * The form is being submitted, save the data, create a new todo
     * This function answer to the POST method, not PUT
     *
     * @param  TodoRequest             $request
     * @param  TodoRepositoryInterface $todoRepository
     * @return void
     */
    public function postForm(
        TodoRequest $request,
        TodoRepositoryInterface $todoRepository
    ) {
        $todoRepository->save($request);

        // Show the form back and pass a "Success" alert
        return redirect()->back()
            ->with('message', '<div class="alert alert-success" role="alert">' .
                'Successfully created</div>');
    }

    /**
     * Edit an existing record : retrieve the record and show the form
     *
     * @param  integer                 $id
     * @param  TodoRepositoryInterface $todoRepository
     * @return void
     */
    public function edit(int $id, TodoRepositoryInterface $todoRepository)
    {
        $data = $todoRepository->get($id);

        return view('form', compact('data'));
    }

    /**
     * Delete a todo
     *
     * @param  integer                 $id
     * @param  TodoRepositoryInterface $todoRepository
     * @return void
     */
    public function delete(int $id, TodoRepositoryInterface $todoRepository)
    {
        $todoRepository->delete($id);

        // Return a JSON string that will be used in an Ajax request
        return Response::json([
            'status' => true,
            'message' => '<div class="alert alert-success" role="alert">' .
                'Successfully deleted</div>'
        ]);
    }

    /**
     * Update a todo record
     * This function answer to the PUT method (updating an existing record)
     *
     * @param  TodoRequest             $request
     * @param  TodoRepositoryInterface $todoRepository
     * @return void
     */
    public function put(TodoRequest $request, TodoRepositoryInterface $todoRepository)
    {
        $todoRepository->put($request);

        // Redirect to the edit form
        return redirect()->back()
            ->with('message', '<div class="alert alert-success" role="alert">' .
                'Successfully updated</div>');
    }
}
8.1.2.8. app/Http/Requests/TodoRequest.php

Define who can use our Todo records and specify which rules should be applied to records.

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class TodoRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'title' => 'required|string|max:100',
            'completed' => 'boolean'
        ];
    }
}
8.1.2.9. resources/views/form.blade.php

Show the submission form.

@extends('master')

@section('content')

  {{-- Display the name of the connected user --}}
  <div class="panel-heading">Hi {{ Auth::user()->name }}, please add your new Todo below</div>  

  <div class="panel-body">   

    {{-- 
      Messages sent by the controller like in
        return view('...')->with('message', '<div class="...">success</div>'))
      will be displayed here
    --}}
    @if(Session::has('message'))
      {!! Session::get('message') !!}
    @endif

    {{--
      The method will be 'POST' unless when $data is set.
      Not set : we're creating a new record so the HTTP method will be POST
      Set     : we're editing an existing record so the HTTP method will be PUT
    --}}
    {!! Form::open(['route' => 'storeTodo', 'method' => (isset($data) ? 'PUT' : 'POST')]) !!}

      {{--
        If we're editing an existing record, we'll need his ID in our controller.
        If we're creating a new record, we don't have yet an ID => 0
      --}}
      {!! Form::hidden('id', $data->id ?? 0) !!}

      <div class="form-group {!! $errors->has('todo') ? 'has-error' : '' !!}"> 
        {{--
          Output the "Title" entry.
        --}}
        {!! Form::text('title', $data->title ?? '', array('size' => '100', 'class' => 'form-control', 'placeholder' => 'Enter Todo\'s title')) !!}
        {!! $errors->first('title', '<div class="alert alert-danger">:message</div>') !!}
      </div>

      <div class="form-group">
        {{--
          Output the "Completed" flag.
        --}}      
        {!! Form::checkbox('completed', 1, (isset($data) ? ($data->completed==1) : 0))  !!}
        {!! Form::label('completed', 'Completed'); !!}
      </div>

      <div class="form-group {!! $errors->has('todo') ? 'has-error' : '' !!}">
        {{--
          Output the "Description" textarea.
        --}}      
        {!! Form::label('description', 'Description (optional)'); !!}
        {!! Form::textarea('description', $data->description ?? '', array('class' => 'form-control')) !!}
      </div>

      {{--
        Output the submit button
      --}}      
      {!! Form::submit('Submit !') !!}
    {!! Form::close() !!}
  </div>
@endsection

@section('navigation')
  <a href="/">Show all</a>
@endsection
8.1.2.10. resources/views/list.blade.php

Display the list of todos, like in a blog.

@extends('master')

@section('content')

  {{--
    $data is a collection of records
  --}}
  @isset($datas)
    {{--
      For each todo, we'll show his title in a H3
      Clicking on the title will display the todo's details
      We'll also show: todo's description and his author.
    --}}
    @foreach($datas as $data)
      <h3><a href="todo/{{ $data->id }}">{{ $data->title }}</a></h3>
      <p>{{ $data->description }}</p>
      <small>Author: {{ $data->user->name }}</small>
      <hr/>
    @endforeach
  @endisset
@endsection

@section('navigation')
  <a href="/todo" >Add new item</a>
@endsection
8.1.2.11. resources/views/master.blade.php

Our page’s master. Defines how pages should looks like.

<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  {{--
    Important for our Ajax requests: we need to protect our server's requests
    with the generated session token
  --}}
  <meta name="csrf-token" content="{{ csrf_token() }}">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Some stupid Todos application</title>
  <link media="screen" rel="stylesheet" type="text/css" href="/css/app.css" />
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
</head>
<body>
  <main role="main">
    <div class="jumbotron">
      <div class="container">
        <h1 class="display-3">Some stupid Todos application</h1>
        <small>A simple Laravel application, learning purposes</small>
      </div>
    </div>
  </main>
  <div class="container">
    @yield('content')
    <hr/>
    @yield('navigation')
  </div>
  @yield('script')
</body>
</html>
8.1.2.12. resources/views/show.blade.php

Show the detail of a todo. If a valid user is logged-in, display action’s buttons.

@extends('master')

@section('content')

  {{-- 
    Display the detail of a todo; make sure we've one
  --}}
  @isset($data)
    {{-- 
      Show informations like title, description and timestamps
    --}}
    <h3>{{ $data->title }}</h3>
    <p>{{ $data->description }}</p>
    <small>
      Created at: {{ $data->created_at }}
      <br/>
      Last updated: {{ $data->updated_at }}
      <br/>

      {{--
        $data->user isn't a column but, in our model, the user() function
        returns an object which represent a record of the users table.
        So, through $data->user we can access to the user's name, email, ...
      --}}
      Author: {{ $data->user->name }}
    </small>

    {{--
      Only for logged-in users, show action's buttons
      --}}
    @if(Illuminate\Support\Facades\Auth::check())      
      <hr/>
      <span class="buttons">
        <input type="button" value="Update" class="edit"/> - 
        <input type="button" value="Delete" class="delete"/>
      </span>
    @endif

  @endisset

@endsection

@section('navigation')
  <a href="/todo" >Add new item</a> - <a href="/">Show all</a>
@endsection

@section('script')
{{--
  Add our script for our buttons
--}}
<script defer="defer">
  $('.delete, .edit').click(function(){

    if (this.value.toUpperCase() === 'DELETE') {
      // Add the csrf-token protection but only when the request is 
      // made on the same site (no cross-domain). 
      // Don't share the token outside
      $.ajaxSetup({
        beforeSend: function(xhr, type) {
          if (!type.crossDomain) {
              xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'));
          }
        }
      });

      // By clicking on the delete button, make an Ajax request
      $.ajax({
        url: '/todo/{{ $data->id }}',
        type: 'DELETE',
        contentType: 'application/json',
        success: function (data) {
          if (data.hasOwnProperty("message")) {
            // Replace buttons and display the feedback message
            // Indeed, when deleted, we can't anymore edit or delete
            $('.buttons').html(data.message);
          }
        },
        error: function (data, textStatus, errorThrown) {
          console.log(data);
        }
      });
    } else {
      // The user has clicked on the edit button, redirect the browser
      // to the edit page
      window.location.replace('/todo/{{ $data->id }}/edit');
    }
});
</script>
@endsection