Extend

Adding more features

Table des matières

1. Add artisan commands

Create own commands to be used on the command prompt level

1.1. Artisan

1.1.1. Create a new command – cache

In a DOS prompt session, run

php artisan make:command clear

A new file will be created: /app/Console/Commands/clear.php.

Edit the file and replace with this content:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class clear extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'todos:clear';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Clear todos app tables';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        \DB::table('todos')->truncate();

        \DB::table('users')->where('email', 'like', '%@todos.com')->delete();

        $this->info('>> Todos - Clear done <<');
    }
}

Now, we can easily truncate the table by running:

php artisan todos:clear

1.1.2. Create a new command – populate

In a DOS prompt session, run

php artisan make:populate clear

A new file will be created: /app/Console/Commands/populate.php.

Edit the file and replace with this content:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Faker;

class populate extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'todos:populate';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Add fake data\'s to tables';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        // First clear previous content
        $this->call('todos:clear');

        // Ask how many items to create, default 20
        $wNbr = $this->ask('How many todos do you want to create?', 20);

        // 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 < $wNbr; $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()
                ]
            ]);
        }

        $this->info('>> Todos - Populating done <<');
    }
}

Now, we can now easily add fake data to tables by running:

php artisan todos:populate

Just ask how many items should be created (default 20) and it’s done.

Populate](https://www.marknotes.fr/docs/Development/Web/Laravel/A.%20Labs/0.%20Todos%20app/2.%20Extend/1.%20Artisan/.images/populate.png)

Thanks to our populate Artisan command, we can now remove the file /database/migrations/2018_08_01_000000_populate_testing_datas.php, we don’t need it anymore.

2. Use a resource and simplify routings

2.1. Using resources

2.1.1. Create a resource

php artisan make:controller TodoController --resource

2.1.2. Update routes

Edit /routes/web.php and replace any existing routes for Todo by

Route::resource('todos', 'TodoController');

2.1.3. Update our previous controller

Edit the /app/Http/Controllers/TodoController.php controller and use this code:

<?php

namespace App\Http\Controllers;

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

class TodoController extends Controller
{
    protected $todoRepository;

    public function __construct(TodoRepositoryInterface $todoRepository)
    {
        $this->todoRepository = $todoRepository;

        // Methods in the controller needs to be logged in
        // Except getting the list of todos (method index) and showing
        // one of them (method show).
        // All other methods requires a valid login.
        $this->middleware('auth', ['except' => ['index', 'show']]);
    }

    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        // Retrieve all todos
        $datas = $this->todoRepository->index();

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

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
        return view('create');
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(TodoRequest $request)
    {
        $todo = $this->todoRepository->store($request);

        return redirect()->route('todos.show', ['id' => $todo->id])->withOk('Todo has been successfully created');
    }

    /**
     * Display the specified resource.
     *
     * @param  int                       $id
     * @return \Illuminate\Http\Response
     */
    public function show(int $id)
    {
        $data = $this->todoRepository->getById($id);

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

    /**
     * Show the form for editing the specified resource.
     *
     * @param  int                       $id
     * @param  TodoRepositoryInterface   $todoRepository
     * @return \Illuminate\Http\Response
     */
    public function edit(int $id)
    {
        $data = $this->todoRepository->getById($id);

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

    /**
     * Update the specified resource in storage.
     * This function answer to the PUT method (updating an existing record)
     *
     * @param  TodoRequest               $request
     * @param  TodoRepositoryInterface   $todoRepository
     * @param  int                       $id
     * @return \Illuminate\Http\Response
     */
    public function update(TodoRequest $request, int $id)
    {
        $this->todoRepository->update($id, $request);

        // Redirect to the edit form
        return redirect()->route('todos.edit', ['id' => $id])->withOk('Successfully updated');
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int                       $id
     * @param  TodoRepositoryInterface   $todoRepository
     * @return \Illuminate\Http\Response
     */
    public function destroy(int $id)
    {
        $this->todoRepository->destroy($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>'
        ]);
    }
}

As we can see in your constructor, we’ve the following line to add authentication layer to our controller:

$this->middleware('auth', ['except' => ['index', 'show']]);

This means that every functions in the controller requires a valid logged-in user except the ones publicly accessible: index and show. So, we’ll easily prevent anyone to delete a resource f.i. (function destroy) if not connected. Easy!

2.1.4. Update our previous views

In the first part of the lab (1. Create), we’ve hard-coded URLs in our views.

For instance, in the /resources/views/list.blade.php, we have this line:

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

This means that we’ve hard-coded our URL for showing a given Todo. The URL should be todo/ follow by the ID of the given TODO.

Now, we’ll use our named routes:

<h3><a href="{{ route('todos.show', ['id' => $data->id]) }}">{{ $data->title }}</a></h3>

That name (todos.show) has been defined in our /web/routes.php file. We can retrieve the list of routes with Artisan:

php artisan route:list

Update the three others views like that i.e. no more hard-coded URL but using named routes.

2.1.5. Create a view for the update

To be as faithful as possible to the standard, don’t use anymore one view for both the creation (POST with todos.store as route) and the modification (PUT with todos.update as route) but use two different views.

So, we’ll no more use the unique /resources/views/form.blade.php with both logic for insertion or updates but two files: /resources/views/create.blade.php and /resources/views/edit.blade.php.

Remove /resources/views/form.blade.php and create the two views like described below:

2.1.5.1. resources/views/create.blade.php
@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

    {!! Form::open(['route' => 'todos.store']) !!}

    <div class="form-group {!! $errors->has('todo') ? 'has-error' : '' !!}"> 
        {{--
          Output the "Title" entry.
        --}}
        {!! Form::text('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, 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', '', array('class' => 'form-control')) !!}
      </div>

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

@section('navigation')
  <a href="{{ route('todos.index') }}">Show all</a>
@endsection
2.1.5.2. resources/views/edit.blade.php

https://laravel.com/docs/4.2/html#form-model-binding

The edit form should display the current values: the field title should be linked with the title of the todo, the field description, and so on.

This can be automatically done by Laravel! Instead of using Form::open, we’ll use Form::model. This allow automatic linking.

@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

    {!! Form::model($data, ['route' => ['todos.update', $data->id], 'method' => 'PUT']) !!}

      <div class="form-group {!! $errors->has('todo') ? 'has-error' : '' !!}"> 
        {{--
          Output the "Title" entry.
        --}}
        {!! 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">
        {{--
          Output the "Completed" flag.
        --}}      
        {!! Form::checkbox('completed', 1, null)  !!}
        {!! 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', null, array('class' => 'form-control')) !!}
      </div>

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

@section('navigation')
  <a href="{{ route('todos.index') }}">Show all</a> - <a href="{{ route('todos.show', ['id' => $data->id]) }}">Show</a>
@endsection

2.1.6. Rename actions and files

To better follow the structure and naming convention of Laravel, rename some actions and files.

2.1.6.1. Rename actions in the controller
Old name New name
delete destroy
2.1.6.2. Rename files
Old name New name
/resources/views/list.blade.php /resources/views/index.blade.php

3. Add pagination

3.1. Pagination

https://openclassrooms.com/fr/courses/3613341-decouvrez-le-framework-php-laravel/3617795-les-ressources-2–2-et-les-erreurs#/id/r-3617622

By getting the list of todos, we could have a list with dozens of entries. Pagination will help to work with big lists.

3.1.1. Add getPaginate() in the interface

Edit /app/Repositories/TodoRepositoryInterface.php and add this new declaration:

public function getPaginate(int $n);

3.1.2. Add getPaginate() in the repository

Edit /app/Repositories/TodoRepository.php and add this new function:

public function getPaginate(int $n)
{
    return $this->todo->paginate($n);
}

Note: we can use sorting feature, f.i.,

public function getPaginate(int $n)
{
    return $this->todo
        ->orderBy('todos.created_date', 'desc')
        ->paginate($n);
}

->orderBy('todos.created_date', 'desc') can be simplified in ->latest('todos.created_at'), same result. The opposite is oldest('todos.created_at').

3.1.3. Edit the controller

Edit /app/Http/Controllers/TodoController.php and update the index() function like this:

public function index()
{
    // Retrieve all todos
    $datas = $this->todoRepository->getPaginate($this->nbrPerPage);
    $links = $datas->render();

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

3.1.4. Edit the index view

Edit the /resources/views/index.blade.php view and add the pagination’s links:

@section('navigation')
  {{ $links }}
  <a href="{{ route('todos.create') }}">Add new item</a>
@endsection

The $links variable is a HTML content created by the render() function.

Pagination

4. Bootstrap

4.1. Boostrap style

4.1.1. Update master.blade.php

Edit /resources/views/master.blade.php and add external css. Update the <head> section like this:

<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 rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.2.0/css/all.css" integrity="sha384-hWVjflwFxL6sNzntih27bfxkr27PmbbK/iSvJ+a4+0owXq79v+lsFkW54bOGbiDQ" crossorigin="anonymous">
  <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>

  <style>textarea { max-height:150px; }</style>
</head>

4.1.2. Update create.blade.php

That view is responsible for showing the creation form that allow to add new todo.

Edit /resources/views/create.blade.php and update how the submit button is made. Add also a back button and remove the content of the navigation section:

{!! Form::submit('Submit !', array('class' => 'btn btn-sm btn-primary')) !!}

<a href="javascript:location.href='{{ route('todos.index') }}'" class="btn btn-sm btn-success">
  <span class="glyphicon glyphicon-circle-arrow-left"></span> Back
</a>
@section('navigation')
@endsection

4.1.3. Update edit.blade.php

That view is responsible for showing the update form that allow to edit existing todo.

Edit /resources/views/edit.blade.php and update how the submit button is made. Add also a back button and remove the content of the navigation section:

{!! Form::submit('Submit !', array('class' => 'btn btn-sm btn-primary')) !!}

<a href="javascript:location.href='{{ route('todos.index') }}'" class="btn btn-sm btn-success">
  <span class="glyphicon glyphicon-circle-arrow-left"></span> Back
</a>
@section('navigation')
@endsection

4.1.4. Update index.blade.php

That view is responsible for showing the list of Todos.

For each todo, we’ll add button for:

Edit /resources/views/index.blade.php and update the foreach construction:

@if(session()->has('ok'))
    <div class="alert alert-success alert-dismissible">{!! session('ok') !!}</div>
@endif

@foreach($datas as $data)
<h3><a href="{{ route('todos.show', ['id' => $data->id]) }}">#{{ $loop->iteration. " - " . $data->title }}</a></h3>

<div style="padding:5px 0 15px 0;">
    <button type="submit" class="btn btn-sm btn-success"
        onclick="location.href='{{ route('todos.show', ['id' => $data->id]) }}'">
        <i class="glyphicon glyphicon-eye-open"></i> Show
    </button>

    <button type="submit" class="btn btn-sm btn-primary"
        onclick="location.href='{{ route('todos.edit', ['id' => $data->id]) }}'">
        <i class="glyphicon glyphicon-edit"></i> Edit
    </button>

    {!! Form::open(['method' => 'DELETE', 'route' => ['todos.destroy', $data->id], 'style' => 'display:inline']) !!}

    <button type="submit" class="btn btn-sm btn-danger delete" onclick="return confirm('Remove this record?')">
        <i class="glyphicon glyphicon-remove"></i> Delete
    </button>

    {!! Form::close() !!}
</div>
<p>{{ $data->description }}</p>
<small>Author: {{ $data->user->name }}</small>
<hr/>
@endforeach

This change implements three buttons between the title and the description of the todo:

Buttons

Note: for the Delete action, we need to create a form since, except with Ajax, only a form can send a DELETE header to the server.

The destroy() function of the controller return a ok message:

return redirect()->route('todos.index')->withOk('Successfully removed');

That message can be displayed in the view by using the Session:

@if(session()->has('ok'))
    <div class="alert alert-success alert-dismissible">{!! session('ok') !!}</div>
@endif

Deleted

4.1.5. Update show.blade.php

The Show view will display a given todo. Add bootstrap style to that view.

Edit /resources/views/show.blade.php and replace with this:

@extends('master')

@section('content')

  <div class="msg hide alert alert-success alert-dismissible">&nbsp;</div>

  {{--
    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>

  @endisset

@endsection

@section('navigation')
  {{--
    Only for logged-in users, show action's buttons
  --}}
  @if(Illuminate\Support\Facades\Auth::check())
    <a href="javascript:history.back()" class="back btn btn-sm btn-success">
      <span class="glyphicon glyphicon-circle-arrow-left"></span> Back
    </a>
    <span class="buttons">
      <button class="btn btn-sm btn-primary edit">
        <i class="glyphicon glyphicon-edit"></i> Update
      </button>
      <button class="btn btn-sm btn-danger delete">
        <i class="glyphicon glyphicon-remove"></i> Delete
      </button>
    </span>
  @endif
@endsection

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

    if ($(this).hasClass('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: '{{ route('todos.destroy', $data->id) }}',
        type: 'DELETE',
        contentType: 'application/json',
        success: function (data) {
          if (data.hasOwnProperty("message")) {
            // Show the message
            $('.msg').html(data.message).removeClass('hide');
            // And remove buttons.
            $('.buttons').remove();
            // The back button should refresh the page so don't
            // use history.back() anymore
            $('.back').attr("href", "{{ route('todos.index') }}");
          }
        },
        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('{{ route('todos.edit', ['id' => $data->id]) }}');
    }
});
</script>
@endsection

In the javascript section, we’ll replace the link for the Go back button when the user has clicked on the Delete. This because history.back() will show the page from the browser’s cache and therefore an obsolete page since the record has been removed. So, in this specific case, the page should be refreshed.

$('.back').attr("href", "{{ route('todos.index') }}");

5. Add navigation bar

5.1. Adding navigation bar

5.1.1. Create the navbar view

Create /resources/views/navbar.blade.php with this content:

<nav class="navbar sticky-top navbar-light" style="background-color: #e3f2fd;">
    <ul class="nav navbar-nav navbar-right">
@guest
        <li><a href="/login"><i class="fas fa-sign-in-alt"></i> Login</a></li>
@else
        <li><a href="/logout"><i class="fas fa-sign-out-alt"></i> Logout</a></li>
@endauth
    </ul>
</nav>

The use of the @guest directive makes things easy: when no user is connected, display a Login link otherwise, display a logout link.

Note: We can also use @auth ... @else ... @endauth, just the opposite approach.

5.1.2. Include navbar in master

Edit /resources/views/master.blade.php and include the navbar:

<body>
  @include('navbar')
  <main role="main">

We’ve put the navbar just after the <body> tag. The complete code for master.blade.php with Bootstrap 4 and FontAwesome 5:

<!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 rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.2.0/css/all.css" integrity="sha384-hWVjflwFxL6sNzntih27bfxkr27PmbbK/iSvJ+a4+0owXq79v+lsFkW54bOGbiDQ" crossorigin="anonymous">
  <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
  <style>
    textarea { max-height:150px; }
  </style>
</head>
<body>
  @include('navbar')
  <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>

6. Blade include

6.1. Blade include

Instead of creating our buttons in each view, use @include instead.

6.1.1. Problem with show.blade.php

At this stage, the show view has this code:

@section('navigation')
  @if(Illuminate\Support\Facades\Auth::check())
    <a href="javascript:history.back()" class="back btn btn-sm btn-success">
      <span class="glyphicon glyphicon-circle-arrow-left"></span> Back
    </a>
    <span class="buttons">
      <button class="btn btn-sm btn-primary edit">
        <i class="glyphicon glyphicon-edit"></i> Update
      </button>
      <button class="btn btn-sm btn-danger delete">
        <i class="glyphicon glyphicon-remove"></i> Delete
      </button>
    </span>
  @endif
@endsection

There are three links for Back, Update and Delete.

Since these buttons are also used in other views, we’ll externalize them.

6.1.2. Create a folder for buttons

Create the folder /resources/views/buttons and add these four views.

  1. add.blade.php

Add new item button.

<button type="submit" class="btn btn-sm btn-primary"
  onclick="location.href='{{ route('todos.create') }}'">
  <i class="far fa-plus-square"></i> Add new item
</button>
  1. back.blade.php

Go back button.

<a href="javascript:history.back()" class="back btn btn-sm btn-success">
    <i class="far fa-hand-point-left"></i> Back
</a>
  1. delete.blade.php

Delete button. Since we need to use the DELETE method, we need to create a form.

{!! Form::open(['method' => 'DELETE', 'route' => ['todos.destroy', $data->id], 'style' => 'display:inline']) !!}

<button type="submit" class="btn btn-sm btn-danger delete" onclick="return confirm('Remove this record?')">
    <i class="far fa-trash-alt"></i> Delete
</button>

{!! Form::close() !!}
  1. edit.blade.php

Edit button, go to the edit form.

<button type="submit" class="btn btn-sm btn-primary"
    onclick="location.href='{{ route('todos.edit', ['id' => $data->id]) }}'">
    <i class="far fa-edit"></i> Edit
</button>
  1. show.blade.php

Show button, display the detail page.

<button type="submit" class="btn btn-sm btn-success"
    onclick="location.href='{{ route('todos.show', ['id' => $data->id]) }}'">
    <i class="far fa-eye"></i> Show
</button>

6.1.3. Use buttons in existing views

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

@section('navigation')
  @if(Illuminate\Support\Facades\Auth::check())
    <a href="javascript:history.back()" class="back btn btn-sm btn-success">
      <span class="glyphicon glyphicon-circle-arrow-left"></span> Back
    </a>
    <span class="buttons">
      <button class="btn btn-sm btn-primary edit">
        <i class="glyphicon glyphicon-edit"></i> Update
      </button>
      <button class="btn btn-sm btn-danger delete">
        <i class="glyphicon glyphicon-remove"></i> Delete
      </button>
    </span>
  @endif
@endsection

with

@section('navigation')
  @include('buttons.back')
  @auth
    <span class="buttons">
      @include('buttons.edit')
      @include('buttons.delete')
    </span>
  @endauth
@endsection

This code is now simplier: include the back buttons and, if the user is logged-in, include also edit and delete.

7. Final

Final code

7.1. Final code

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

7.1.1. Install a fresh copy

  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. Add fake data: php artisan todos:populate
  13. Run composer require "laravelcollective/html"
  14. This done, start artisan by starting php artisan serve on the DOS prompt.
  15. 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.
  16. Then we can use the application: http://127.0.0.1:8000

7.1.2. Code source

7.1.2.1. env

Our environment file.

APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:SS+CZVwEkBRyzZQTxpogAroaFSbazMf+DtE9J0vftA8=
APP_DEBUG=true
APP_URL=http://127.0.0.1

LOG_CHANNEL=stack

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=todos
DB_USERNAME=root
DB_PASSWORD=

BROADCAST_DRIVER=log
CACHE_DRIVER=file
SESSION_DRIVER=file
SESSION_LIFETIME=120
QUEUE_DRIVER=sync

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null

PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1

MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
7.1.2.2. app
7.1.2.2.1. 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');
    }
}
7.1.2.2.2. app/Console/Commands/clear.php

Our artisan command for clear tables

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class clear extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'todos:clear';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Clear todos app tables';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        \DB::table('todos')->truncate();

        \DB::table('users')->where('email', 'like', '%@todos.com')->delete();

        $this->info('>> Todos - Clear done <<');
    }
}
7.1.2.2.3. app/Console/Commands/populate.php

Our artisan command for populating tables

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Faker;

class populate extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'todos:populate';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Add fake data\'s to tables';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        // First clear previous content
        $this->call('todos:clear');

        // Ask how many items to create, default 20
        $wNbr = $this->ask('How many todos do you want to create?', 20);

        // 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 < $wNbr; $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()
                ]
            ]);
        }

        $this->info('>> Todos - Populating done <<');
    }
}
7.1.2.2.4. app/Http/Controllers/TodoController.php

Implements the code to run for each route.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Http\Requests\TodoRequest;
use App\Repositories\TodoRepositoryInterface;
use App\Repositories\TodoRepository;
use Response;

class TodoController extends Controller
{
    protected $todoRepository;

    protected $nbrPerPage = 5;

    public function __construct(TodoRepositoryInterface $todoRepository)
    {
        $this->todoRepository = $todoRepository;

        // Methods in the controller needs to be logged in
        // Except getting the list of todos (method index) and showing
        // one of them (method show).
        // All other methods requires a valid login.
        $this->middleware('auth', ['except' => ['index', 'show']]);
    }

    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        // Retrieve all todos
        $datas = $this->todoRepository->getPaginate($this->nbrPerPage);
        $links = $datas->render();

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

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
        return view('create');
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(TodoRequest $request)
    {
        $todo = $this->todoRepository->store($request->all());

        return redirect()->route('todos.show', ['id' => $todo->id])->withOk('Todo has been successfully created');
    }

    /**
     * Display the specified resource.
     *
     * @param  int                       $id
     * @return \Illuminate\Http\Response
     */
    public function show(int $id)
    {
        $data = $this->todoRepository->getById($id);

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

    /**
     * Show the form for editing the specified resource.
     *
     * @param  int                       $id
     * @param  TodoRepositoryInterface   $todoRepository
     * @return \Illuminate\Http\Response
     */
    public function edit(int $id)
    {
        $data = $this->todoRepository->getById($id);

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

    /**
     * Update the specified resource in storage.
     * This function answer to the PUT method (updating an existing record)
     *
     * @param  TodoRequest               $request
     * @param  int                       $id
     * @return \Illuminate\Http\Response
     */
    public function update(TodoRequest $request, int $id)
    {
        $this->todoRepository->update($id, $request->all());

        // Redirect to the edit form
        return redirect()->route('todos.edit', ['id' => $id])->withOk('Successfully updated');
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int                       $id
     * @param  TodoRepositoryInterface   $todoRepository
     * @return \Illuminate\Http\Response
     */
    public function destroy(Request $request, int $id)
    {
        $this->todoRepository->destroy($id);

        if (!$request->ajax()) {
            return redirect()->route('todos.index')->withOk('Successfully removed');
        } else {
            // Return a JSON string that will be used in an Ajax request
            return Response::json([
                'status' => true,
                'message' => 'Successfully deleted'
            ]);
        }
    }
}
7.1.2.2.5. 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'
        ];
    }
}
7.1.2.2.6. app/Providers/AppServiceProvider.php

Since we need to bind our interface and our repository.

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Schema;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Schema::defaultStringLength(191);
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(
            'App\Repositories\TodoRepositoryInterface',
            'App\Repositories\TodoRepository'
        );
    }
}
7.1.2.2.7. 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 a pagination
     *
     * @param  int        $n
     * @return Collection
     */
    public function getPaginate(int $n);

    /**
     * Get the list of records of the table
     *
     * @return Collection
     */
    public function index() : Collection;

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

    /**
     * Save the todo
     *
     * @param  array $inputs Submitted datas
     * @return void
     */
    //public function store(TodoRequest $todo) : Todo;
    public function store(array $inputs) : Todo;

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

    /**
     * Update the specified todo, update columns
     *
     * @param  int   $id
     * @param  array $inputs Submitted datas
     * @return void
     */
    public function update(int $id, array $inputs) : Todo;
}
7.1.2.2.8. app/Repositories/TodoRepository.php

Implements functions for our data logic.

<?php

namespace App\Repositories;

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;
    }

    public function getPaginate(int $n)
    {
        return $this->todo->paginate($n);
    }

    /**
     * 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  int  $id
     * @return Todo
     */
    public function getById(int $id) : Todo
    {
        return $this->todo->where('id', $id)->firstOrFail();
    }

    private function save(Todo $todo, array $inputs) : Todo
    {
        $todo->title = $inputs['title'];
        $todo->description = $inputs['description'];

        $todo->save();

        return $todo;
    }

    /**
     * Save the todo
     *
     * @param  array $inputs List of fields present in the creation form
     * @return Todo
     */
    public function store(array $inputs) : Todo
    {
        $todo = new $this->todo();

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

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

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

    /**
     * Update the specified todo, update columns
     *
     * @param  int   $id
     * @param  array $inputs Submitted datas
     * @return void
     */
    public function update(int $id, array $inputs) : Todo
    {
        return $this->save($this->getById($id), $inputs);
    }
}
7.1.2.3. database
7.1.2.3.1. 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');
    }
}
7.1.2.4. resources
7.1.2.4.1. resources/views/create.blade.php

The form for adding a new todo.

@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

    {!! Form::open(['route' => 'todos.store']) !!}

    <div class="form-group {!! $errors->has('todo') ? 'has-error' : '' !!}"> 
        {{--
          Output the "Title" entry.
        --}}
        {!! Form::text('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, 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', '', array('class' => 'form-control')) !!}
      </div>

      {{--
        Output the submit button
      --}}
      {!! Form::submit('Submit !', array('class' => 'btn btn-sm btn-primary')) !!}

      <a href="javascript:location.href='{{ route('todos.index') }}'" class="btn btn-sm btn-success">
        <span class="glyphicon glyphicon-circle-arrow-left"></span> Back
      </a> 
    {!! Form::close() !!}
  </div>
@endsection

@section('navigation')
@endsection
7.1.2.4.2. resources/views/edit.blade.php

The form for the edition of an existing todo.

@extends('master')

@section('content')

  <div class="panel-heading">Need to edit a Todo?</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

    {!! Form::model($data, ['route' => ['todos.update', $data->id], 'method' => 'PUT']) !!}

      <div class="form-group {!! $errors->has('todo') ? 'has-error' : '' !!}"> 
        {{--
          Output the "Title" entry.
        --}}
        {!! 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">
        {{--
          Output the "Completed" flag.
        --}}      
        {!! Form::checkbox('completed', 1, null)  !!}
        {!! 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', null, array('class' => 'form-control')) !!}
      </div>

      {{--
        Output the submit button
      --}}      
      {!! Form::submit('Submit !', array('class'=> 'btn btn-sm btn-primary')) !!}

      @include('buttons.back')

      {!! Form::close() !!}
  </div>
@endsection

@section('navigation')  
@endsection
7.1.2.4.3. resources/views/index.blade.php

Display the list of todos, like in a blog.

@extends('master')

@section('content')

    @if(session()->has('ok'))
    <div class="alert alert-success alert-dismissible">{!! session('ok') !!}</div>
  @endif

  {{--
    $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="{{ route('todos.show', ['id' => $data->id]) }}">#{{ $loop->iteration. " - " . $data->title }}</a></h3>

      <div style="padding:5px 0 15px 0;">
        @include('buttons.show')
        @auth 
          @include('buttons.edit')
          @include('buttons.delete')
        @endauth 
      </div>
      <p>{{ $data->description }}</p>
      <small>Author: {{ $data->user->name }}</small>
      <hr/>
    @endforeach
    <small>Number of todos: {{ count($datas) }}</small>
  @endisset
@endsection

@section('navigation')
  {{ $links }}
  <hr/>
  @include('buttons.add')

@endsection
7.1.2.4.4. 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 rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.2.0/css/all.css" integrity="sha384-hWVjflwFxL6sNzntih27bfxkr27PmbbK/iSvJ+a4+0owXq79v+lsFkW54bOGbiDQ" crossorigin="anonymous">
  <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
  <style>
    textarea { max-height:150px; }
  </style>
</head>
<body>
  @include('navbar') 
  <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>
7.1.2.4.5. 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')

  <div class="msg hide alert alert-success alert-dismissible">&nbsp;</div>

  {{-- 
    Display the detail of a todo; make sure we've one
  --}}
  @isset($data)
    {{-- 
      Show information's 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>

  @endisset

@endsection

@section('navigation')
  @include('buttons.back')
  @auth 
    <span class="buttons">
      @include('buttons.edit')
      @include('buttons.delete')
    </span>
  @endauth
@endsection

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

    if ($(this).hasClass('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: '{{ route('todos.destroy', $data->id) }}',
        type: 'DELETE',
        contentType: 'application/json',
        success: function (data) {
          if (data.hasOwnProperty("message")) {
            // Show the message
            $('.msg').html(data.message).removeClass('hide');
            // And remove buttons.
            $('.buttons').remove();
            // The back button should refresh the page so don't 
            // use history.back() anymore
            $('.back').attr("href", "{{ route('todos.index') }}");
          }
        },
        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('{{ route('todos.edit', ['id' => $data->id]) }}');
    }
});
</script>
@endsection
7.1.2.4.6. resources/views/buttons/add.blade.php

Add a new item button.

<button type="submit" class="btn btn-sm btn-primary" 
  onclick="location.href='{{ route('todos.create') }}'">
  <i class="far fa-plus-square"></i> Add new item
</button>
7.1.2.4.7. resources/views/buttons/back.blade.php

Go back button.

<a href="javascript:history.back()" class="back btn btn-sm btn-success">
    <i class="far fa-hand-point-left"></i> Back
</a>
7.1.2.4.8. resources/views/buttons/delete.blade.php

Delete an item button.

{!! Form::open(['method' => 'DELETE', 'route' => ['todos.destroy', $data->id], 'style' => 'display:inline']) !!}

<button type="submit" class="btn btn-sm btn-danger delete" onclick="return confirm('Remove this record?')">
    <i class="far fa-trash-alt"></i> Delete
</button>

{!! Form::close() !!}
7.1.2.4.9. resources/views/buttons/edit.blade.php

Edit an existing item button.

<button type="submit" class="btn btn-sm btn-primary" 
    onclick="location.href='{{ route('todos.edit', ['id' => $data->id]) }}'">
    <i class="far fa-edit"></i> Edit
</button>
7.1.2.4.10. resources/views/buttons/show.blade.php

Show button, go to the detail page.

<button type="submit" class="btn btn-sm btn-success" 
    onclick="location.href='{{ route('todos.show', ['id' => $data->id]) }}'">
    <i class="far fa-eye"></i> Show
</button>
7.1.2.5. routes
7.1.2.5.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']);

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

// Attach the controller to every action(GET, HEAD, POST, DELETE, ...)
// for the "todos" resource. Use "php artisan route:list" to retrieve all
// routes created by this Route::resource() statement.
Route::resource('todos', 'TodoController');

// Default homepage : show the list of todos
Route::get('/', 'TodoController@index');

Auth::routes();

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