Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zhiephie/2b5c9a7729c427c1687f2f0d2b4eab50 to your computer and use it in GitHub Desktop.
Save zhiephie/2b5c9a7729c427c1687f2f0d2b4eab50 to your computer and use it in GitHub Desktop.
Note from YouTube Laravel 5.8 Tutorial From Scratch

Laravel 5.8 - From The Ground Up

YouTube Laravel 5.8 Tutorial From Scratch

The purpose of the note are to follow the above tutorial, making detailed notes of each step. In the end a summary will be created.

This tutorial uses sqlite, which isn't enable by default in php.ini. Any sql database can be used with Laravel.

Later in this tutorial we create a user, the default password used is 12345678

Notes & disclaimer:

  • The purpose of the note are to follow the above tutorial, making detailed notes of each step.
  • They are not verbatim of the original video.
  • Although the notes are detailed, it is possible they may not make sense out of context.
  • The notes are not intended as a replacement for the video series
    • Notes are more of a companion
    • They allow an easy reference search.
    • Allowing a particular video to be found and re-watched.
  • Code snippets are often used to highlight the code changed, any code prior or post the code snipped is generally unchanged from previous notes, or to highlight only the output of interest. To signify a snippet of a larger code block, dots are normally used e.g.
\\ ...
echo "Hello";
\\ ...

This tutorial uses sqlite, which isn't enable by default in php.ini. Any sql database can be used with Laravel.

Later in this tutorial we create a user, the default password used is 12345678

1. 1 5:22 Laravel 5.8 Tutorial From Scratch - e01 - Installation

Information on setup, requirements etc. (composer).

laravel new my-first-project

Once Laravel is installed, navigate to the project cd my-first-project and serve the folder

php artisan serve

Open the site localhost:8000

2. 2 4:17 Laravel 5.8 Tutorial From Scratch - e02 - Web Routes

Open routes\web.php

Add two new routes:

<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
 */

Route::get('/', function () {
    return view('welcome');
});

Route::get('contact', function () {
    return "Contact us";
});

Route::get('about', function () {
    return "About us";
});

Navigate to localhost:8000/contact and localhost:8000/about to see the messages created by the route.

3. 3 5:39 Laravel 5.8 Tutorial From Scratch - e03 - Views

Views are stored in resources/views/welcome.blade.php is the main page.

Create a new file in the resources/views/ folder called contact.blade.php

<h1>Contact Us</h1>

<p>Company Name</p>
<p>123-123-134</p>

Create another file called about.blade.php

<h1>About US</h1>

<p>Company bio here..</p>

Update the web.php to the new views:

Route::get('/', function () {
    return view('welcome');
});

Route::get('contact', function () {
    return view('contact');
});

Route::get('about', function () {
    return view('about');
});

Navigate to localhost:8000/contact:

Contact Us

Company Name

123-123-134

localhost:8000/about:

About US

Company bio here..

The Web Routes web.php can be refactored to use the view single line notation:

Route::view('/', 'welcome');

Route::view('contact', 'contact');

Route::view('about', 'about');

4. 4 6:21 Laravel 5.8 Tutorial From Scratch - e04 - Passing Data to Views

Single line notation doesn't work when passing in data, to web.php needs have use the get route:

# add a route:
Route::get('customers', function () {
    return view('internals.customers');
});

Note: The view syntax is directory.file (without blade.php) internals.customers, the slash notation can also be used internals/customers

Create a new folder in the resources/views/ called internals, then create a customer.blade.php file in the internals folder, this way the views can be segregated into separate folders.

<h1>Customers</h1>
<ul>
  <li>Customer 1</li>
  <li>Customer 2</li>
  <li>Customer 3</li>
</ul>

Open the website to: localhost:8000/customers:

Customers

* Customer 1
* Customer 2
* Customer 3

Next add data to the customers route, create an array with data and pass the data into the view, inside an array.

Route::get('customers', function () {
    $customers = [
        'John Doe',
        'Jane Doe',
        'Fred Bloggs',
    ];
    return view('internals.customers', [
        'customers' => $customers,
    ]
    );
});

Next update the customers.blade.php to loop thorough the customer data:

<h1>Customers</h1>
<ul>
    <?php
        foreach ($customers as $customer) {
            echo "<li>$customer</li>";
        }
    ?>
</ul>

Refresh the page to see the data:

Customers
* John Doe
* Jane Doe
* Fred Bloggs

The customers.blade.php can be refactored to use the blade syntax

<h1>Customers</h1>
<ul>
    @foreach ($customers as $customer)
        <li>{{ $customer }}</li>
    @endforeach
</ul>

Refresh the page to see the data is still the same

Note: The variable $customers is the key passed into the view 'customer', if the key was anotherName e.g.:

return view('internals.customers', [
        'anotherName' => $customers,
    ]

Then the foreach would be $anotherName:

@foreach ($anotherName as $customer)

5. 5 4:17 Laravel 5.8 Tutorial From Scratch - e05 - Controllers

Instead the web.php route controlling the data to be passed to the view controllers can be created to take care of the logic.

PHP artisan serve was used to create a server for laravel, it has many commands

php artisan
Laravel Framework 5.8.8

Usage:
  command [options] [arguments]

Options:
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
      --env[=ENV]       The environment the command should run under
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Available commands:
  clear-compiled       Remove the compiled class file
  down                 Put the application into maintenance mode
  dump-server          Start the dump server to collect dump information.
  env                  Display the current framework environment
  help                 Displays help for a command
  inspire              Display an inspiring quote
  list                 Lists commands
  migrate              Run the database migrations
  optimize             Cache the framework bootstrap files
  preset               Swap the front-end scaffolding for the application
  serve                Serve the application on the PHP development server
  tinker               Interact with your application
  up                   Bring the application out of maintenance mode
 app
  app:name             Set the application namespace
 auth
  auth:clear-resets    Flush expired password reset tokens
 cache
  cache:clear          Flush the application cache
  cache:forget         Remove an item from the cache
  cache:table          Create a migration for the cache database table
 config
  config:cache         Create a cache file for faster configuration loading
  config:clear         Remove the configuration cache file
 db
  db:seed              Seed the database with records
 event
  event:generate       Generate the missing events and listeners based on registration
 key
  key:generate         Set the application key
 make
  make:auth            Scaffold basic login and registration views and routes
  make:channel         Create a new channel class
  make:command         Create a new Artisan command
  make:controller      Create a new controller class
  make:event           Create a new event class
  make:exception       Create a new custom exception class
  make:factory         Create a new model factory
  make:job             Create a new job class
  make:listener        Create a new event listener class
  make:mail            Create a new email class
  make:middleware      Create a new middleware class
  make:migration       Create a new migration file
  make:model           Create a new Eloquent model class
  make:notification    Create a new notification class
  make:observer        Create a new observer class
  make:policy          Create a new policy class
  make:provider        Create a new service provider class
  make:request         Create a new form request class
  make:resource        Create a new resource
  make:rule            Create a new validation rule
  make:seeder          Create a new seeder class
  make:test            Create a new test class
 migrate
  migrate:fresh        Drop all tables and re-run all migrations
  migrate:install      Create the migration repository
  migrate:refresh      Reset and re-run all migrations
  migrate:reset        Rollback all database migrations
  migrate:rollback     Rollback the last database migration
  migrate:status       Show the status of each migration
 notifications
  notifications:table  Create a migration for the notifications table
 optimize
  optimize:clear       Remove the cached bootstrap files
 package
  package:discover     Rebuild the cached package manifest
 queue
  queue:failed         List all of the failed queue jobs
  queue:failed-table   Create a migration for the failed queue jobs database table
  queue:flush          Flush all of the failed queue jobs
  queue:forget         Delete a failed queue job
  queue:listen         Listen to a given queue
  queue:restart        Restart queue worker daemons after their current job
  queue:retry          Retry a failed queue job
  queue:table          Create a migration for the queue jobs database table
  queue:work           Start processing jobs on the queue as a daemon
 route
  route:cache          Create a route cache file for faster route registration
  route:clear          Remove the route cache file
  route:list           List all registered routes
 schedule
  schedule:run         Run the scheduled commands
 session
  session:table        Create a migration for the session database table
 storage
  storage:link         Create a symbolic link from "public/storage" to "storage/app/public"
 vendor
  vendor:publish       Publish any publishable assets from vendor packages
 view
  view:cache           Compile all of the application's Blade templates
  view:clear           Clear all compiled view files

This lesson will focus on make:controller, to get all the command run:

PHP artisan help make:controller
Description:
  Create a new controller class

Usage:
  make:controller [options] [--] <name>

Arguments:
  name                   The name of the class

Options:
  -m, --model[=MODEL]    Generate a resource controller for the given model.
  -r, --resource         Generate a resource controller class.
  -i, --invokable        Generate a single method, invokable controller class.
  -p, --parent[=PARENT]  Generate a nested resource controller class.
      --api              Exclude the create and edit methods from the controller.
  -h, --help             Display this help message
  -q, --quiet            Do not output any message
  -V, --version          Display this application version
      --ansi             Force ANSI output
      --no-ansi          Disable ANSI output
  -n, --no-interaction   Do not ask any interactive question
      --env[=ENV]        The environment the command should run under
  -v|vv|vvv, --verbose   Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

To create a customers controller:

PHP artisan make:controller CustomersController
# Controller created successfully.

Note: Capitalisation of the name and it is plural.

Open CustomersController.php (it is in app\Http\Controllers)

The scaffolding for the controller has been created. create a new public function called list and Cut and paste the logic from web.php, add Joe Bloggs as a new customer name.

<?php

namespace App\Http\Controllers;

class CustomersController extends Controller
{
    public function list()
    {
        $customers = [
            'John Doe',
            'Jane Doe',
            'Fred Bloggs',
            'Joe Bloggs',
        ];
        return view(
            'internals.customers',
            [
                'customers' => $customers,
            ]
        );
    }
}

The web.php route will need to call the CustomerController list method, it can do it like this:

Route::get('customers', 'CustomersController@list');

Open the website to see there is no error, the page will display as before, with Joe Bloggs displayed too: localhost:8000/customers

Customers
* John Doe
* Jane Doe
* Fred Bloggs
* Joe Bloggs

The controller method can be called anything, however there is a naming convention for controller methods, this will be covered in later videos.

6. 6 8:21 Laravel 5.8 Tutorial From Scratch - e06 - Blade Tempting Basics

Blade is the rendering engine used in Laravel to parse HTML, it is very powerful.

Start by creating a new file resources\views\layout.blade.php, this has the basic scoffing of a nav from Bootstrap, the content

<!DOCTYPE html>
<html lang=" {{ str_replace('_','-', 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" />
    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />

    <title>Document</title>
  </head>
  <body>
    <ul class="nav">
      <li class="nav-item">
        <a class="nav-link" href="/">Home</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="about">About Us</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="contact">Contact Us</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="customers">Customer List</a>
      </li>
    </ul>
    <div class="container">
      @yield('content')
    </div>

    <script
      src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
      integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
      crossorigin="anonymous"
    ></script>
  </body>
</html>

The customers.blade.php can be amened to pass only the content of the body:

@extends('layout')
@section('content')
<h1>Customers</h1>

<ul>
    @foreach ($customers as $customer)
    <li>{{ $customer }}</li>
    @endforeach
</ul>
@endsection

Add extends and section to the about and contact views. Create a new view called home.blade.php

@extends('layout')
@section('content')
    <h1>Welcome to Laravel 5.8</h1>
@endsection

Update the web.php route for '/' from 'welcome' to 'home':

Route::view('/', 'home');

The four page website is not prepared.

7. 7 10:20 Laravel 5.8 Tutorial From Scratch - e07 - SQLite Database

Laravel has a choice of many databases. Databases can be mix and match and different databases can be used for development and production (I don't think his is recommended).

For this demo we will use sqlite. Open .env file, change the DB*_ information (used for MySQL)

DB_CONNECTION=sqlite
Description:
  Create a new Eloquent model class

Usage:
  make:model [options] [--] <name>

Arguments:
  name                  The name of the class

Options:
  -a, --all             Generate a migration, factory, and resource controller for the model
  -c, --controller      Create a new controller for the model
  -f, --factory         Create a new factory for the model
      --force           Create the class even if the model already exists
  -m, --migration       Create a new migration file for the model
  -p, --pivot           Indicates if the generated model should be a custom intermediate table model
  -r, --resource        Indicates if the generated controller should be a resource controller
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
      --env[=ENV]       The environment the command should run under
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Create an empty file called database/database.sqlite

Migrations are used to setup or modify the database, including adding fields. In production migrations are not normally undone (rollback). Migrations can be run from the command line/shell

php artisan migrate

Laravel ships with a users table and resets table.

Models are used to define the tables in a database. Models are singular. For details on models run the help command:

php artisan help make:model

To make a new model for Customer (singular) run the command:

php artisan make:model Customer -m
Model created successfully.
Created Migration: 2019_03_30_114129_create_customers_table

The table is created in database/migrations/2019_03_30_114129_create_customers_table

Open it and see the model up() contains the scaffolding for defining a table, to add the table.

// class definition
public function up()
{
    Schema::create('customers', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->string('name') // add this line.
        $table->timestamps();
    });
}
// down model

Run the migrate command:

php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table
Migrating: 2019_03_30_114129_create_customers_table
Migrated:  2019_03_30_114129_create_customers_table

The user and password_resets tables are built in, customers is the table created above.

Laravel has a built in command tool called tinker, to access resources.

php artisan tinker
# Psy Shell v0.9.9 (PHP 7.3.3 — cli) by Justin Hileman
>>> Customer::all();

An empty array is returned:

[!] Aliasing 'Customer' to 'App\Customer' for this Tinker session.
=> Illuminate\Database\Eloquent\Collection {#2927
     all: [],
   }

To create a new customer, from the tinker command line (>>> are the commands run and => are the responses):

>>> $customer = new Customer();
=> App\Customer {#2926}
>>> $customer->name = 'John Doe';
=> "John Doe"
>>> $customer->save();
=> true
>>> Customer::all();
=> Illuminate\Database\Eloquent\Collection {#2929
     all: [
       App\Customer {#2930
         id: "1",
         name: "John Doe",
         created_at: "2019-03-30 12:00:39",
         updated_at: "2019-03-30 12:00:39",
       },
     ],
   }
>>>

In the CustomersController change the list method instead of returning an array of customers it pull the data from the database:

class CustomersController extends Controller
{
    public function list()
    {
        $customers = Customer::all();
        // dd($customers); // uncomment to see the output
        return view(
            'internals.customers',
            [
                'customers' => $customers,
            ]
        );
    }
}

the dd($customers) is called dump and die, which will return all the $customers data then stop. Open the website to customer, and the data in items has been seen. Expand attributes to see the name. Comment it out (or remove) after viewing the output.

Open the customers view and change the

@extends('layout')
@section('content')
    <h1>Customers</h1>

    <ul>
        @foreach ($customers as $customer)
            <li>{{ $customer->name }}</li>
        @endforeach
    </ul>
@endsection

Add another customer using Tinker:

php artisan tinker
>>> $customer = new Customer();
[!] Aliasing 'Customer' to 'App\Customer' for this Tinker session.
=> App\Customer {#2921}
>>> $customer->name = 'Jane Doe';
=> "Jane Doe"
>>> $customer->save();
=> true

As before >>> are the php commands => is the output.

Open the website and navigate to Customers List to see the customers.

8. 8 6:12 Laravel 5.8 Tutorial From Scratch - e08 - Adding Customers To The Database

In the Customers list view a form can be created to add a customer open resources\views\internals\customers.blade.php:

<form action="customers" method="POST" class="pb-5">
    <div class="input-group">
        <input type="text" name="name" id="name">
    </div>
    <button type="submit" class="btn btn-primary">Add Customer</button>
</form>

The web.php route needs to have a new post route, add the following line:

Route::post('customers', 'CustomersController@store');

Open the app\Http\Controllers\CustomersController.php. Add a new public function for the store method:

public function store()
    {
        dd(request('name'));
    }

Return to the website, customer list and try to submit any name, an error will display 419 | Page Expired. This is a built in function deliberately shown to explain it it a CSRF measure for cross site security. Only the form on the laravel site can be set to post the data to the database. to fix this add the blade directive @csrf in the form

<form action="customers" method="POST" class="pb-5">
    <div class="input-group">
        <input type="text" name="name" id="name">
    </div>
    <button type="submit" class="btn btn-primary">Add Customer</button>
    @csrf
</form>

Now refresh the website and try and add another name, the data will display once submitted.

There are a few ways to handle to data, the long way is:

public function store()
{
    $customer = new Customer();
    $customer->name = request('name');
    $customer->save();

    return back();
}

Try to add a customer, they will be saved to the database and the view will automatically update.

Try to add an empty name, the form will error with:

Illuminate \ Database \ QueryException (23000)
SQLSTATE[23000]: Integrity constraint violation:
19 NOT NULL constraint failed: customers.name
(SQL: insert into "customers" ("name", "updated_at",
"created_at") values (, 2019-04-01 14:00:33,
2019-04-01 14:00:33))

This will be fixed in the next episode.

9. 9 4:41 Laravel 5.8 Tutorial From Scratch - e09 - Form Validation

Update the app\Http\Controllers\CustomersController.php to add a validation rule of min:3

public function store()
{
    $data = request()->validate([
        'name' => 'required|min:3'
    ]);

    $customer = new Customer();
    $customer->name = request('name');
    $customer->save();

    return back();
}

Use the {{ $error }} blade helper to display an errors to the form:

<form action="customers" method="POST" class="pb-5">
    <div class="input-group">
        <input type="text" name="name" id="name">
    </div>
    // Add this div block to display any returned errors.
    <div class="text-danger">
        <small>{{ $errors->first('name') }}</small>
    </div>
    <button type="submit" class="btn btn-primary btn-sm">Add Customer</button>
    @csrf
</form>

If any errors are made they will display in red under the input box.

Laravel Validation rules.

Have a go at adding an email field to the input field, database and controller.

10. 10 8:18 Laravel 5.8 Tutorial From Scratch - e10 - Adding Email For Customers

The table is created in database/migrations/2019_03_30_114129_create_customers_table

public function up()
{
    Schema::create('customers', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->string('name');
        $table->string('email')->unique(); // add this line
        $table->timestamps();
    });
}

To rollback one migration and re-run the migration:

php artisan migrate:rollback
php artisan migrate

app/Http/Controllers/CustomersController.php, store method:

public function store()
{
    $data = request()->validate([
        'name' => 'required|min:3',
        'email' => 'required|email|unique:customers,email'
    ]);

    $customer = new Customer();
    $customer->name = request('name');
    $customer->email = request('email');

    $customer->save();

    return back();
}

resources/views/internals/customers.blade.php

@extends('layout')
@section('content')
    <h1>Customers</h1>
    <form action="customers" method="POST" class="pb-5">
        <div class="input-group">
        <input type="text" name="name" id="name" placeholder="Customer name" value="{{ old('name') }}">
            <div class="text-danger ml-3">
                <small>{{ $errors->first('name') }}</small>
            </div>
        </div>

        <div class="input-group my-3">
            <input type="text" name="email" id="email" placeholder="Customer E-Mail" value="{{ old('email') }}">
            <div class="text-danger ml-3">
                <small>{{ $errors->first('email') }}</small>
            </div>
        </div>
        <button type="submit" class="btn btn-primary btn-sm">Add Customer</button>
        @csrf
    </form>
    <dl class="row">
            <dt class="col-sm-6">Name</dt>
            <dt class="col-sm-6">Email</dt>
    </dl>
    <dl class="row">
        @foreach ($customers as $customer)
            <dd class="col-sm-6">{{ $customer->name }}</dd>
            <dd class="col-sm-6">{{ $customer->email }}</dd>
        @endforeach
    </dl>
@endsection

Note the use of {{ old() }} blade function to return any old values for forms that didn't pass validation.

11. 11 9:13 Laravel 5.8 Tutorial From Scratch - e11 - Cleaning Up The Views

Create a nav.blade.php and copy in the nav section:

<ul class="nav py-3">
    <li class="nav-item">
        <a class="nav-link" href="/">Home</a>
    </li>
    <li class="nav-item">
        <a class="nav-link" href="about">About Us</a>
    </li>
    <li class="nav-item">
        <a class="nav-link" href="contact">Contact Us</a>
    </li>
    <li class="nav-item">
        <a class="nav-link" href="customers">Customer List</a>
    </li>
</ul>

This is commonly called a partial, the benefit if the easy to edit the file, e.g. in VS Code: CTRL + P nav and it will be easy to open and edit. It is better to use a quick file open than trying to navigate the file structure.

Clean up the customer form in the customers.blade.php, add label, use form-group row class, and add other bootstrap classes as follows:

<div class="row">
    <div class="col-12">

        <form action="customers" method="POST">
            <div class="form-group row">
                <label for="name" class="col-sm-2 col-form-label">Name</label>
                <div class="col-sm-10">
                    <input class="form-control" type="text" name="name" id="name" placeholder="Customer name" value="{{ old('name') }}">
                </div>
                <div class="text-danger offset-sm-2">
                    <div class="ml-3">
                        <small>{{ $errors->first('name') }}</small>
                    </div>
                </div>
            </div>

            <div class="form-group row">
                <label for="email" class="col-sm-2 col-form-label">Email</label>
                <div class="col-sm-10">
                    <input class="form-control" type="text" name="email" id="email" placeholder="Customer E-Mail" value="{{ old('email') }}">
                </div>
                <div class="text-danger offset-sm-2">
                    <div class="ml-3">
                        <small>{{ $errors->first('email') }}</small>
                    </div>
                </div>
            </div>
            <div class="offset-sm-2">
                <button type="submit" class="btn btn-primary btn-sm ml-1">Add Customer</button>
           </div>
            @csrf
        </form>
    </div>
</div>

Open the resources/views/layout.blade.php:

  • Create a new title which will yield the title or default to Learning Laravel 5.8 (the second parameter)
  • Create a new @include for the nav created above, replacing the old nav.
    • note the @include directive can take an array as a second argument. e.g. @include('nav', ['username' => 'some_user_name'])
// head setup
    <title>@yield('title', 'Learning Laravel 5.8')</title>
</head>
<body>
    <div class="container">
        @include('nav')

        @yield('content')
    </div>
// body layout

Open the contact.blade.php add a @section('title), this can take a second argument which is the string:

@extends('layout')
@section('title', 'Contact us') // Add a section for 'title'
@section('content')
    <h1>Contact Us</h1>

    <p>Company Name</p>
    <p>123-123-134</p>
@endsection

Note the single line format for something simple, like a title.

Repeat for about and contact:

@section('title', 'About Us')
@section('title', 'Customers')

Leave home and it will default to Learning Laravel 5.8

12. 12 11:43 Laravel 5.8 Tutorial From Scratch - e12 - Eloquent Where Clause

In the customers.blade.php

  • add a new form-group for active and inactive customers, using a drop down menu, located after the email and before the button.
<div class="form-group row">
    <label for="active" class="col-sm-2 col-form-label">Status</label>
    <div class="col-sm-10">
        <select name="active" id="active" class="form-control">
            <option value="" disabled>Select customer status</option>
            <option value="1" selected>Active</option>
            <option value="0">Inactive</option>
        </select>
    </div>
    <div class="text-danger offset-sm-2">
        <div class="ml-3">
            <small>{{ $errors->first('active') }}</small>
        </div>
    </div>
</div>

In the database/migrations/2019_03_30_114129_create_customers_table.php

  • Add a new field for active customers to the up method
public function up()
{
    Schema::create('customers', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->string('name');
        $table->string('email')->unique();
        $table->integer('active'); // add this line
        $table->timestamps();
    });
}

Rollback the database and migrate again.

php artisan migrate:rollback
php artisan migrate

Update the customers controller

  • List method.
    • Add activeCustomers and inactiveCustomers with where clause
    • Delete $customers = Customer::all();
  • Store method.
    • Add 'active' => 'required' to the validate array
    • Add $customer->active = request('active'); before the save
public function list()
{
    // $customers = Customer::all(); // Delete this line

    $activeCustomers = customers::where('active', 1)->get(); // Add this line
    $inactiveCustomers = customers::where('active', 0)->get(); // Add this line

    return view(
        'internals.customers',
        [
            // 'customers' => $customers, // Delete this line
            'activeCustomers' => $activeCustomers, // Add this line
            'inactiveCustomers' => $inactiveCustomers, // Add this line
        ]
    );
}
public function store()
{
    $data = request()->validate([
        'name' => 'required|min:3',
        'email' => 'required|email|unique:customers,email',
        'active' => 'required' // Add this line
    ]);

    $customer = new Customer();
    $customer->name = request('name');
    $customer->email = request('email');
    $customer->active = request('active'); // Add this line

    $customer->save();

    return back();
}

Back in customers.blade.php

  • Add an extra column for active and inactive customers.
  • Updated the layout for one column of active customers and one of inactive customers
<dl class="row">
    <dt class="col-sm-6">Active Customers</dt>
    <dt class="col-sm-6">Inactive Customers</dt>
</dl>
<dl class="row">
    <div class="col-sm-6">
        @foreach ($activeCustomers as $activeCustomer)
            <dd>{{ $activeCustomer->name }} <span class="text-secondary">({{ $activeCustomer->email }})</span> </dd>
        @endforeach
    </div>
    <div class="col-sm-6">
            @foreach ($inactiveCustomers as $inactiveCustomer)
            <dd>{{ $inactiveCustomer->name }} <span class="text-secondary">({{ $inactiveCustomer->email }})</span> </dd>
        @endforeach
    </div>
</dl>

Finally the CustomersController.php can return the view using compact function:

// Was:
return view(
        'internals.customers',
        [
            'activeCustomers' => $activeCustomers,
            'inactiveCustomers' => $inactiveCustomers,
        ]
    );
// Now:
return view(
    'internals.customers',
    compact('activeCustomers', 'inactiveCustomers')
);

13. 13 15:42 Laravel 5.8 Tutorial From Scratch - e13 - Eloquent Scopes & Mass Assignment

Laravel can use many different databases. We are currently using sqlite.

When we created the Customer model, using php artisan make:model, the CustomersController was created and the Customer.php Model. An active and inactive scope can be created in the Customer Model, the CustomerController can then be refactored to make it easy to read.

Mass assignment controls the fields that are allowed to be entered into, there are two way to allow data to be entered into a database:

  • protected $fillable filed with an array explicitly containing all the fields that are allowed. e.g.
    • protected $fillable = ['name', 'email', 'active'];
  • protected $guarded to guard fields that are not mass fillable. e.g.
    • protected $guarded = [] means nothing is guarded.
    • protected $guarded = [id] means the id is guarded.
class Customer extends Model
{
    // Fillable Example
    // protected $fillable = ['name', 'email', 'active'];

    // Guarded Example
    protected $guarded = [];

    public function scopeActive($query)
    {
        return $query->where('active', 1);
    }

    public function scopeInactive($query)
    {
        return $query->where('active', 0);
    }
}

Tutor explained $guarded is his preferred option as all fields are validated before being created.

public function list()
{
    // These lines are now easier to read:
    // Now reads get active customers and get inactive customers.
    // Was: $activeCustomers = customers::where('active', 1)->get();
    $activeCustomers = Customer::active()->get();
    // Was: $inactiveCustomers = customers::where('active', 0)->get();
    $inactiveCustomers = Customer::inactive()->get();

    return view(
        'internals.customers',
        compact('activeCustomers', 'inactiveCustomers')
    );
}
public function store()
{
    $data = request()->validate([
        'name' => 'required|min:3',
        'email' => 'required|email|unique:customers,email',
        'active' => 'required'
    ]);
    // Was:
    // $customer = new Customer();
    // $customer->name = request('name');
    // $customer->email = request('email');
    // $customer->active = request('active');

    // $customer->save();

    // Now:
    Customer::create($data); // Mass assignment must be configured first!

    return back();
}

14. 14 13:37 Laravel 5.8 Tutorial From Scratch - e14 - Eloquent BelongsTo & HasMany Relationships

In this lesson the customers will belong to a company.

Start by creating a model with the -m flag, remember the models are singular.

php artisan create:model Company -m
Model created successfully.
Created Migration: 2019_04_02_181919_create_companies_table

Open the database/migrations/2019_04_02_181919_create_companies_table.php migration:

public function up()
{
    Schema::create('companies', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->string('name');
        $table->string('phone');
        $table->timestamps();
    });
}

Open the app/Company.php model and turn mass assignment off

class Company extends Model
{
    protected $guarded = [];
}

Tutor created a company using php artisan tinker and then as the mass assignment has been turned off created a company record.

php artisan tinker
$c = Company::create(['name' => 'ABC Company', 'phone' => '123-123-1234']);
$c->get()

See far below for how I created a factory to seed the data instead.

We have a company in our database, the way we can think the association between a company and customers:

  • A company has many customers.
  • Customers belongs to a company.

Open the app/Company.php model and add the relationship with customers (note the conventions, plural is used)

class Company extends Model
{
    protected $guarded = [];

    public function customers()
    {
        return $this->hasMany(Customer::class);
    }
}

In the app/Customer.php model the inverse need to be written, note the company is singular this time! Customers belong to a company.

public function company()
{
    return $this->belongsTo(Company::class);
}

The create customers table needs to hold a foreign key for the company, open the create customers table

public function up()
{
    Schema::create('customers', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->unsignedInteger('company_id'); // Add this key
        $table->string('name');
        $table->string('email')->unique();
        $table->integer('active');
        $table->timestamps();
    });
}

The company data will need to be passed to the customer view to display a drop down list. Open the app/Http/Controllers/CustomersController.php

public function list()
{
    $activeCustomers = Customer::active()->get();
    $inactiveCustomers = Customer::inactive()->get();
    $companies = Company::all(); // Add this line to fetch all companies

    return view(
        'internals.customers',
        compact('activeCustomers', 'inactiveCustomers', 'companies') // Add companies
    );
}

On the resources/views/internals/customers.blade.php the company need to be available in a drop down list. Add this between the status drop down and the button.

<div class="form-group row">
        <label for="company_id" class="col-sm-2 col-form-label">Company</label>
        <div class="col-sm-10">
            <select name="company_id" id="company_id" class="form-control">
            <option value="" disabled>Select company status</option>
            @foreach ($companies as $company)
                <option value="{{ $company->id }}">{{ $company->name }}</option>
            @endforeach
        </select>
    </div>
    <div class="text-danger offset-sm-2">
        <div class="ml-3">
            <small>{{ $errors->first('company') }}</small>
        </div>
    </div>
</div>


<dl class="row">
    <dt class="col-sm-6">Active Customers</dt>
    <dt class="col-sm-6">Inactive Customers</dt>
</dl>
<dl class="row">
    <div class="col-sm-6">
        @foreach ($activeCustomers as $activeCustomer)
            <dd>{{ $activeCustomer->name }} <span class="text-secondary">({{ $activeCustomer->company->name }})</span> </dd> // update this line
        @endforeach
    </div>
    <div class="col-sm-6">
            @foreach ($inactiveCustomers as $inactiveCustomer)
            <dd>{{ $inactiveCustomer->name }} <span class="text-secondary">({{ $inactiveCustomer->company->name }})</span> </dd> // update this line
        @endforeach
    </div>
</dl>

// at the bottom of the view (before @endsection) add this inverse list:
<div>
    @foreach ($companies as $company)
        <h3>{{ $company->name }}</h3>
        <ul>
            @foreach ($company->customers as $customer)
                <li>{{ $customer->name }}</li>
            @endforeach
        </ul>
    @endforeach
</div>

This demonstrates the company and many customers and a customer belongs to a company.

15. 15 15:45 Laravel 5.8 Tutorial From Scratch - e15 - Eloquent Accessors & RESTful Controller - Part 1

RESTful means if follows a particular pattern of method names to actions and when you follow this pattern it ensures your controllers stay nice clean and short. It improves code dramatically.

This lesson will refactor the customer view and controller.

Search the Laravel docs for resource controllers: laravel.com docs resource controllers

From the docs, there are seven actions:

Actions Handled By Resource Controller

Verb URI Action Route Name
GET /photos index photos.index
GET /photos/create create photos.create
POST /photos store photos.store
GET /photos/{photo} show photos.show
GET /photos/{photo}/edit edit photos.edit
PUT/PATCH /photos/{photo} update photos.update
DELETE /photos/{photo} destroy photos.destroy

Uri is the web address syntax (sometimes called the stub)

Action is the name of the method in the controller.

Name only applies if a resource is used in your web browser file.

In the app/Http/Controllers/CustomersController.php change name of the the list method to index, also change the routes/web.php route. Also change the return view from internals.customers to customers.index to follow the convention.

The top section, of the customer view is the create view, this needs to be split off from the index view.

  • Rename the internals directory to customers
  • Rename the customers.blade.php to resources/views/customers/index.blade.php
  • Copy the index and rename it create resources/views/customers/create.blade.php
  • Cut the form to create a new customer from index and paste it in the create file
  • Change the heading and title to Add New Customer
  • Change the form action="/customers"
  • In the index remove the form to display customers.

Resources/views/customers/create.blade.php:

@extends('layout')
@section('title', 'Add New Customer')
@section('content')
<div class="row">
    <div class="col-12">
        <h1>Add New Customer</h1>
    </div>
</div>
<div class="row">
    <div class="col-12">

        <form action="/customers" method="POST">
            <div class="form-group row">
                <label for="name" class="col-sm-2 col-form-label">Name</label>
                <div class="col-sm-10">
                    <input class="form-control" type="text" name="name" id="name" placeholder="Customer name" value="{{ old('name') }}">
                </div>
                <div class="text-danger offset-sm-2">
                    <div class="ml-3">
                        <small>{{ $errors->first('name') }}</small>
                    </div>
                </div>
            </div>

            <div class="form-group row">
                <label for="email" class="col-sm-2 col-form-label">Email</label>
                <div class="col-sm-10">
                    <input class="form-control" type="text" name="email" id="email" placeholder="Customer E-Mail" value="{{ old('email') }}">
                </div>
                <div class="text-danger offset-sm-2">
                    <div class="ml-3">
                        <small>{{ $errors->first('email') }}</small>
                    </div>
                </div>
            </div>

            <div class="form-group row">
                <label for="active" class="col-sm-2 col-form-label">Status</label>
                <div class="col-sm-10">
                    <select name="active" id="active" class="form-control">
                        <option value="" disabled>Select customer status</option>
                        <option value="1" selected>Active</option>
                        <option value="0">Inactive</option>
                    </select>
                </div>
                <div class="text-danger offset-sm-2">
                    <div class="ml-3">
                        <small>{{ $errors->first('active') }}</small>
                    </div>
                </div>
            </div>

            <div class="form-group row">
                    <label for="company_id" class="col-sm-2 col-form-label">Company</label>
                    <div class="col-sm-10">
                        <select name="company_id" id="company_id" class="form-control">
                            <option value="" disabled>Select company status</option>
                            @foreach ($companies as $company)
                                <option value="{{ $company->id }}">{{ $company->name }}</option>
                            @endforeach
                        </select>
                    </div>
                    <div class="text-danger offset-sm-2">
                        <div class="ml-3">
                            <small>{{ $errors->first('company') }}</small>
                        </div>
                    </div>
                </div>


            <div class="form-group row">
                <div class="col-sm-2">
                </div>
                <div class="col-sm-10">
                    <button type="submit" class="btn btn-primary btn-sm">Add Customer</button>
                </div>
           </div>
            @csrf
        </form>
    </div>
</div>
@endsection

In the CustomersController:

  • Create a create method to pass all companies to the view
  • In the index method revert the customers to all and update the return view.
  • In the store method, change the return to redirect to the customers view once the company has been added.
public function index()
{
    $customers = Customer::all();

    return view('customers.index', compact('customers'));
}

public function create()
{
    $companies = Company::all();

    return view('customers.create', compact('companies'));
}
// ... public function store()
        return redirect('customers');
// ...

In the Customer.php model

  • create a public function called getActiveAttribute, which will return Inactive when the data is 0 and Active when the data is 1.
public function getActiveAttribute($attribute)
{
    return [
        0 => 'Inactive',
        1 => 'Active',
    ][$attribute];
}

Rework the index.blade.php to display the customer data in a table. Thanks to the above getActiveAttribute method the active data will automatically be displayed in text. One way to display the data could be using a ternary statement this: {{ $customer->active ? 'Active' : 'Inactive'}}

@extends('layout')
@section('title', 'Customers')
@section('content')
<div class="row">
    <div class="col-12">
        <h1>Customers</h1>
        <p><a href="/customers/create"><button class="btn btn-primary">Create New Customer</button></a></p>
    </div>
</div>

<div class="row">
    <div class="col-1 font-weight-bold"><u>Id</u></div>
    <div class="col-5 font-weight-bold"><u>Name</u></div>
    <div class="col-5 font-weight-bold"><u>Company</u></div>
    <div class="col-1 font-weight-bold"><u>Active</u></div>
</div>
@foreach ($customers as $customer)
    <div class="row">
        <div class="col-1"> {{ $customer->id }} </div>
        <div class="col-5"> {{ $customer->name }} </div>
        <div class="col-5"> {{ $customer->company->name }} </div>
        <div class="col-1"> {{ $customer->active}} </div>
    </div>
@endforeach
@endsection

16. 16 9:55 Laravel 5.8 Tutorial From Scratch - e16 - Eloquent Route Model Binding & RESTful Controller - Part 2

Show view to display an individual customer's details.

Verb URI Action Route Name
GET /customers/{customer} show customers.show

To display a the full details of a customer the view is called /customer/show.blade.php The web.php route is a GET route for /customer.show (or customer/show) The CustomerController model is show, which will get all the details of the individual customer by their id and call the view to display it.

The url for customer id of 1 would look like this: https://localhost/customers/1

In web.php add a new route:

Route::get('customers/{customer}', 'CustomersController@show');

In CustomerController.php create a store method:

  • As a test use dd($customer); to see what is returned to the method.
  • Use the find method to retrieve the customer form the database
  • As another test use dd($customer); to view the customer data.
  • Return the view to customers.show with the customer data
public function show($customer)
{
    // dd($customer); displays "1" for customer 1
    // (if the first customer is clicked)

    // This will give a fatal error if the customer is not found
    // $customer = Customer::find($customer);

    // Using firstOrFail will give 404 error if the customer isn't found.
    $customer = Customer::where('id', $customer)->firstOrFail()

    // dd($customer); // Displays the data for customer 1
    return view('customers.show', compact('customer'));
}

Using type hinting, as long as the same variable name (customer) is used in the route and model Laravel will automatically retrieve the customer for us:

public function show(Customer $customer)
{
    return view('customers.show', compact('customer'));
}

Modify the customers/index.blade.php to display the Customer as a link

// Was: <div class="col-5"> {{ $customer->name }} </div>
<div class="col-4"><a href="/customers/{{ $customer->id }}
        ">{{ $customer->name }}</a></div>

Create a customers/show.blade.php (use index as a boilerplate)

@extends('layout')
@section('title', 'Details for ' . $customer->name )
@section('content')
<div class="row">
    <div class="col-12">
        <h1>Details for {{ $customer->name }}</h1>
    </div>
</div>

<div class="row">
    <div class="col-3">
        <dl>
            <dt>Name</dt>
        </dl>
    </div>
    <div class="col-9">
        <dl>
            <dd>{{ $customer->name }}</dd>
        </dl>
    </div>
    <div class="col-3">
        <dl>
            <dt>Email</dt>
        </dl>
    </div>
    <div class="col-9">
        <dl>
            <dd>{{ $customer->email }}</dd>
        </dl>
    </div>
    <div class="col-3">
        <dl>
            <dt>Company</dt>
        </dl>
    </div>
    <div class="col-9">
        <dl>
            <dd>{{ $customer->company->name }}</dd>
        </dl>
    </div>
</div>

@endsection

17. 17 9:29 Laravel 5.8 Tutorial From Scratch - e17 - Eloquent Route Model Binding & RESTful Controller - Part 3

This lesson will focus on editing a customer

Verb URI Action Route Name
GET /customers/{customer}/edit edit customer.edit
  • The web url will look like: http://localhost/1/edit
    • This is to edit customer 1
  • The web route will route to the CustomersController edit method
  • The edit method will be very similar to the show method
    • fetch a customer from the database and give the customer data to the view
  • The view will display the customer in a form:
    • The form will be the similar to the create form.
    • A partial can be created and refactored for duel purpose.
    • The data values will be pre-populated from the existing data
    • Data can be edited and have a submit button.
    • The form will submit a PUT request to /customers/{customer}, which will use the update method.

Open the create.blade.php file

@extends('layout')
@section('title', 'Add New Customer')
@section('content')
<div class="row">
    <div class="col-12">
        <h1>Add New Customer</h1>
    </div>
</div>
<div class="row">
    <div class="col-12">

        <form action="/customers" method="POST">

            @include('customers.form')

            <div class="form-group row">
                <div class="col-sm-2">
                </div>
                <div class="col-sm-10">
                    <button type="submit" class="btn btn-primary btn-sm">Add Customer</button>
                </div>
           </div>
        </form>
    </div>
</div>
@endsection

Create a customers/form.blade.php which is a partial containing the form.

  • Change the value attribute so either the old data or the customer data is filled.
  • The company name and active will be handled in another lesson.
<div class="form-group row">
    <label for="name" class="col-sm-2 col-form-label">Name</label>
    <div class="col-sm-10">
        <input class="form-control" type="text" name="name" id="name" placeholder="Customer name" value="{{ $old->name ?? $customer->name }}">
    </div>
    <div class="text-danger offset-sm-2">
        <div class="ml-3">
            <small>{{ $errors->first('name') }}</small>
        </div>
    </div>
</div>

<div class="form-group row">
    <label for="email" class="col-sm-2 col-form-label">Email</label>
    <div class="col-sm-10">
        <input class="form-control" type="text" name="email" id="email" placeholder="Customer E-Mail" value="{{ $old->email ?? $customer->email }}">
    </div>
    <div class="text-danger offset-sm-2">
        <div class="ml-3">
            <small>{{ $errors->first('email') }}</small>
        </div>
    </div>
</div>

<div class="form-group row">
    <label for="active" class="col-sm-2 col-form-label">Status</label>
    <div class="col-sm-10">
        <select name="active" id="active" class="form-control">
            <option value="" disabled>Select customer status</option>
            <option value="1" selected>Active</option>
            <option value="0">Inactive</option>
        </select>
    </div>
    <div class="text-danger offset-sm-2">
        <div class="ml-3">
            <small>{{ $errors->first('active') }}</small>
        </div>
    </div>
</div>

<div class="form-group row">
    <label for="company_id" class="col-sm-2 col-form-label">Company</label>
    <div class="col-sm-10">
        <select name="company_id" id="company_id" class="form-control">
            <option value="" disabled>Select company status</option>
            @foreach ($companies as $company)
                <option value="{{ $company->id }}">{{ $company->name }}</option>
            @endforeach
        </select>
    </div>
    <div class="text-danger offset-sm-2">
        <div class="ml-3">
            <small>{{ $errors->first('company') }}</small>
        </div>
    </div>
</div>
@csrf

Created a customers/edit.blade.php file which is the edit form, (copy the create file as a boilerplate).

@extends('layout')
@section('title', 'Edit Details for ' . $customer->name)
@section('content')
<div class="row">
    <div class="col-12">
    <h1>Edit Details for {{ $customer->name }}</h1>
    </div>
</div>
<div class="row">
    <div class="col-12">

        <form action="/customers/{{ $customer->id }}" method="POST">
            @method('PATCH')
            @include('customers.form')

            <div class="form-group row">
                <div class="col-sm-2">
                </div>
                <div class="col-sm-10">
                    <button type="submit" class="btn btn-primary btn-sm">Save Customer</button>
                </div>
           </div>
        </form>
    </div>
</div>
@endsection

Update the web.php route:

  • add the get route for customer/{id}/edit for the CustomersController edit method
  • add the patch route for customer/{id} for the CustomersController update method
Route::get('customers/{customer}/edit', 'CustomersController@edit');
Route::patch('customers/{customer}', 'CustomersController@update');

Open the customers/show.blade.php

  • Add a button to edit the customer, under the heading.
<p><a href="/customers/{{ $customer->id }}/edit"><button class="btn btn-primary">Edit Customer</button></a></p>
</div>

Open the CustomersController.php

  • Create an edit method
  • Create an update method
    • Data needs to be validated (same as for creating new data)
public function edit(Customer $customer)
{
    $companies = Company::all();
    return view('customers.edit', compact('customer', 'companies'));
}
public function update(Customer $customer)
{
    $data = request()->validate([
        'name' => 'required|min:3',
        'email' => 'required|email',
        // 'active' => 'required', // will be handled in a future lesson
        // 'company_id' => 'required'
    ]);

    $customer->update($data);

    return redirect('customers/' . $customer->id);
}

18. 18 16:48 Laravel 5.8 Tutorial From Scratch - e18 - Eloquent Route Model Binding & RESTful Controller - Part 4

When the a new customer is created and verified it does not have any customer data, so the above form will fail. Also this lesson will fix the editing of an existing customer, validating the active status and company. In the CustomersController.php:

  • In the create method add a line to pass in an empty customer.
  • In the update method add the rest of the form to be validated.
public function create()
{
    $companies = Company::all();
    $customer = new Customer(); // Add this line.

    return view('customers.create', compact('companies', 'customer')); // Add customer to the compact function.
}

public function update(Customer $customer)
{
    $data = request()->validate([
        'name' => 'required|min:3',
        'email' => 'required|email',
        'active' => 'required', // Add this line
        'company_id' => 'required' // Add this line
    ]);

    $customer->update($data);

    return redirect('customers/' . $customer->id);
}

form.blade.php:

  • One way to select the active customer drop down list would be to use a ternary operator in the
  • For the company, during the loop, if is is equal to the company_id of the customer then it is selected
// Selection on drop down for Active or Inactive.
<option value="1" {{ $customer->active == 'Active' ? 'selected' : '' }}>Active</option>
<option value="0" {{ $customer->active == 'Inactive' ? 'selected' : '' }}>Inactive</option>
// foreach loop for company:
@foreach ($companies as $company)
    <option
    value="{{ $company->id }}"
     {{ $company->id == $customer->company_id ? 'selected' : '' }}> // Add this line
    {{ $company->name }}
    </option>
@endforeach
@endforeach

Again this will break the new customer form, as $customer is null. The way around this is to set some defaults for the model. In the Customer.php model create a new protected $attributes array:

protected $attributes = [
    'active' => 1,
];

The CustomersController.php has some duplication, namely the validation of the form. This can be placed in a private method.

  • In the create and update methods, cut out the validate array.
  • Create a private function called validateRequest
  • Inline the calling of the function

In the Customer.php model

  • Refactor the getActiveAttribute so it has a public function of activeOptions
public function getActiveAttribute($attribute)
{
    return $this->activeOptions()[$attribute];
}
// Create public function:
public function activeOptions()
{
    return [
        1 => 'Active', // As an example this order was changed.
        0 => 'Inactive', // As an example this order was changed.
        2 => 'In-Progress' // As an example this status was added
    ];
}

In the form.blade.php the above activeOptions can be used to render the drop down list.

  • The above activeOptionValue function provides the array of values for the drop down
  • If the active field is set for the customer the selected option will be set
<select name="active" id="active" class="form-control">
    <option value="" disabled>Select customer status</option>
    @foreach ($customer->activeOptions() as $activeOptionKey => $activeOptionValue)
        <option value="{{ $activeOptionKey }}" {{ $customer->active == $activeOptionValue ? 'selected' : '' }}>{{ $activeOptionValue }}</option>
    @endforeach
</select>

By refracting the code like this, the view isn't responsible for which data is displayed, it is the models responsibility to provide the data. In the above example an extra option 'In-progress' was added in the model and this was instantly available to the view.

Last of the RESTful states is DELETE.

Verb URI Action Route Name
DELETE /customers/{customer} destroy customers.destroy

We need:

  • web.php Router add route for a delete request to CustomersController destroy method
  • CustomerController.php add the destroy method
  • Customer show.blade.php view add a delete button with the verb delete and endpoint /customers/{customer}
    • {customer} is the customer id record e.g. /customers/1 would be the first customer

Router web.php add:

Route::delete('customers/{customer}', 'CustomersController@destroy');

CustomersController.php add the destroy method

public function destroy(Customer $customer)
{
    $customer->delete();

    return redirect('customers');
}

show.blade.php add a new button to delete the customer record, with method of delete and csrf

<div class="row mb-3">
    <div class="col-12">
        <h1>Details for {{ $customer->name }}</h1>
    </div>
    <div class="col-md-3">
        <a href="/customers/{{ $customer->id }}/edit"><button class="btn btn-primary">Edit Customer</button></a>
    </div>
    <div class="col-md-3">
        <form action="/customers/{{ $customer->id }}" method="POST">
            @method('DELETE')
            @csrf
            <button type="submit" class="btn btn-danger">Delete Customer</button>
        </form>
    </div>
</div>

Finally as the Laravel guidelines have been followed for the seven steps of a resource, the web.php can be updated with the seven calls to the controller replaced with just one line:

// Route::get('customers', 'CustomersController@index');
// Route::get('customers/create', 'CustomersController@create');
// Route::post('customers', 'CustomersController@store');
// Route::get('customers/{customer}', 'CustomersController@show');
// Route::get('customers/{customer}/edit', 'CustomersController@edit');
// Route::patch('customers/{customer}', 'CustomersController@update');
// Route::delete('customers/{customer}', 'CustomersController@destroy')

Route::resource('customers', 'CustomersController');

As this is a regular scenario a resource controller can be created using a php artisan command:

php artisan help make:controller
Description:
  Create a new controller class

Usage:
  make:controller [options] [--] <name>

Arguments:
  name                   The name of the class

Options:
  -m, --model[=MODEL]    Generate a resource controller for the given model.
  -r, --resource         Generate a resource controller class.
  -i, --invokable        Generate a single method, invokable controller class.
  -p, --parent[=PARENT]  Generate a nested resource controller class.
      --api              Exclude the create and edit methods from the controller.
  -h, --help             Display this help message
  -q, --quiet            Do not output any message
  -V, --version          Display this application version
      --ansi             Force ANSI output
      --no-ansi          Disable ANSI output
  -n, --no-interaction   Do not ask any interactive question
      --env[=ENV]        The environment the command should run under
  -v|vv|vvv, --verbose   Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Use the -r flag to create the resource and -m for the target model. e.g.

php artisan make:controller TestController -r -m Customer

Will create the following TestController.php stub with route model binding for the Customer model.

<?php

namespace App\Http\Controllers;

use App\Customer;
use Illuminate\Http\Request;

class TestController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        //
    }

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

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        //
    }

    /**
     * Display the specified resource.
     *
     * @param  \App\Customer  $customer
     * @return \Illuminate\Http\Response
     */
    public function show(Customer $customer)
    {
        //
    }

    /**
     * Show the form for editing the specified resource.
     *
     * @param  \App\Customer  $customer
     * @return \Illuminate\Http\Response
     */
    public function edit(Customer $customer)
    {
        //
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \App\Customer  $customer
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, Customer $customer)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  \App\Customer  $customer
     * @return \Illuminate\Http\Response
     */
    public function destroy(Customer $customer)
    {
        //
    }
}

19. 19 18:25 Laravel 5.8 Tutorial From Scratch - e19 - Handling a Contact Form Using a Laravel Mailable

This lesson is about sending email, this will start inline. The Contract Us page needs a form.

First make the ContactFromController

php artisan make:controller ContactFormController
# Controller created successfully.

The web route currently displays a view, this need to be changed to use the controller

Open web.php change the contact view to the ContactFormController, the method is create as it the correct verb for the form as the action is to display a form to be submitted, similar to the create a new customer form. the form will need to post request to the store method of the controller:

// Route::view('contact', 'contact');
Route::get('contact', 'ContactFormController@create');
Route::post('contact', 'ContactFormController@store');

In the ContactFormController.php create a public function create and return the contact.create view:

class ContactFormController extends Controller
{
    public function create()
    {
        return view('contact.create');
    }
}

Move the current contact.blade.php into a new directory called contact and rename the form create.blade.php

Open the website and click on Contact Us, the contact form will display.

Open the create.blade.php, use the form.blade.php for name and email and create a third field for message, remember the csrf helper, rework the form and layout as follows:

@extends('layout')
@section('title', 'Contact us')
@section('content')
<h1>Contact Us</h1>

<div class="row">
    <div class="col-12">
        <form action="/contact" method="POST">
            <div class="form-group row">
                <label for="name" class="col-sm-2 col-form-label">Name</label>
                <div class="col-sm-10">
                    <input class="form-control" type="text" name="name" id="name" placeholder="Your name" value="{{ old('name') }}">
                </div>
                <div class="text-danger offset-sm-2">
                    <div class="ml-3">
                        <small>{{ $errors->first('name') }}</small>
                    </div>
                </div>
            </div>

            <div class="form-group row">
                <label for="email" class="col-sm-2 col-form-label">Email</label>
                <div class="col-sm-10">
                    <input class="form-control" type="text" name="email" id="email" placeholder="Your E-Mail" value="{{ old('email') }}">
                </div>
                <div class="text-danger offset-sm-2">
                    <div class="ml-3">
                        <small>{{ $errors->first('email') }}</small>
                    </div>
                </div>
            </div>

            <div class="form-group row">
                <label for="message" class="col-sm-2 col-form-label">Message</label>
                <div class="col-sm-10">
                    <textarea class="form-control" type="text" name="message" id="message" placeholder="Your message" value="{{ old('message') }}" rows="6"></textarea>
                </div>
                <div class="text-danger offset-sm-2">
                    <div class="ml-3">
                        <small>{{ $errors->first('message') }}</small>
                    </div>
                </div>
            </div>
            <div class="form-group row">
                <div class="col-sm-2">
                </div>
                <div class="col-sm-10">
                    <button type="submit" class="btn btn-primary btn-sm">Send Message</button>
                </div>
            </div>
            @csrf
        </form>
    </div>
</div>
@endsection

Open the app/Http/Controllers/ContactFormController.php

  • create the public function create
  • create the public function store
class ContactFormController extends Controller
{
    public function create()
    {
        return view('contact.create');
    }

    public function store()
    {
        dd(request()->all()); // To test
    }
}

Laravel ships with a mailable template.

See php artisan help above, there is a make:mail command which will create a new email class.

php artisan help make:mail
Description:
  Create a new email class

Usage:
  make:mail [options] [--] <name>

Arguments:
  name                       The name of the class

Options:
  -f, --force                Create the class even if the mailable already exists
  -m, --markdown[=MARKDOWN]  Create a new Markdown template for the mailable
  -h, --help                 Display this help message
  -q, --quiet                Do not output any message
  -V, --version              Display this application version
      --ansi                 Force ANSI output
      --no-ansi              Disable ANSI output
  -n, --no-interaction       Do not ask any interactive question
      --env[=ENV]            The environment the command should run under
  -v|vv|vvv, --verbose       Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

To make a contact form with a markdown view

  • the markdown view will be in the views directory, to keep email separate the tutor recommends they be put in their own email folder with the same structure as the form.
php artisan make:mail ContactFormMail --markdown=emails.contact.contact-form
# Mail created successfully.

This will create two files:

  • resources/views/emails/contact/contact-form.blade.php
  • app/Mail/ContactFormMail.php

Open the app/Http/Controllers/ContactFormController.php

  • Update the store method:
// add these use
use App\Mail\ContactFormMail;
use Illuminate\Support\Facades\Mail;

// Amend this method
public function store()
{
    $data = request()->validate([
        'name' => 'required|min:3',
        'email' => 'required|email',
        "message" => 'required|min:3',
    ]);

    Mail::to('test@test.com')->send(new ContactFormMail($data));

    return redirect('contact'); // Normally a flash message is better.
}

Update the mail setting in .env file:

MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=username
MAIL_PASSWORD=password
MAIL_FROM_ADDRESS=from@example.com
MAIL_FROM_NAME=Example

After editing the .env file the website will need to be restarted for the new settings to be loaded into cache, one way to force this is to run php artisan config:cache

Open the contact us page and send a dummy form, the form will be sent to mailtrap.io.

To pass the data to the contact-from template, the app/Mail/ContactFormMail.php will need to be configured:

  • Add a public property called data
  • In the contractor take the data and pass it to the data property
<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;

class ContactFormMail extends Mailable
{
    use Queueable, SerializesModels;

    public $data; // Add this public variable

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct($data) // Take the $data
    {
        $this->data = $data; // Add it to the data property.
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->markdown('emails.contact.contact-form');
    }
}

To modify the email template open resources/views/emails/contact/contact-form.blade.php, this is the same as a view, except it uses markdown syntax, although it can use html too.

@component('mail::message')

# Thank you for your message

**Name:** {{ $data['name'] }}

**Email:** {{ $data['email'] }}

**Message:**

{{ $data['message'] }}

@endcomponent

Send a new form and check mailtrap once more to see it has been received.

20. 20 7:05 Laravel 5.8 Tutorial From Scratch - e20 - Flashing Data to Session & Conditional Alerts in View

Bootstrap has alerts than can advise the user. e.g.

<div class="alert alert-success" role="alert">
  A simple success alert—check it out!
</div>

When a mail has been successfully sent the return redirect can actually pass a message too.

return redirect('contact')
    ->with('message', 'Thanks for your message. We\'ll be in touch.');

The resources/views/layout.blade.php can display the message under the nav bar:

<div class="container">
    @include('nav')

    @if ( session()->has('message'))
        <div class="alert alert-success" role="alert">
            <strong>Success</strong> {{ session()->get('message') }}
        </div>
    @endif
    @yield('content')
</div>

The form can be wrapped in an if statement to only display if there is no message.

@if ( ! session()->has('message'))
    // display the form
@endif

This is a crude way to display a message to a user, a better way will be available in the vue later in the course.

21. 21 11:37 Laravel 5.8 Tutorial From Scratch - e21 - Artisan Authentication - Register, Login & Password Reset

Laravel can wip up authorisation with one command. It does change some views so is recommended as one of the first things to setup before creating the content.

To view the help on make:auth run the help command.

php artisan help make:auth
Description:
  Scaffold basic login and registration views and routes

Usage:
  make:auth [options]

Options:
      --views           Only scaffold the authentication views
      --force           Overwrite existing views by default
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
      --env[=ENV]       The environment the command should run under
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Run the command:

php artisan make:auth
# The [home.blade.php] view already exists. Do you want to replace it? (yes/no) [no]:
 > y

Note: as home.blade.php already existed we had the above question, the home view is replaced with with login options.

The auth home page uses resources/views/layouts/app.blade.php, which has all the functionality for login and sign out. Plus it displays the name for the user name who is signed in, as this is more advanced than the basic layout file we have created it will be adopted for this project and modified with the navigation already setup.

Open the app.blade.php:

  • cut all of the nav and paste it into our nav.blade.php
  • replace the nav @include('nav')
  • Add a div with container class around the @yield('content')

app.blade.php:

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>{{ config('app.name', 'Laravel') }}</title>

    <!-- Scripts -->
    <script src="{{ asset('js/app.js') }}" defer></script>

    <!-- Fonts -->
    <link rel="dns-prefetch" href="//fonts.gstatic.com">
    <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet" type="text/css">

    <!-- Styles -->
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
    <div id="app">
        @include('nav')
        <main class="py-4">
            <div class="container">
                @yield('content')
            </div>
        </main>
    </div>
</body>
</html>

nav.blade.php:

  • Cut the original nav and past it into the area commended as Left Side Of Navbar
<nav class="navbar navbar-expand-md navbar-light navbar-laravel">
    <div class="container">
        <a class="navbar-brand" href="{{ url('/') }}">
            {{ config('app.name', 'Laravel') }}
        </a>
        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
            <span class="navbar-toggler-icon"></span>
        </button>

        <div class="collapse navbar-collapse" id="navbarSupportedContent">
            <!-- Left Side Of Navbar -->
            <ul class="navbar-nav mr-auto">
                <li class="nav-item">
                    <a class="nav-link" href="/">Home</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="/about">About Us</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="/contact">Contact Us</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="/customers">Customer List</a>
                </li>
            </ul>

            <!-- Right Side Of Navbar -->
            <ul class="navbar-nav ml-auto">
                <!-- Authentication Links -->
                @guest
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('login') }}">{{ __('Login') }}</a>
                    </li>
                    @if (Route::has('register'))
                        <li class="nav-item">
                            <a class="nav-link" href="{{ route('register') }}">{{ __('Register') }}</a>
                        </li>
                    @endif
                @else
                    <li class="nav-item dropdown">
                        <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
                            {{ Auth::user()->name }} <span class="caret"></span>
                        </a>

                        <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
                            <a class="dropdown-item" href="{{ route('logout') }}"
                                onclick="event.preventDefault();
                                                document.getElementById('logout-form').submit();">
                                {{ __('Logout') }}
                            </a>

                            <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
                                @csrf
                            </form>
                        </div>
                    </li>
                @endguest
            </ul>
        </div>
    </div>
</nav>

Next the current views have @extends('layout') which references layout.blade.php and this needs to be changed to layouts.app, to reference the new authentication layouts/app.blade.php. This needs a mass search and replace. (CTRL+SHIT+F) search @extends('layout') replace @extends('layouts.app'), this should be in 6 files.

22. 22 8:40 Laravel 5.8 Tutorial From Scratch - e22 - Artisan Authentication Restricting Access with Middleware

Now we have login the app can be locked down so only logged in users can view parts of the website.

Middleware stands in between the request and the response. In the case of authorisation, if the user requests a page which needs to be logged in the user is redirected to the login page.

Middleware is located in Http/Middleware. For example there is a middleware for maintenance, run the command php artisan down and the site will be in maintenance mode, the users will not be able to view the pages, they will receive a 503 | Service Unavailable message. Run the command php artisan up to return to live. The one we are interested in this lesson is app/Http/Middleware/Authenticate.php. There are two ways to apply this middleware.

  • The route level.

In web.php tag on ->middleware('auth')

Route::resource('customers', 'CustomersController')->middleware('auth');

Even if the user directly navigate to the customer page, they will be redirected, unless logged in.

  • The Controller level

In CustomersController.php add a __construct method with $this->middleware('auth');

public function __construct()
{
    $this->middleware('auth');
}

There are additional options:

  • only
  • except
    • e.g. ->except(['index']); will lock down all pages, except for the customer list.
$this->middleware('auth')->except(['index']);

Selecting the customer to show their details will redirect to the login page. An example of this is comments, visitors can view comments, but can not leave a comment or edit one.

23. 23 11:04 Laravel 5.8 Tutorial From Scratch - e23 - Adding a Custom Middleware

Basic example of how to create a middleware.

php artisan help make:middleware
Description:
  Create a new middleware class

Usage:
  make:middleware <name>

Arguments:
  name                  The name of the class

Options:
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
      --env[=ENV]       The environment the command should run under
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Needs a name, we will create a test middleware:

php artisan make:middleware TestMiddleware
#Middleware created successfully.

In TestMiddleware.php's handle method:

public function handle($request, Closure $next)
{
    dd('Hit');
}

To register edit the app/Http/Kernel.php, note there are two Kernel files, edit the one in the one in app/Http not app/console. There are two arrays of middleware.

  • $middleware array is a global list, hit every request.
  • $middlewareGroups array are route middleware groups.
    • Only called for routes, again every route is called automatically.
  • $routeMiddleware array is an application route
    • Is is manually assigned to a route or controller.
    • It contains the 'auth' middleware used above.

In the $routeMiddleware array add the line:

protected $routeMiddleware = [
    'auth' => \App\Http\Middleware\Authenticate::class,
    // Other middleware...
    'test' => \App\Http\Middleware\TestMiddleware::class,
];

In web.php add the tag to the about route.

Route::view('about', 'about')->Middleware('test');

Navigate to the about web page and the dd 'Hit' message will display.

To demonstrate how middleware could be used, in the TestMiddleware update the handle method:

if (now()->format('s') % 2 ) {
    return $next($request);
}
return response('Not Allowed');

Now each time the About page is displayed it will only show the page on odd seconds, on even seconds it will display 'Not Allowed'. Middleware could be used to dispatch events. E.G. If a user hasn't logged in for three months a welcome back message could be triggered.

24. 24 10:17 Laravel 5.8 Tutorial From Scratch - e24 - URL Helpers

Laravel has three helpers to generate URLS for our app.

In the create.blade.php we have a relative url to post the form to /contact

  • URL creator
{{ url('/contact') }}

In the web page the source will be the full path to the page (http://127.0.0.1/contact), rather than the relative path.

  • Named routes

In web.php tag on ->name('contact.create') to the contact route:

Route::get('contact', 'ContactFormController@create')->name('contact.create');
Route::post('contact', 'ContactFormController@store')->name('contact.store');

In web.php change the helper:

{{ route('contact.store') }}

In customers edit.blade.php view, a parameter needs to be passed for the customer id, this can be handled by using second parameter.

// Was: <form action="/customers/{{ $customer->id }}" method="POST">
<form action="{{ route('customer.update', ['customer' => $customer]) }}" method="POST">

There is a command in php artisan called route:list

php artisan help route:list
Description:
  List all registered routes

Usage:
  route:list [options]

Options:
      --columns[=COLUMNS]  Columns to include in the route table (multiple values allowed)
  -c, --compact            Only show method, URI and action columns
      --method[=METHOD]    Filter the routes by method
      --name[=NAME]        Filter the routes by name
      --path[=PATH]        Filter the routes by path
  -r, --reverse            Reverse the ordering of the routes
      --sort[=SORT]        The column (domain, method, uri, name, action, middleware) to sort by [default: "uri"]
  -h, --help               Display this help message
  -q, --quiet              Do not output any message
  -V, --version            Display this application version
      --ansi               Force ANSI output
      --no-ansi            Disable ANSI output
  -n, --no-interaction     Do not ask any interactive question
      --env[=ENV]          The environment the command should run under
  -v|vv|vvv, --verbose     Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
Domain Method URI Name Action Middleware
GET|HEAD / Illuminate\Routing\ViewController web
GET|HEAD about Illuminate\Routing\ViewController web
GET|HEAD api/user Closure api,auth:api
GET|HEAD contact contact.create App\Http\Controllers\ContactFormController@create web
POST contact contact.store App\Http\Controllers\ContactFormController@store web
GET|HEAD customers customers.index App\Http\Controllers\CustomersController@index web,auth
POST customers customers.store App\Http\Controllers\CustomersController@store web,auth
GET|HEAD customers/create customers.create App\Http\Controllers\CustomersController@create web,auth
PUT|PATCH customers/{customer} customers.update App\Http\Controllers\CustomersController@update web,auth
DELETE customers/{customer} customers.destroy App\Http\Controllers\CustomersController@destroy web,auth
GET|HEAD customers/{customer} customers.show App\Http\Controllers\CustomersController@show web,auth
GET|HEAD customers/{customer}/edit customers.edit App\Http\Controllers\CustomersController@edit web,auth
GET|HEAD home home App\Http\Controllers\HomeController@index web,auth
GET|HEAD login login App\Http\Controllers\Auth\LoginController@showLoginForm web,guest
POST login App\Http\Controllers\Auth\LoginController@login web,guest
POST logout logout App\Http\Controllers\Auth\LoginController@logout web
POST password/email password.email App\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail web,guest
GET|HEAD password/reset password.request App\Http\Controllers\Auth\ForgotPasswordController@showLinkRequestForm web,guest
POST password/reset password.update App\Http\Controllers\Auth\ResetPasswordController@reset web,guest
GET|HEAD password/reset/{token} password.reset App\Http\Controllers\Auth\ResetPasswordController@showResetForm web,guest
GET|HEAD register register App\Http\Controllers\Auth\RegisterController@showRegistrationForm web,guest
POST register App\Http\Controllers\Auth\RegisterController@register web,guest

The named routes are listed under name, for example if you wanted to logout a user you could call the logout page. the resource routes all 7 verbs are automatically named.

  • Action('')

This can call the controller method.

{{ action('HomeController@index') }}

In the browser the FQ url which will hit the controller will be listed. e.g. http://127.0.0.1:8000/home for the HomeController Index method.

Alternatively the action helper can take an array:

{{ action([\App\Http\Controllers\HomeController::class, 'index']) }}

When using the above method an IDE, like PHP Storm can CTRL + click to open that class.

Task: Update all routes to named routes or urls throughout the project pages.

web.php:

Route::view('/', 'home')->name('home');

Route::get('contact', 'ContactFormController@create')->name('contact.create');
Route::post('contact', 'ContactFormController@store')->name('contact.store');

Route::view('about', 'about')->name('about');

nav.blade.php:

<!-- Left Side Of Navbar -->
<ul class="navbar-nav mr-auto">
    <li class="nav-item">
        <a class="nav-link" href="{{ route('home') }}">Home</a>
    </li>
    <li class="nav-item">
        <a class="nav-link" href="{{ route('about') }}">About Us</a>
    </li>
    <li class="nav-item">
        <a class="nav-link" href="{{ route('contact.create') }}">Contact Us</a>
    </li>
    <li class="nav-item">
        <a class="nav-link" href="{{ route('customers.index') }}">Customer List</a>
    </li>
</ul>
<p><a href="{{ route('customers.create') }}"><button class="btn btn-primary">Create New Customer</button></a></p>
// ...
<div class="col-5"><a href="{{ route('customers.show', ['customer' => $customer]) }}

resources/views/contact/create.blade.php:

<form action="{{ route('contact.store') }}" method="POST">

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

<a href="{{ route('customers.edit', ['customer' => $customer]) }}"><button class="btn btn-primary">Edit Customer</button></a>

resources/views/customers/edit.blade.php:

<form action="{{ route('customers.update', ['customer' => $customer]) }}" method="POST">

25. 25 11:13 Laravel 5.8 Tutorial From Scratch - e25 - Front End Setup with NPM, Node, Vue & Webpack

Vue is a Javascript framework built into Laravel.

To check the locally installed version of npm and node:

npm -v
# 6.7.0
node -v
# v11.10.0

To initialise the frontend framework

npm install

The node library is the equivalent to composer's vendor directory. It is installed in node_modules directory. The installation configuration is in package.json.

Webpack is a javascript library which uses node to compile the javascript files.

Open webpack.mix.js

const mix = require("laravel-mix");

mix
  .js("resources/js/app.js", "public/js")
  .sass("resources/sass/app.scss", "public/css");

Open resources/js/app.js

/**
 * First we will load all of this project's JavaScript dependencies which
 * includes Vue and other libraries. It is a great starting point when
 * building robust, powerful web applications using Vue and Laravel.
 */

require("./bootstrap");

window.Vue = require("vue");

/**
 * The following block of code may be used to automatically register your
 * Vue components. It will recursively scan this directory for the Vue
 * components and automatically register them with their "basename".
 *
 * Eg. ./components/ExampleComponent.vue -> <example-component></example-component>
 */

// const files = require.context('./', true, /\.vue$/i);
// files.keys().map(key => Vue.component(key.split('/').pop().split('.')[0], files(key).default));

Vue.component(
  "example-component",
  require("./components/ExampleComponent.vue").default
);

/**
 * Next, we will create a fresh Vue application instance and attach it to
 * the page. Then, you may begin adding components to this application
 * or customize the JavaScript scaffolding to fit your unique needs.
 */

const app = new Vue({
  el: "#app"
});

This file opens an example-component and initializes vue.

Open resources/sass/app.scss

// Fonts
@import url("https://fonts.googleapis.com/css?family=Nunito");

// Variables
@import "variables";

// Bootstrap
@import "~bootstrap/scss/bootstrap";

.navbar-laravel {
  background-color: #fff;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
}

This file defines the css used by Laravel.

To compile everything once, run this command:

npm run dev

The complied files are placed in the public directory public/css/app.css and public/js/app.js

For development purposes there is a command which compiles everything, then watches files for any changes, recompiling if any are detected.

npm run watch

Info: If there are any errors, when watch is first run, delete the node_modules and reinstall:

rm -rf node_modules && npm install

Edit resources/sass/app.scss, add the following class:

.new-class {
  background-color: red;
}

There will be a popup confirmed successful build.

Open home.blade.php, wrap the class around the logged in message:

<div class="new-class">
  You are logged in!
</div>

If not already running start php artisan serve and open the home page. If already running refresh the hme page by clicking Laravel in the navbar. The "You are logged in!" message will have a red background.

As we used the app.blade.php created by Laravel when auth was enabled, we are automatically importing the js and css in the public directory. Open resources/views/layouts/app.blade.php, the scripts, fonts and styles are brought in using an asset helper method. Also note the div with an id of app, this is what vue is using as a target.

<!-- Scripts -->
<script src="{{ asset('js/app.js') }}" defer></script>

<!-- Fonts -->
<link rel="dns-prefetch" href="//fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet" type="text/css">

<!-- Styles -->
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
// ..
<body>
    <div id="app">
// ..

26. 26 15:57 Laravel 5.8 Tutorial From Scratch - e26 - Vue Basics 101

If not already running and before we start run, so our code will automatically be recompiled.:

npm run watch

This lesson will create a simple component based on the example component that ships with Laravel:

  • resources/js/components/ExampleComponent.vue

Components should be small, modular and reusable. For more advanced vue use, see other tutorials on the subject.

Rename the ExampleComponent.vue to MyButton.vue, we will create a new component for a button.

Note: Vue needs a parent div to cover the entire template.

<template>
  <div>
    <button type="submit" class="my-button">My Button</button>
  </div>
</template>

<script>
  export default {
    mounted() {
      console.log("Component mounted.");
    }
  };
</script>

<style>
  .my-button {
    background-color: #333;
  }
</style>

The component need to be registered to work, similar to the way middleware needs to be registered. Open resources/js/app.js

  • Change the Vue.component:
Vue.component("my-button", require("./components/MyButton.vue").default);

There should be a build successful message.

The button is ready to be used in a view, for example open home.blade.php:

  • Change the login message to a my-button tag:
<my-button></my-button>

Refresh the home page and a button will display in the place of the login message.

To update the component, e.g. to give it some extra style, edit the style section of MyButton.vue

<style>
  .my-button {
    background-color: #333;
    color: white;
    padding: 10px 20px;
    font-weight: bold;
  }
</style>

Wait for the success message and then refresh the home page. The new button styles will display.

Vue is data driven, that means you don't have to dive into the DOM pick an object by ID or class and then save that to a variable and every time and every time you need to access it you need to go back into the DOM and change that. It is reactive so lets change that button.

  • Add v-text="text" to MyButton.vue
  • Add props with an array containing text
<template>
  <div>
    <button type="submit" class="my-button" v-text="text"></button>
  </div>
</template>

<script>
  export default {
    mounted() {
      console.log("Component mounted.");
    },
    props: ["text"] // add this line
  };
</script>

In home.blade.php:

  • add text="My New Text Button" to the button tag.
<my-button text="My New Text Button"></my-button>

Refresh the home page and the button will display with My New Text Button.

To customize the type of button, in MyButton.vue:

  • change submit to type and put a colon in fount of type, to bind the type.
  • add type to the array of prop.
<button :type="type" class="my-button" v-text="text"></button> // ... props:
["text", "type"]

In home.blade.php

  • Add type="submit"
<my-button text="My New Text Button" type="submit"></my-button>

The component can be used throughout the project, but only needs to be changed in one place to update in all locations.

Next, use vue to get data from the back-end, vue uses a library called axios.

php artisan make:controller TestingVueController
# Controller created successfully.

Open the TestingVueController.php:

  • Add the index method to return a name of John Doe. Note: Laravel will automatically return this php array as JSON data.
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TestingVueController extends Controller
{
    public function index()
    {
        return [
            'name' => 'John Doe',
        ];
    }
}

Api routes use a different Routes/api.php file.

  • Add Route::post for vue to the new testingVueController index method.
  • Note: all api routes will be prefixed with api/, this doesn't need to be explicitly added.
<?php

use Illuminate\Http\Request;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

Route::middleware('auth:api')->get('/user', function (Request $request) {
    return $request->user();
});

Route::post('vue', 'TestingVueController@index'); // Add this line.

In MyButton.vue:

  • add the axios.post line
  • add data function, which defines test.
  • Change the v-text to test
<template>
  <div>
    <button :type="type" class="my-button" v-text="test.name"></button>
    <!--update-->
  </div>
</template>

<script>
  export default {
    mounted() {
      console.log("Component mounted.");
      // Add the following:
      axios.post("api/vue", {}).then(response => {
        this.test = response.data;
        console.log(this.test);
      });
    },
    data: function() {
      return {
        test: ""
      };
    },
    props: ["text", "type"]
  };
</script>

Save and refresh. John Doe is now displayed in the button.

27. 27 7:31 Laravel 5.8 Tutorial From Scratch - e27 - Frontend Presets for React, Vue, Bootstrap & Tailwind CSS

Laravel scaffolds with Vue and Bootstrap, however we can change the scaffolding to anything we want, or even none!

For this lesson create a new Laravel project called presets:

cd ..
laravel new presets
cd presets
php artisan serve

Open the browser and a new install of Laravel will display.

To view the preset options run the php artisan command:

php artisan help preset
Description:
  Swap the front-end scaffolding for the application

Usage:
  preset [options] [--] <type>

Arguments:
  type                   The preset type (none, bootstrap, vue, react)

Options:
      --option[=OPTION]  Pass an option to the preset command (multiple values allowed)
  -h, --help             Display this help message
  -q, --quiet            Do not output any message
  -V, --version          Display this application version
      --ansi             Force ANSI output
      --no-ansi          Disable ANSI output
  -n, --no-interaction   Do not ask any interactive question
      --env[=ENV]        The environment the command should run under
  -v|vv|vvv, --verbose   Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
  • none: removes all the fount end
  • bootstrap: adds twitter bootstrap (installed by default)
  • vue: adds Vue (installed by default)
  • react: installs React.

To install react:

php artisan preset react
# Once installed
npm install
npm run dev
  • package.json contains react and react-dom.
  • app.js requires ./bootstrap & ./components/Example
  • js/components/Example.js is a React example component

To remove all presets:

php artisan preset none
  • app.js only has require ./bootstrap
  • app.scss is actually empty!
php artisan preset bootstrap
  • app.scss contains the import strings for bootstrap scss.
  • app.js does not have vue
php artisan preset vue
  • app.js contains Vue.component...
  • app.scss still contains bootstrap (back to default setup)

There are other presets available. Google Laravel frontend presets

Laravel Frontend Presets - Github

Contains sixteen further presets, notably:

  • tailwindcss
  • bulma

For example to install tailwindcss follow the instructions:

27.1. For Presets with Authentication

  1. Use php artisan preset tailwindcss-auth for the basic preset, auth route entry and Tailwind CSS auth views in one go. (NOTE: If you run this command several times, be sure to clean up the duplicate Auth entries in routes/web.php)
  2. npm install && npm run dev
  3. Configure your favourite database (mysql, sqlite etc.)
  4. php artisan migrate to create basic user tables.
  5. php artisan serve (or equivalent) to run server and test preset.

There is also a scaffolding to create your own preset.

28. 28 20:19 Laravel 5.8 Tutorial From Scratch - e28 - Events & Listeners

Events are invaluable tool in Laravel, an example of this is a new user registering for project, they may be sent a welcome email and subscribe them to a news letter and send out a slack notification. This can be done at the controller level, this is be demonstrated, then refactored to an event.

In the example when a new user is registered they will be sent a welcome message.

php artisan make:mail WelcomeNewUserMail --markdown emails.new-welcome
# Mail created successfully.

Open WelcomeNewUserMail.php:

@component('mail::message')
# Welcome New User
@endcomponent

Open CustomerController.php store method:

  • Add $customer = to Customer::create...
  • Add Mail::to($customer->email)->send(new WelcomeNewUserMail());
  • Add use Illuminate\Support\Facades\Mail;
  • Add dump('Register to newsletter');
  • Add dump('Slack message to Admin');
  • Temp comment out the return.
use App\Mail\WelcomeNewUserMail;
use Illuminate\Support\Facades\Mail;
// ..
    public function store()
    {
        $customer = Customer::create($this->validateRequest());

        Mail::to($customer->email)->send(new WelcomeNewUserMail());
        // Register to news letter
        dump('Register to newsletter');
        // Slack notification to Admin
        dump('Slack message to Admin');

        // temp delay before the return..
        sleep(20);

        return redirect('customers');
    }

Create a new user and view the Welcome email in mailtrap.io

Now to refactor the code:

// ..
$customer = Customer::create//
event(new NewCustomerHasRegisteredEvent($customer));

28.1. Manual way to create event and listeners

There are two php artisan commands to use:

php artisan help make:event
php artisan help make:listener

Using the above example, there is one event, a new user event and three listeners, send email, register for newsletter and slack the Admin

php artisan make:event NewCustomerHasRegisteredEvent

Open app/Event/NewCustomerHasRegisteredEvent.php:

  • in the __construct accept the $customer
  • Then assign $customer to the property
  • Make the $customer property public, this way the listeners have access to this property.
  • Clean up the unused method.
<?php

namespace App\Providers;

use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        Registered::class => [
            SendEmailVerificationNotification::class,
        ],
        \App\Events\NewCustomerHasRegisteredEvent::class => [
            \App\Listeners\WelcomeNewCustomerListener::class,
            \App\Listeners\RegisterCustomerToNewsLetterListener::class,
            \App\Listeners\NotifyAdminViaSlackListener::class
        ],
    ];

    /**
     * Register any events for your application.
     *
     * @return void
     */
    public function boot()
    {
        parent::boot();

        //
    }
}

Next make a listeners for each step:

php artisan make:listeners WelcomeNewCustomerListener

Open WelcomeNewCustomerListener.php

  • Cut the send mail from created in the controller earlier
  • Paste it into the handle method.
  • Import the Mail class (use Illuminate\...\Mail)
  • Import the WelcomeNewUserMail class
  • Clean up the unused methods.
<?php

namespace App\Listeners;

use App\Mail\WelcomeNewUserMail;
use Illuminate\Support\Facades\Mail;
use App\Events\NewCustomerHasRegisteredEvent;

class WelcomeNewCustomerListener
{
    /**
     * Handle the event.
     *
     * @param  NewCustomerHasRegisteredEvent  $event
     * @return void
     */
    public function handle(NewCustomerHasRegisteredEvent $event)
    {
        Mail::to($event->customer->email)->send(new WelcomeNewUserMail());
    }
}

The Events need to be registered with EventServiceProvider, so the event is linked to the listener.

Open EventServiceProvider.php:

protected $listen = [
    newCustomerHasRegisteredEvent::class => [
        WelcomeNewCustomerListener::class,
    ],
];

28.2. Automated way to create Listeners

Open EventServiceProvider.php:

  • Add the next two Listeners to the listen array, including the namespace:
protected $listen = [
    \App\Events\NewCustomerHasRegisteredEvent::class => [
        \App\Listeners\WelcomeNewCustomerListener::class,
        \App\Listeners\RegisterCustomerToNewsLetterListener::class,
        \App\Listeners\NotifyAdminViaSlackListener::class
    ],
];

There is another php artisan command called event:generate

php artisan help event:generate
Description:
  Generate the missing events and listeners based on registration

Usage:
  event:generate

Options:
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
      --env[=ENV]       The environment the command should run under
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Any events and listeners registered in the EventServiceProvider that are not already created will automatically be created.

php artisan event:generate
# Events and listeners generated successfully!

Open /App/Listeners/RegisterCustomerToNewsLetterListener.php

  • Cut and paste in the news letter dump from the CustomersController
<?php

namespace App\Listeners;

class RegisterCustomerToNewsLetterListener
{
    /**
     * Handle the event.
     *
     * @return void
     */
    public function handle()
    {
        // Register to news letter
        dump('Register to newsletter');
    }
}

Open /App/Listeners/NotifyAdminViaSlackListener.php

  • Cut and paste in the slack dump message from the CustomersController
<?php

namespace App\Listeners;

class NotifyAdminViaSlackListener
{
    /**
     * Handle the event.
     *
     * @return void
     */
    public function handle()
    {
        // Slack notification to Admin
        dump('Slack message to Admin');
    }
}

The CustomerController.php is now lean and clean with only three lines of code (the sleep(20) can be removed after testing).

    public function store()
    {
        $customer = Customer::create($this->validateRequest());

        event(new NewCustomerHasRegisteredEvent($customer));

        // temp 20s delay before the return.
        sleep(20);

        return redirect('customers');
    }

As the events are in one place, if an event needs to be removed, the line can be removed from the EventServiceProvider.php list.

Another thing to note, in .env there is a QUEUE_CONNECTION=sync, this means things will run in sync, this isn't typically how things are run.

29. 29 8:57 Laravel 5.8 Tutorial From Scratch - e29 - Queues: Database Driver

The user shouldn't have to wait for emails to be sent or images to be resized, or any other background task which may take some time. In production the queue should be use.

In the .env file there is a QUEUE_CONNECTION configuration option. In development the sync driver is recommended, but in production the redis driver is recommended, for this lesson the database driver will be used.

QUEUE_CONNECTION=sync

In the config/queue.php file there is a list of available drivers:

// Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null"

To demonstrate a long running job in the three listeners created in the previous lesson add sleep (10); to each method.

If the database driver is used then the table will need to be created:

php artisan has a command queue:table

php artisan help queue:table
Description:
  Create a migration for the queue jobs database table

Usage:
  queue:table

Options:
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
      --env[=ENV]       The environment the command should run under
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Run the commands to create the table and migrate it.

php artisan queue:table
# Migration created successfully!
php artisan migrate
# Migrating: 2019_04_09_131936_create_jobs_table
# Migrated:  2019_04_09_131936_create_jobs_table

Update the .env file to use the database driver.

QUEUE_CONNECTION=database

After altering the .env file reload the config cache.

php artisan config:cache
# Configuration cache cleared!
# Configuration cached successfully!

Create a new customer and check the email has been received in mailtrap.io, the user will be created, but no email sent. View the table for jobs and there is an entry in the table.

Open sqlite explorer and the tables will be populated with the jobs ready to run.

The jobs in the queue need to be processed. There is a php artisan command queue:work

php artisan help queue:work
Description:
  Start processing jobs on the queue as a daemon

Usage:
  queue:work [options] [--] [<connection>]

Arguments:
  connection               The name of the queue connection to work

Options:
      --queue[=QUEUE]      The names of the queues to work
      --daemon             Run the worker in daemon mode (Deprecated)
      --once               Only process the next job on the queue
      --stop-when-empty    Stop when the queue is empty
      --delay[=DELAY]      The number of seconds to delay failed jobs [default: "0"]
      --force              Force the worker to run even in maintenance mode
      --memory[=MEMORY]    The memory limit in megabytes [default: "128"]
      --sleep[=SLEEP]      Number of seconds to sleep when no job is available [default: "3"]
      --timeout[=TIMEOUT]  The number of seconds a child process can run [default: "60"]
      --tries[=TRIES]      Number of times to attempt a job before logging it failed [default: "0"]
  -h, --help               Display this help message
  -q, --quiet              Do not output any message
  -V, --version            Display this application version
      --ansi               Force ANSI output
      --no-ansi            Disable ANSI output
  -n, --no-interaction     Do not ask any interactive question
      --env[=ENV]          The environment the command should run under
  -v|vv|vvv, --verbose     Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

To run with default options:

php artisan queue:work
# [2019-04-09 13:37:35][1] Processing: App\Listeners\WelcomeNewCustomerListener
# [2019-04-09 13:37:46][1] Processed:  App\Listeners\WelcomeNewCustomerListener
# [2019-04-09 13:37:46][2] Processing: App\Listeners\RegisterCustomerToNewsLetterListener
# "Register to newsletter"
# [2019-04-09 13:37:56][2] Processed:  App\Listeners\RegisterCustomerToNewsLetterListener
# [2019-04-09 13:37:56][3] Processing: App\Listeners\NotifyAdminViaSlackListener
# "Slack message to Admin"
# [2019-04-09 13:38:07][3] Processed:  App\Listeners\NotifyAdminViaSlackListener

As we added sleep (10); to each method on the three listeners, they each took 10 seconds, however the user didn't need to wait! Like php artisan serve. The php artisan queue:work command will need to be kept open for run in the foreground! See the next lesson for how to run in the background.

30. 30 5:40 Laravel 5.8 Tutorial From Scratch - e30 - queue:work In The Background

30.1. Linux

This lesson will run the queue:work job in the background.

php artisan queue:work &
# [1] 29008

29008 is the process ID (PID). To view the currently running jobs type jobs.

jobs
# [1] + running   php artisan queue:work

To see the process ID for the running jobs add -l

jobs -l
# [1] + 29008 done    php artisan queue:work

To stop the running process use kill with the PID.

KILL 29008

To run the command with any output stored in a logs file:

php artisan queue:work > storage/logs/jobs.log &

This will store the output in a log file in Laravel storage location.

Supervisor is recommended to be used to monitor the process and restart any closed job.

30.2. Windows

Create a batch file called queue.bat and place it in the root of the project folder:

php artisan queue:work > storage/logs/jobs.log

Create a schedule job to run the queue.bat, taking care with restarting the batch file on fail and time out.

31. 31 16:11 Laravel 5.8 Tutorial From Scratch - e31 - Deployment: Basic Server Setup - SSH, UFW, Nginx - Part 1

For the install guide see Setting Up Laravel in Ubuntu / DigitalOcean

32. 32 21:52 Laravel 5.8 Tutorial From Scratch - e32 - Deployment: Basic Server Setup - MySQL, PHP 7 - Part 2

Continue with install, see guide above for details.

33. 33 12:52 Laravel 5.8 Tutorial From Scratch - e33 - Deployment: Basic Server Setup - SSL, HTTPS - Part 3

Final setup steps.

34. 34 11:08 Laravel 5.8 Tutorial From Scratch - e34 - Artisan Commands - Part 1

Create a php artisan command to create a new company.

php artisan help make:command
Description:
  Create a new Artisan command

Usage:
  make:command [options] [--] <name>

Arguments:
  name                     The name of the command

Options:
      --command[=COMMAND]  The terminal command that should be assigned [default: "command:name"]
  -h, --help               Display this help message
  -q, --quiet              Do not output any message
  -V, --version            Display this application version
      --ansi               Force ANSI output
      --no-ansi            Disable ANSI output
  -n, --no-interaction     Do not ask any interactive question
      --env[=ENV]          The environment the command should run under
  -v|vv|vvv, --verbose     Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

In the example we will create an add company command:

php artisan make:command AddCompanyCommand
#Console command created successfully.

Open app/Console/Commands/AddCompanyCommand.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

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

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

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

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        //
    }
}
  • $signature is the command which is run after php artisan
    • $signature = 'contact:company';
  • $description displays when php artisan help or php artisan help contact:company is run.
    • $description = 'Adds new company';
php artisan
# ...
contact
  contact:company      Adds new company
# ...

Remove construct as it isn't required for this example.

In handle method:

  • Check the migration database/migrations/2019_04_02_181919_create_companies_table.php for the field names.
  • Create a manual record for name and phone
  • How to add return text:
    • $this->info('Info String here');
    • $this->warn('This is a warning');
    • $this->error('This is an error');
use App/Company
//...
$company = Company::create([
    'name'= > 'Test Company',
    'phone' => '123-123-134',
]);

// Examples of how to return some text:
$this->info('Info String here');
$this->warn('This is a warning');
$this->error('This is an error');

Another item to check is the Model will accept the create method, open Company.php and confirm protected $guarded = []; or the fillable fields are explicitly listed.

php artisan contact:company
# Added: Test Company

Refresh the website and edit the details of a customer, the drop down for Company as an additional company called Test Company

To add input fields:

  • In the $signature add the name of the fields
    • protected $signature = 'contact:company {name}';
  • In the handle method add the arguments with the key
    • 'name' => $this->argument('name'),
php artisan help contact:company
Description:
  Adds new company

Usage:
  contact:company <name>

Arguments:
  name

...

The help file is automatically updated based on the class, methods and arguments.

Adding a new company only takes one argument, which is the company name.

To add the telephone as an optional field:

  • Add phone with a question mark
    • protected $signature = 'contact:company {name} {phone?}';
  • Add the argument to the phone field in the handle method
    • 'phone' => $this->argument('phone') ?? 'N/A',
  • Alternatively the default value can go in the curly brackets
    • protected $signature = 'contact:company {name} {phone=N/A}';
    • 'phone' => $this->argument('phone')',

The finished class looks like this:

<?php

namespace App\Console\Commands;

use App\Company;
use Illuminate\Console\Command;

class AddCompanyCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'contact:company {name} {phone=N/A}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Adds new company';

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $company = Company::create([
            'name' => $this->argument('name'),
            'phone' => $this->argument('phone'),
        ]);
        $this->info('Added: ' . $company->name);
    }
}

Test the command with the following data:

php artisan contact:company "New Company"
# Added: New Company
php artisan contact:company "Another company" "123-321-4567"
# Added: Another company
id name phone created_at updated_at
1 Wisozk-Schimmel 729-222-4723 2019-04-02 19:46:26 2019-04-02 19:46:26
2 Greenfelder-Howe 1-649-758-5626 x84269 2019-04-02 19:46:26 2019-04-02 19:46:26
3 Torphy-Kuvalis +1-560-716-5434 2019-04-02 19:46:26 2019-04-02 19:46:26
4 Test Company 123-123-134 2019-04-09 17:01:27 2019-04-09 17:01:27
5 New Company N/A 2019-04-09 17:30:29 2019-04-09 17:30:29
6 Another company 123-321-4567 2019-04-09 17:31:03 2019-04-09 17:31:03

35. 35 5:00 Laravel 5.8 Tutorial From Scratch - e35 - Artisan Commands - Part 2

start 0:34

This lesson will take the above example and make it interactive.

  • $this->ask will ask a question and expect an keyboard input
  • $this->confirm will ask a question with yes/no answer expected.
<?php

namespace App\Console\Commands;

use App\Company;
use Illuminate\Console\Command;

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

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Adds new company';

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $name = $this->ask('What is the company name?');
        $phone = $this->ask('What is the company\'s phone number?');

        $this->info('Company name is: ' . $name);
        $this->info('Company phone is: ' . $phone);
        if ($this->confirm('Are you ready to insert ' . $name . '?')) {
            $company = Company::create([
                'name' => $name,
                'phone' => $phone,
            ]);
            return $this->info('Added: ' . $company->name);
        }
        $this->warn('No new company was added.');
    }
}

This interactive method guides a use through entering the information, making it more user friendly.

36. 36 8:08 Laravel 5.8 Tutorial From Scratch - e36 - Artisan Commands (Closure) - Part 3

Another way to add commands. Laravel gives a closure based console command.

Open routes/console.php

There is a method called 'inspire'

Artisan::command('inspire', function () {
    $this->comment(Inspiring::quote());
})->describe('Display an inspiring quote');

Run php artisan inspire

  • He who is contented is rich. - Laozi

This is a random quote from vendor/laravel/framework/src/Illuminate/Foundation/Inspiring.php

It is possible to add new commands to console.php:

  • In this example any unused companies will be deleted from the database.
  • whereDoesntHave('customers') will return a collection of companies with no customers
  • each one will be passed to through a function
    • Deleted
    • Warning message displayed.
Artisan::command('contact:company-clean', function () {
    $this->info('Cleaning');
    Company::whereDoesntHave('customers')
        ->get()
        ->each(function ($company) {
            $company->delete();

            $this->warn('Deleted: ' . $company->name);
        });
})->describe('Cleans Up Unused Companies');

Testing:

php artisan contact:company

#  What is the company name?:
#  > New company

#  What is the company's phone number?:
#  > 987-654-1234

# Company name is: New company
# Company phone is: 987-654-1234

#  Are you ready to insert New company? (yes/no) [no]:
#  > y

# Added: New company

php artisan contact:company-clean
# Cleaning
# Deleted: New company

37. 37 7:55 Laravel 5.8 Tutorial From Scratch - e37 - Model Factories

Model factory and database seeders, fake data in the database to use in a development environment.

Laravel ships with a Model factory to create new user data. See database/factories/UserFactory.php

<?php

use App\User;
use Illuminate\Support\Str;
use Faker\Generator as Faker;

//..

$factory->define(User::class, function (Faker $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'email_verified_at' => now(),
        'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
        'remember_token' => Str::random(10),
    ];
});

This will create a user with a fake name, safe email address, time stamp of now and a password of password

Run the factory from tinker

php artisan tinker

To create one user:

>>> factory(\App\User::class)->create();
=> App\User {#2987
     name: "Cali Ondricka",
     email: "ejacobs@example.org",
     email_verified_at: "2019-04-09 18:52:34",
     updated_at: "2019-04-09 18:52:34",
     created_at: "2019-04-09 18:52:34",
     id: 2,
   }

If more users are required enter the quantity as second parameter:

  • factory(\App\User::class, 3)->create();

To create a factory to generate companies:

php artisan help make:factory
Description:
  Create a new model factory

Usage:
  make:factory [options] [--] <name>

Arguments:
  name                  The name of the class

Options:
  -m, --model[=MODEL]   The name of the model
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
      --env[=ENV]       The environment the command should run under
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

It takes an argument of a name and an option of a model.

php artisan make:factory CompanyFactory -m Company

Open database/factories/CompanyFactory.php

<?php

use Faker\Generator as Faker;

$factory->define(App\Company::class, function (Faker $faker) {
    return [
        'name' => $faker->company,
        'phone' => $faker->phoneNumber,
    ];
});

To create a company using tinker:

>>> factory(\App\Company::class)->create();
=> App\Company {#2977
     name: "Wisoky, Trantow and Will",
     phone: "(345) 782-9219 x6787",
     updated_at: "2019-04-09 19:07:38",
     created_at: "2019-04-09 19:07:38",
     id: 9,
   }

To create 10 companies:

factory(\App\Company::class, 10)->create();

38. 38 12:19 Laravel 5.8 Tutorial From Scratch - e38 - Database & Table Seeders

Next we will create database seeders. there is already a command to create users, open DatabaseSeeders.php, uncomment:

$this->call(UsersTableSeeder::class);

However, out of the box Laravel doesn't work:

php artisan db:seed
Seeding: UsersTableSeeder
ReflectionException : Class UsersTableSeeder does not exist

To fix this run the make seeder command for UsersTableSeeder

php artisan make:seeder UsersTableSeeder
# Seeder created successfully.

Open database/seeds/UsersTableSeeder.php

This looks identical to the DatabaseSeeder, the DatabaseSeeder should be thought of as the main file, which will call all the individual seeder files. In the run method call the user Factory requirements:

public function run()
{
    factory(\App\User::class, 3)->create();
}

Now run db:seed

php artisan db:seed
# Seeding: UsersTableSeeder
# Database seeding completed successfully.
id name email email_verified_at password remember_token created_at updated_at"
1 Mickey Mouse Mickey @mouse.test NULL $2y$10$xd3 ai.xC5k7tU 01lIo69h.u WKYkK1aQon juac08r20 3E83SCnUa5C NULL 2019-04-05 12:47:52 2019-04-05 12:47:52"
2 Cali Ondricka ejacobs @example.org 2019-04-09 18:52:34 $2y$10$92I XUNpkjO0rO Q5byMi.Ye4 oKoEa3Ro9l lC/.og/at2 .uheWG/ igi TCoW1i35EJ 2019-04-09 18:52:34 2019-04-09 18:52:34"
3 Prof. Miracle Lehner collins.corrine @example.com 2019-04-09 19:30:28 $2y$10$92I XUNpkjO0rO Q5byMi.Ye4 oKoEa3Ro9l lC/.og/at2 .uheWG/ igi VQy1JHkuPS 2019-04-09 19:30:28 2019-04-09 19:30:28"
4 Shea Reichert abshire.aaliyah @example.org 2019-04-09 19:30:28 $2y$10$92I XUNpkjO0rO Q5byMi.Ye4 oKoEa3Ro9l lC/.og/at2 .uheWG/ igi kkVEeRkJhz 2019-04-09 19:30:28 2019-04-09 19:30:28"
5 Riley Koelpin PhD hilbert15 @example.com 2019-04-09 19:30:28 $2y$10$92I XUNpkjO0rO Q5byMi.Ye4 oKoEa3Ro9l lC/.og/at2 .uheWG/ igi LJWKmUYkCn 2019-04-09 19:30:28 2019-04-09 19:30:28"

To create a Company seeder, the naming convention is to use the plural, so Companies:

php artisan make:seeder CompaniesTableSeeder
# Seeder created successfully.

Open CompaniesTableSeeder.php

  • Using the Company factory created last lesson, add 10 companies:
<?php
namespace App;

use Illuminate\Database\Seeder;

class CompaniesTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        factory(\App\Company::class, 10)->create();
    }
}
php artisan make:factory CustomerFactory --model=Customer
php artisan make:seeder CustomersTableSeeder

As with Companies, feed to CustomerFactory.php:

<?php

use App\Customer;
use Faker\Generator as Faker;

$factory->define(Customer::class, function (Faker $faker) {
    return [
        'name' => $faker->company,
        'company_id' => $faker->numberBetween($min = 1, $max = 10), // This line was added in lesson 14
        // Alternative: 'company_id' => factory(Company::class)->create();
        'email' => $faker->unique()->companyEmail,
        'active' => $faker->boolean,
    ];
});

Then the CustomersTableSeeder:

<?php

use Illuminate\Database\Seeder;

class CustomersTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        factory(App\Customer::class, 10)->create()->each(function ($customer) {
            $customer->make();
        });
    }
}

Now all the tables can be recreated

php artisan migrate:fresh
Dropped all tables successfully.
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table
Migrating: 2019_03_30_114129_create_customers_table
Migrated:  2019_03_30_114129_create_customers_table
Migrating: 2019_04_02_181919_create_companies_table
Migrated:  2019_04_02_181919_create_companies_table
Migrating: 2019_04_09_131936_create_jobs_table
Migrated:  2019_04_09_131936_create_jobs_table

Then seeded:

php artisan db:seed
Seeding: UsersTableSeeder
Seeding: CustomersTableSeeder
Seeding: CompaniesTableSeeder
Database seeding completed successfully.

This can also be run in one command:

php artisan migrate:fresh --seed
Dropped all tables successfully.
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table
Migrating: 2019_03_30_114129_create_customers_table
Migrated:  2019_03_30_114129_create_customers_table
Migrating: 2019_04_02_181919_create_companies_table
Migrated:  2019_04_02_181919_create_companies_table
Migrating: 2019_04_09_131936_create_jobs_table
Migrated:  2019_04_09_131936_create_jobs_table
Seeding: UsersTableSeeder
Seeding: CustomersTableSeeder
Seeding: CompaniesTableSeeder
Database seeding completed successfully.

Troubleshooting, sometimes the autoloader need the be recreated:

composer dump-autoload

39. 39 19:46 Laravel 5.8 Tutorial From Scratch - e39 - Image Upload - Part 1

When a customer is creating a account there is an option to upload an image. Also available for editing a user.

When uploading a file an encryption type needs to be added. Edit customer/create.blade.php & customer/edit.blade.php in the form tag add:

  • enctype="multipart/form-data"
<form action="/customers" method="POST" enctype="multipart/form-data"></form>

Change migrations to allow an image to be stored. Open database\migrations\2019_03_30_114129_create_customers_table.php, add an optional filed for the image name. It is optional so can be null.

  • $table->string('image')->nullable();
public function up()
{
    Schema::create('customers', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->unsignedInteger('company_id');
        $table->string('name');
        $table->string('email')->unique();
        $table->integer('active');
        $table->string('image')->nullable();
        $table->timestamps();
    });
}

Recreate and seed it all databases:

php artisan migrate:fresh --seed
Dropped all tables successfully.
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table
Migrating: 2019_03_30_114129_create_customers_table
Migrated:  2019_03_30_114129_create_customers_table
Migrating: 2019_04_02_181919_create_companies_table
Migrated:  2019_04_02_181919_create_companies_table
Migrating: 2019_04_09_131936_create_jobs_table
Migrated:  2019_04_09_131936_create_jobs_table
Seeding: UsersTableSeeder
Seeding: CustomersTableSeeder
Seeding: CompaniesTableSeeder
Database seeding completed successfully.

form.blade.php:

  • Add a form with upload.
<div class="form-group row">
  <label for="image" class="col-sm-2 col-form-label"
    >Profile Image (optional)</label
  >
  <div class="col-sm-10">
    <input type="image" src="" alt="" name="image" id="image" />
  </div>
  <div class="text-danger offset-sm-2">
    <div class="ml-3">
      <small>{{ $errors->first('image') }}</small>
    </div>
  </div>
</div>

CustomerController.php:

  • Store method
    • Needs to handle images
    • Needs to be validated, however it is optional.
    • if statement for hasImage.. validate image => file|image|max:5000

Example 1:

private function validateRequest()
{
    $validatedData = request()->validate([
        'name' => 'required|min:3',
        'email' => 'required|email',
        'active' => 'required',
        'company_id' => 'required'
    ]);

    if (request()->hasFile('image')) {
        dd(request()->image);
        request()->validate([
            'image' => 'file|image|max:5000',
        ]);
    };
    return $validatedData;
}

Info on the image being uploaded (viewable using dd(request()->image);)

UploadedFile {#243 ▼
  -test: false
  -originalName: "IMG_20190406_173438.jpg"
  -mimeType: "image/jpeg"
  -error: 0
  #hashName: null
  path: "C:\Users\UserName\AppData\Local\Temp"
  filename: "php4F01.tmp"
  basename: "php4F01.tmp"
  pathname: "C:\Users\UserName\AppData\Local\Temp\php4F01.tmp"
  extension: "tmp"
  realPath: "C:\Users\UserName\AppData\Local\Temp\php4F01.tmp"
  aTime: 2019-04-10 12:43:24
  mTime: 2019-04-10 12:43:24
  cTime: 2019-04-10 12:43:24
  inode: 0
  size: 2651291
  perms: 0100666
  owner: 0
  group: 0
  type: "file"
  writable: true
  readable: true
  executable: false
  file: true
  dir: false
  link: false
  linkTarget: "C:\Users\UserName\AppData\Local\Temp\php4F01.tmp"

Refactor the code:

  • Alternative method called tap, which includes a closure.
private function validateRequest()
{
    return tap(
        request()->validate([
            'name' => 'required|min:3',
            'email' => 'required|email',
            'active' => 'required',
            'company_id' => 'required'
        ]),
        function () {
            if (request()->hasFile('image')) {
                dd(request()->image);
                request()->validate([
                    'image' => 'file|image|max:5000',
                ]);
            }
        }
    );
}

Store the image:

  • new method storeImage()
  • Image returns an upload file class with a store method, this takes the directory and the location.
    • Images will be stored in: storage\app\public\uploads
private function storeImage($customer)
{
    if (request()->has('image')) {
        $customer->update([
            'image' => request()->image->store('uploads', 'public'),
        ]);
    }
}

The storage directory isn't accessible to the public.

  • A symbolic link needs to be created so users can view the storage location (it isn't a sub directory of the public folder)
php artisan help storage:link
Description:
  Create a symbolic link from "public/storage" to "storage/app/public"

Usage:
  storage:link

Options:
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
      --env[=ENV]       The environment the command should run under
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
php artisan storage:link
# The [public/storage] directory has been linked.

The uploaded file can be accessed publicly:

Next the image need to be shown in the view, open show.blade.php:

  • Add if statement and display the image, if there is one.
@if($customer->image)
    <div class="row">
        <div class="col-12">
            <img
            width="200px"
            class="img-thumbnail"
            src="{{ asset('storage/' . $customer->image ) }}">
        </div>
    </div>
@endif

40. 40 11:24 Laravel 5.8 Tutorial From Scratch - e40 - Image Upload: Cropping & Resizing - Part 2

Fist clean up the image upload so it is required sometimes.

private function validateRequest()
{
    return request()->validate([
        'name' => 'required|min:3',
        'email' => 'required|email',
        'active' => 'required',
        'company_id' => 'required',
        'image' => 'sometimes|file|image|max:5000',
    ]);
}

We will resize an image on the fly, by pulling in a package called intervention image.

composer require intervention/image

The package will be added to the composer.json file and installed.

Using version ^2.4 for intervention/image
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 4 installs, 0 updates, 0 removals
  - Installing ralouphie/getallheaders (2.0.5): Loading from cache
  - Installing psr/http-message (1.0.1): Loading from cache
  - Installing guzzlehttp/psr7 (1.5.2): Loading from cache
  - Installing intervention/image (2.4.2): Downloading (100%)
intervention/image suggests installing ext-imagick (to use Imagick based image processing.)
intervention/image suggests installing intervention/imagecache (Caching extension for the Intervention Image library)
Writing lock file
Generating optimized autoload files
> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover --ansi
Discovered Package: [beyondcode/laravel-dump-server[39m
Discovered Package: [fideloper/proxy[39m
Discovered Package: [intervention/image[39m
Discovered Package: [laravel/tinker[39m
Discovered Package: [nesbot/carbon[39m
Discovered Package: [nunomaduro/collision[39m
[32mPackage manifest generated successfully.[39m

Open CustomersController.php

  • In storeImage method add:
    • $image = Image::make()
    • In the storeImage method
      • Add the line to resize the image

For more details see the intervention.io getting started guide

use Intervention\Image\Facades\Image;
// ...
private function storeImage($customer)
{
    if (request()->has('image')) {
        $customer->update([
            'image' => request()->image->store('uploads', 'public'),
        ]);
        // Add the following lines (ony adjust if one has been uploaded):
        $image = Image::make(public_path('storage/' . $customer->image))
            ->fit(300, 300);
        $image->save();
    }
}

Note: the save method will overwrite the original image, my passing the save method a parameter, it is possible to save the image as a different file name or location.

41. 41 5:31 Laravel 5.8 Tutorial From Scratch - e41 - Telescope

Documentation: Laravel Telescope

Laravel Telescope is an elegant debug assistant for the Laravel framework. Telescope provides insight into the requests coming into your application, exceptions, log entries, database queries, queued jobs, mail, notifications, cache operations, scheduled tasks, variable dumps and more. Telescope makes a wonderful companion to your local Laravel development environment. - Introduction

composer require laravel/telescope
Using version ^2.0 for laravel/telescope
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
  - Installing moontoast/math (1.1.2): Downloading (100%)
  - Installing laravel/telescope (v2.0.4): Downloading (100%)
Writing lock file
Generating optimized autoload files
> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover --ansi
Discovered Package: [beyondcode/laravel-dump-server[39m
Discovered Package: [fideloper/proxy[39m
Discovered Package: [intervention/image[39m
Discovered Package: [laravel/telescope[39m
Discovered Package: [laravel/tinker[39m
Discovered Package: [nesbot/carbon[39m
Discovered Package: [nunomaduro/collision[39m
[32mPackage manifest generated successfully.[39m
php artisan telescope:install
# Publishing Telescope Service Provider...
# Publishing Telescope Assets...
# Publishing Telescope Configuration...
# Telescope scaffolding installed successfully.
php artisan migrate
# Migrating: 2018_08_08_100000_create_telescope_entries_table
# Migrated:  2018_08_08_100000_create_telescope_entries_table

For some reason I had major problems with getting telescope to work with sqlite, I had to create a new database in mysql and run php artisan migrate --seed.

If not already serving the site run php artisan serve

Open the telescope site at http://127.0.0.1:8000/telescope

  • Navigate the left hand menu and look at the records.

Create 50 customers using tinker:

php artisan tinker
# Psy Shell v0.9.9 (PHP 7.3.3 — cli) by Justin Hileman
>>> factory(\App\Customer::class, 50)->create();
# 50 customer created.

In Telescope:

Queries
Query                                                           Duration  Happened
select * from `companies` where `companies`.`id` = ? limit 1    0.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    0.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    1.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    1.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    0.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    0.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    0.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    0.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    1.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    1.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    0.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    1.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    0.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    1.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    0.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    0.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    0.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    1.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    1.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    1.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    1.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    0.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    1.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    0.00ms    35s ago
select * from `companies` where `companies`.`id` = ? limit 1    0.00ms    35s ago
...

This is called an N + 1 problem, this will be fixed in the next video.

42. 42 5:03 Laravel 5.8 Tutorial From Scratch - e42 - Lazy Loading vs. Eager Loading (Fixing N + 1 Problem)

Currently the when all the customers are selected, each customer has to request the company information, as the company is foreign key on the customer table. This is called lazy loading. For every customer there will be one select statement to fetch the company, therefore there will be N + 1 statements, 1 for all the customers and 50 for the company.

The fix is to request all the customers and the company in one query, in the CustomersController.php index method:

public function index()
{
    // Was $customers = Customer::all();
    $customers = Customer::with('company')->get();
    // temp use dd to view the data
    dd($customers->toArray());
    return view('customers.index', compact('customers'));
}
array:60 [▼
  0 => array:9 [▼
    "id" => 1
    "company_id" => 8
    "name" => "Reynolds Ltd"
    "email" => "gay.shields@johnston.com"
    "active" => "Active"
    "image" => null
    "created_at" => "2019-04-11 09:46:06"
    "updated_at" => "2019-04-11 09:46:06"
    "company" => array:5 [▼
      "id" => 8
      "name" => "Koch-Hamill"
      "phone" => "307-842-5732 x7033"
      "created_at" => "2019-04-11 09:46:06"
      "updated_at" => "2019-04-11 09:46:06"
    ]
  ]
  1 => array:9 [▼
    "id" => 2
    "company_id" => 1
    "name" => "Moore Group"
    "email" => "gmurphy@spinka.info"
    "active" => "Active"
    "image" => null
    "created_at" => "2019-04-11 09:46:06"
    "updated_at" => "2019-04-11 09:46:06"
    "company" => array:5 [▼
      "id" => 1
      "name" => "Leffler-Schuster"
      "phone" => "+1.372.731.6616"
      "created_at" => "2019-04-11 09:46:06"
      "updated_at" => "2019-04-11 09:46:06"
...

View the telescope Queries and there is now only three queries listed:

Query                                                                                  Duration   Happened
select * from `companies` where `companies`.`id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)    0.00ms    5s ago
select * from `customers`                                                              1.00ms    5s ago
select * from `users` where `id` = ? limit 1                                           1.00ms    5s ago

The last one is do do with the signed in user.

The thing to watch for is allot of select statements with limit 1.

43. 43 4:19 Laravel 5.8 Tutorial From Scratch - e43 - Pagination

Laravel can take 50 customers, instead of sending all customers to the Laravel can send a set number to the view, e.g. 15, with the option to view the next page. This is called pagination. Laravel makes it very easy to do. Laravel Documentation

In the CustomersController.php index method:

  • change the get() to paginate(15)
public function index()
{
    $customers = Customer::with('company')->paginate(15); // Update

    return view('customers.index', compact('customers'));
}

Refresh the customers page, only 15 customers will be displayed. The view needs to be updated with the previous and next links.

In the resources\views\customers\index.blade.php add a pagination bar using the laravel blade helper links():

// ...
@endforeach
<div class="row pt-5">
    <div class="col-12 d-flex justify-content-center">
        {{ $customers->links() }}
    </div>
</div>
@endsection

Create 500 customers:

php artisan tinker
>>> factory(\App\Customer::class, 500)->create();

Refresh the Customers page and the pagination is automatically updated to 38 pages. To change the number of customers per page change the number in the CustomersController index method, e.g. paginate(25) will display 25 customer per page.

44. 44 16:08 Laravel 5.8 Tutorial From Scratch - e44 - Policies

Polices are used to authorise users to do actions in our app. E.G. admin users and regular users can do different things.

  • Policies attach to models.

Regular users can add new users to the list

php artisan help make:policy
Description:
  Create a new policy class

Usage:
  make:policy [options] [--] <name>

Arguments:
  name                  The name of the class

Options:
  -m, --model[=MODEL]   The model that the policy applies to
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
      --env[=ENV]       The environment the command should run under
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
  • Name is required, -m can specify and model.
php artisan make:policy CustomerPolicy -m Customer
# Policy created successfully.

A new policy class will be created, app\Policies\CustomerPolicy.php, with each of the restful methods for the view, create, update, delete, restore, and forceDelete actions. Each method needs to return either true of false and will be aligned to the Controller for the same model.

For this example the admin will be defined by the email address, for a larger scale app a field could be added to the user table defining standard and admin users. In the created method create a policy so the add new customer button will be viable to admin users only.

public function create(User $user)
{
    return in_array($user->email, [
        'admin@admin.test',
    ]);
}

In the CustomerController.php store method

  • add $this->authorize('create', Customer::class);
public function store()
{
    // Add this line first, to validate the user is authorised before any actions:
    $this->authorize('create', Customer::class);

    $customer = Customer::create($this->validateRequest());

    $this->storeImage($customer);

    event(new NewCustomerHasRegisteredEvent($customer));

    return redirect('customers');
}

If a customer, who is not an admin tried to create a user they will receive a 403 | Forbidden, as they are not authorised.

Next, in the view the Add Ne Customer button can be hidden, from users who are not authorised. In resources\views\customers\index.blade.php

  • Use the @can method to test if the user is authorized to add a new customer, only display the button if they are.
  • Note the full name space for the class.
@can('create', App\Customer::class)
    <div class="row">
        <div class="col-12">
            <h1>Customers</h1>
            <p><a href="{{ route('customers.create') }}"><button class="btn btn-primary">Create New Customer</button></a></p>
        </div>
    </div>
@endcan

Another example, for only allowing admin to delete.

  • In the CustomerPolicy.php delete method use the same logic as the create method.
public function delete(User $user, Customer $customer)
{
    return in_array($user->email, [
        'admin@admin.test',
    ]);
}
  • In CustomersController.php destroy method use

    • Add $this->authorize('delete', $customer);
public function destroy(Customer $customer)
{
    // Add this check before any actions
    $this->authorize('delete', $customer);

    $customer->delete();

    return redirect('customers');
}

Some models have two parameters, e.g. the view has user and customer, as the customer data already exists.

Policies can also be applied to routes. e.g. in the web.php router, for testing purposes

  • Comment out the Route::resources('customers', 'CustomerController');
  • Route::get(customers/{customer}, 'CustomersController@show)->middleware('can:view,customer')

Info: Laravel now has an auto register for the policy, if the policy doesn't register they can be manually added in the AuthServiceProvider.php, add to the $policies array, e.g.:

    protected $policies = [
        'App\Customer' => 'App\Policies\CustomerPolicy',
    ];
]

Homework 1:

  • update the index.blade.php to show a link to view the customer details only if you are an admin (authorized).

Pause the video to do this.

@foreach ($customers as $customer)
    <div class="row">
        <div class="col-1"> {{ $customer->id }} </div>
        <div class="col-5">
            @can('view', $customer)
                <a href="{{ route('customers.show', ['customer' => $customer]) }}">
            @endcan
                {{ $customer->name }}
            @can('view', $customer)
                </a>
            @endcan
        </div>
        <div class="col-5"> {{ $customer->company->name }} </div>
        <div class="col-1"> {{ $customer->active}} </div>
    </div>
@endforeach

CustomerPolicy.php:

// I added a parameters for the list of administrators
private $administrators = ['admin@admin.test',];
// For the view method as well as the delete and view methods.
public function view(User $user, Customer $customer)
{
    // then used $this->administrators instead of a separate array.
    return in_array($user->email, $this->administrators);
}

My method used two @can to remove the a tag, the tutor demonstrated a @can and @cannot method, which reads better:

  • Use @can('view, $customer') for the block of html to display the a tag and customer.
  • Use @cannot('view, $customer') to display the customer without a link.

Homework 2:

Update all the policies.

  • Updated and test all policies for admin user and standard user including manually setting the url with standard user.

For the documentation of Authorization and policies see: Authorization documentation

Reading the documentation there is a way to allow administrators access to all (Policy Filters), which basically override all policies

For certain users, you may wish to authorize all actions within a given policy. To accomplish this, define a before method on the policy. The before method will be executed before any other methods on the policy, giving you an opportunity to authorize the action before the intended policy method is actually called. This feature is most commonly used for authorizing application administrators to perform any action:

public function before($user, $ability)
{
    if ($user->isSuperAdmin()) {
        return true;
}

45. 45 9:10 Laravel 5.8 Tutorial From Scratch - e45 - Eloquent Relationships - One To One (hasOne, BelongsTo)

Create a new model for Users who have 1 phone. This relationship isn't very common.

php artisan make:model Phone -m
Model created successfully.
Created Migration: 2019_04_15_155113_create_phones_table

This creates a migration and one model.

Open the model Phone.php and set fillable to an empty array.

protected $fillable = [];

CreatePhoneTable class (database \migrations \2019_04_15_155113_create_phones_table.php) need to have several fields added to the up method

  • $table->string('phone');
  • $table->unsignedBigInteger('user_id')->index();
    • Note: Laravel naming convention for model name, singular underscore id (user_id).
  • $table->foreign('user_id')->references('id')->on('users');
    • To setup a foreign key on the user_id
public function up()
{
    Schema::create(
        'phones', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('phone');
            $table->unsignedBigInteger('user_id')->index();
            $table->timestamps();

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

Next link the user and phone models to each other.

Open Users.php add a new method for phone:

  • Note: phone is again singular for the model name
public function phone()
{
    return $this->hasOne(\App\Phone::class);
}

In the Phone.php model add the inverse:

public function user()
{
    return $this->belongsTo(\App\User::class);
}

To clarify, the table that has the reference, in this case the user_id reference is is the one that gets the belongsTo, the table that is referenced is the one that gets the hasOne method.

To test open the routes file web.php

Route::get('/phone', function () {
    $user = factory(\App\User::class)->create();
    $phone = new \App\Phone();

    $phone->phone = '123-123-1234';

    $user->phone()->save($phone);
})

Next migrate the database:

php artisan migrate

Open the browser to http://localhost:8000/phone, nothing will display, should be no errors!

Using tinker the links can be tested:

php artisan tinker
>>> $phone = \App\Phone::first();
# => App\Phone {#2937
#      id: "1",
#      phone: "222-333-4567",
#      user_id: "1",
#      created_at: "2019-04-15 19:31:00",
#      updated_at: "2019-04-15 19:31:00",
#    }
>>> $phone->user->name;
# => "Claire Kris"
>>> $user = \App\User::first();
# => App\User {#2932
#      id: "1",
#      name: "Claire Kris",
#      email: "cedrick85@example.net",
#      email_verified_at: "2019-04-15 19:31:00",
#      created_at: "2019-04-15 19:31:00",
#      updated_at: "2019-04-15 19:31:00",
#    }
>>> $user->phone->phone;
# => "222-333-4567"

There is a shortcut for creating, in web.php:

Route::get('/phone', function () {
    $user = factory(\App\User::class)->create();
    $user->phone()->create([
      'phone' = '222-333-4567',
    ]);
})

If there is an exception, open the model Phone.php and either add phone to the empty $fillable array, or remove $fillable and add $guarded as an empty array, which will allow all.

protected $fillable = ['phone'];

or

protected $guarded = [];

46. 46 7:40 Laravel 5.8 Tutorial From Scratch - e46 - Eloquent Relationships One To Many (hasMany, BelongsTo)

Next type of relationship is the hasMany / belongsTo, this is one of the most common relationships. The owner has many of the secondary property. In this example a user has many posts. A user can have an unlimited number of posts, but a post belongs to a user.

In this example a fresh install of Laravel has been created, called one-to-many.

First make a model with a migration.

php artisan make:model Post -m
Model created successfully.
Created Migration: 2019_04_16_090110_create_posts_table

Open Posts.php, to prevent any mass assignment problems set guarded to an empty array.

class Post extends Model
{
    protected $guarded = [];
}

Open the database\migrations\2019_04_16_090110_create_posts_table.php.php

  • Add three fields
    • $table->unsignedBigInteger('user_id');
    • $table->string('title');
    • $table->text('body');
public function up()
{
    Schema::create(
        'posts', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger('user_id'); // ->index()?
            $table->string('title');
            $table->text('body');
            $table->timestamps();
            // foreign key? $table->foreign('user_id')->references('id')->on('users');
        }
    );
}

Use php artisan migrate to create the database(s) (or php artisan migrate:fresh for a renewed setup)

php artisan migrate
Migrating: 2019_04_16_090110_create_posts_table
Migrated:  2019_04_16_090110_create_posts_table

Open the Post.php model.

  • Add a user method, with belongsTo.
public function user()
{
        return $this->belongsTo(App\User::class);
}

Open User.php model

  • Add the inverse, a user has many posts (note: plural - not singular)
public function posts()
{
    return $this->hasMany(\App\Post::class);
}

Open web.php

  • Create a quick route to create a post.
Route::get(
    '/post', function () {

        $post = new \App\Post(
            [
            'title' => 'Title here',
            'body' => 'Body here'
            ]
        );

        dd($post);
    }
);
Post {#365 ▼
...
  +wasRecentlyCreated: false
  #attributes: array:2 [▼
    "title" => "Title here"
    "body" => "Body here"
...

The post was instantiated, but not persisted. It needs to be saved.

Still in web.php

  • Create a user, using the user factory.
    • Long form is to pass in the user->id
Route::get(
    '/post', function () {

        $user = factory(\App\User::class)->create();

        $post = new \App\Post(
            [
            'title' => 'Title here',
            'body' => 'Body here',
            'user_id' => $user->id,
            ]
        );

        $post->save();
    }
);
  • The short form method:
<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
 */

Route::view('/', 'home');

// Route::view('contact', 'contact');
Route::get('contact', 'ContactFormController@create')->name('contact.create');
Route::post('contact', 'ContactFormController@store')->name('contact.store');

Route::view('about', 'about')->name('about'); //->Middleware('test');

// Route::get('customers', 'CustomersController@index');
// Route::get('customers/create', 'CustomersController@create');
// Route::post('customers', 'CustomersController@store');
// Route::get('customers/{customer}', 'CustomersController@show');
// Route::get('customers/{customer}/edit', 'CustomersController@edit');
// Route::patch('customers/{customer}', 'CustomersController@update');
// Route::delete('customers/{customer}', 'CustomersController@destroy');

Route::resource('customers', 'CustomersController'); //->middleware('auth');

Auth::routes();

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

Route::get(
    '/phone', function () {
        $user = factory(\App\User::class)->create();
        $user->phone()->create(
            [
            'phone' => '222-333-4567',
            ]
        );
    }
);


Route::get(
    '/post', function () {

        $user = factory(\App\User::class)->create();

        $user->posts()->create(
            [
            'title' => 'Title here ' . random_int(1, 10),
            'body' => 'Body here '  . random_int(1, 100),
            ]
        );

        return $user->posts;
    }
);

To update an existing post, through the user relationship:

Route::get(
'/post', function () {

    $user = factory(\App\User::class)->create();

    $user->posts()->create(
        [
        'title' => 'Title here ' . random_int(1, 10),
        'body' => 'Body here '  . random_int(1, 100),
        ]
    );

    $user->posts->first()->title = "New Title";
    $user->posts->first()->body = 'New Better Body';

    $user->push();

    echo 'Created post:' . "\n";

    return $user->posts;
}
[
  {
    "id": 5,
    "user_id": 13,
    "title": "New Title",
    "body": "New Better Body",
    "created_at": "2019-04-16 10:01:32",
    "updated_at": "2019-04-16 10:01:32"
  }
]

47. 47 13:54 Laravel 5.8 Tutorial From Scratch - e47 - Eloquent Relationships Many To Many (BelongsToMany)

We're going to be tackling a many-to-many relationship between two tables. Working with the user table and a new table for roles.

  • User.php already exists, it ships with Laravel.

Make a new model for Roles with a migration.

php artisan make:model -m Role

Or with the artisan extension: F1 mm Role yes

Open the migration database\migrations\2019_04_19_102003_create_roles_table.php

public function up()
{
    Schema::create('roles', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->string('name');
        $table->timestamps();
    });
}

In a many to many relationship there is a third table, called a pivot table which connects them.

php artisan has the option to make a migration.

  • Naming convention is to use both existing models, in alphabetical order and singular.
php artisan make:migration create_role_user_table --create role_user

Or using artisan extension: F1 mmig create_role_user_table yes role_user

Open database\migrations\2019_04_19_103917_create_role_user_table.php

  • Add role_id and user_id fields.
  • timestamps are optional, sometimes used to check when a user was granted a role.
public function up()
{
    Schema::create('role_user', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->unsignedBigInteger('role_id');
        $table->unsignedBigInteger('user_id');
        $table->timestamps();
    });
}

Run the migration

php artisan migrate

Or F1 migrate

Tutor manually added the roles to the table, I created the following seeder to do it automatically:

php artisan make:seeder RolesTableSeeder

Or F1 ms RolesTableSeeder

Open database\seeds\RoleTableSeeder.php (for the source see so question)

<?php

use Illuminate\Database\Seeder;

class RolesTableSeeder extends Seeder
public function run()
{
    // Deletes any existing data (if the table is not empty)
    DB::table('roles')->truncate();

    // Adds the required roles
    App\Role::create(['name' => 'delete_user']);
    App\Role::create(['name' => 'add_user']);
}

Add the RolesTableSeeder to the database\seeds\DatabaseSeeder.php (for future requirement)

public function run()
{
    $this->call(UsersTableSeeder::class);
    $this->call(CustomersTableSeeder::class);
    $this->call(CompaniesTableSeeder::class);
    $this->call(PostsTableSeeder::class);
    $this->call(RolesTableSeeder::class);
}

Run the seeder for that class:

php artisan db:seed --class=RolesTableSeeder
# Database seeding completed successfully.

Continue with lesson.

In the routes file web.php:

  • Create a route to create a user
Route::get('/users', function () {
    factory(\App\User::class)->create();
    return 'User created';
});

Now the user and roles have been created they can be linked.

Open the app\User.php model

public function roles()
{
    return $this->belongsToMany(\App\Role::class);
}

The app\Role.php model has exactly the same relationship.

public function users()
{
    return $this->belongsToMany(User::class);
}

Now the user has been created it can be used, re-write the route to get the first user:

Route::get('/users', function () {
    // Get the first user:
    $user = \App\User::first();

    // Get a collection of roles:
    $roles = \App\Role::all();

    // Create the relationship
    $user->roles()->attach($roles);

    return 'User and roles relationship created for user ' . $user->name .' (id:'. $user->id . ')';
});

Check the the role_user table:

id role_id user_id created_at updated_at
1 1 1 null null
2 2 1 null null

Example of how to detach a role from a user, back in web.php:

Route::get('/users', function () {
    // Get the first user:
    $user = \App\User::first();

    // Get the first role:
    $role = \App\Role::first();

    // detach (remove) the relationship
    $user->roles()->detach($role);

    $message = 'User and role relationship removed for:<br/>';
    $message .= 'User: ' . $user->name . ' (id:'. $user->id . ')<br/>';
    $message .= 'Role: ' . $role->name .' (id:'. $role->id . ')';

    return $message;
    /*
    User and role relationship removed for:
    User: Deondre Thompson IV (id:1)
    Role: delete_user (id:1)
    */
});

The role_user table only has the one relationship now:

id role_id user_id created_at updated_at
2 2 1 null null

The time stamps are not working, to fix this open the User.php and Role.php models and add ->withTimestamps(); to the relationship methods.

  • User.php:
public function roles()
{
    return $this->belongsToMany(\App\Role::class)->withTimestamps();
}
  • Role.php:
public function users()
{
    return $this->belongsToMany(User::class)->withTimestamps();
}

Attach the role once more:

Route::get('/users', function () {
    // Get the first user:
    $user = \App\User::first();

    // Get the first role:
    $role = \App\Role::first();

    // attach (add) the relationship
    $user->roles()->attach($role);

    $message = 'User and role relationship added for:<br/>';
    $message .= 'User: ' . $user->name . ' (id:'. $user->id . ')<br/>';
    $message .= 'Role: ' . $role->name .' (id:'. $role->id . ')';

    return $message;
    /*
    User and role relationship added for:
    User: Deondre Thompson IV (id:1)
    Role: delete_user (id:1)
    */
});

The time stamps are now added (for new records):

id role_id user_id created_at updated_at
2 2 1 null null
3 1 1 Fri Apr 19 2019 12:37:58 GMT+0100 (GMT Daylight Time) Fri Apr 19 2019 12:37:58 GMT+0100 (GMT Daylight Time)

Now to add some more data, to the Roles table add some more records.

  • I added them using the seeder:
public function run()
{
    DB::table('roles')->truncate();

    App\Role::create(['name' => 'delete_user']);
    App\Role::create(['name' => 'add_user']);
    App\Role::create(['name' => 'modify_user']);  // Added
    App\Role::create(['name' => 'delete_comments']); // Added
    App\Role::create(['name' => 'edit_comments']); // Added
}

Rerun the seeder:

php artisan db:seed --class:RolesTableSeeder

Roles table is now updated:

id name created_at updated_at
1 delete_user Fri Apr 19 2019 12:48:19 GMT+0100 (GMT Daylight Time) Fri Apr 19 2019 12:48:19 GMT+0100 (GMT Daylight Time)
2 add_user Fri Apr 19 2019 12:48:20 GMT+0100 (GMT Daylight Time) Fri Apr 19 2019 12:48:20 GMT+0100 (GMT Daylight Time)
3 modify_user Fri Apr 19 2019 12:48:20 GMT+0100 (GMT Daylight Time) Fri Apr 19 2019 12:48:20 GMT+0100 (GMT Daylight Time)
4 delete_comments Fri Apr 19 2019 12:48:20 GMT+0100 (GMT Daylight Time) Fri Apr 19 2019 12:48:20 GMT+0100 (GMT Daylight Time)
5 edit_comments Fri Apr 19 2019 12:48:20 GMT+0100 (GMT Daylight Time) Fri Apr 19 2019 12:48:20 GMT+0100 (GMT Daylight Time)

In this example a new user will be allowed to do roles 1, 3 & 5.

  • Instead of passing in the role, an array with the ids can be passed in
Route::get('/users', function () {
    // Get the first user:
    $user = \App\User::first();

    // detach (remove) the relationship
    $user->roles()->attach([1, 3, 5]);

    $message = 'User and roles relationship added for:<br/>';
    $message .= 'User: ' . $user->name . ' (id:'. $user->id . ')<br/>';

    return $message;
    /*
    User and roles relationship added for:
    User: Deondre Thompson IV (id:1)
    */
});

The role_user table now has those roles with timestamps:

id role_id user_id created_at updated_at
2 2 1 null null
3 1 1 Fri Apr 19 2019 12:37:58 GMT+0100 (GMT Daylight Time) Fri Apr 19 2019 12:37:58 GMT+0100 (GMT Daylight Time)
4 1 1 Fri Apr 19 2019 12:54:12 GMT+0100 (GMT Daylight Time) Fri Apr 19 2019 12:54:12 GMT+0100 (GMT Daylight Time)
5 3 1 Fri Apr 19 2019 12:54:12 GMT+0100 (GMT Daylight Time) Fri Apr 19 2019 12:54:12 GMT+0100 (GMT Daylight Time)
6 5 1 Fri Apr 19 2019 12:54:12 GMT+0100 (GMT Daylight Time) Fri Apr 19 2019 12:54:12 GMT+0100 (GMT Daylight Time)

From the above we can see there is a duplicate data, user 1 and role 1 are listed twice. Record 3 and 4.

  • First clear the table:
Route::get('/users', function () {
    // Get the first user:
    $user = \App\User::first();

    // Clear the table of all the current records:
    $user->roles()->detach([1, 2, 3, 5]);

    $message = 'User and roles relationship detached for:<br/>';
    $message .= 'User: ' . $user->name . ' (id:'. $user->id . ')<br/>';

    return $message;
    /*
    User and role relationship added for:
    User: Deondre Thompson IV (id:1)
    Role: delete_user (id:1)
    */
});
  • Instead of using attach, use sync:
Route::get('/users', function () {
    $user = \App\User::first();

    // sync the relationship
    $user->roles()->sync([1, 3, 5]);

    $message = 'User and roles relationship synced for:<br/>';
    $message .= 'User: ' . $user->name . ' (id:'. $user->id . ')<br/>';

    return $message;
    /*
    User and role relationship synced for:
    User: Deondre Thompson IV (id:1)
    */
});

The table is now in sync (time stamps removed for readability)

id role_id user_id
7 1 1
8 3 1
9 5 1

Test the above with roles 2 and 4

// ...
$user->roles()->sync([2, 4]);
// ...
id role_id user_id
10 2 1
11 4 1

Next test another method, syncWithoutDetach

// ...
$user->roles()->syncWithoutDetaching(3);
// ...
id role_id user_id
10 2 1
11 4 1
15 3 1

To flip the way the relationship is created flip to the role model and add users to a role.

// role with id of 4
$role = \App\Role::find(4);

// sync with user with id 2, 5 , 10 and 1 (1 already has a relationship)
$role->users()->syncWithoutDetaching([2, 5, 10, 1]);

$message = 'Role 4 and user 2, 5 & 10 relationship synced';
// Role 4 and user 2, 5 & 10 relationship synced
id role_id user_id
13 2 1
14 4 1
15 3 1
16 4 2
17 4 5
18 4 10

Note: Role 4 is added to user 2, 5 and 10, user 1 already has the role.

48. 48 6:03 Laravel 5.8 Tutorial From Scratch - e48 - Eloquent Relationships Many To Many Part 2 (BelongsToMany)

This lesson is talking about attaching data to the pivot table, this is useful because sometimes there relational data with two things are in sync with one another.

Who granted the permission for someone to do something in the app.

Open the database\migrations\2019_04_19_103917_create_role_user_table.php

public function up()
{
    Schema::create('role_user', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->unsignedBigInteger('role_id');
        $table->unsignedBigInteger('user_id');
        $table->string('name'); // Add for the person's name (who added the role)
        $table->timestamps();
    });
}

Re-create a new database using migrate fresh (use --seed if required).

php artisan migrate:fresh --seed

If the seeder hasn't been setup a user can be created using the factory:

php artisan tinker
factory(\App\User::class)->create();

A role will also need to be created, either manually, in the database or by using a seeder (see lesson above).

In the web.php router:

Route::get('/users', function () {
    // Get the first user:
    $user = \App\User::first();

    // Sync the role(s) and add the data for the name
    $user->roles()->sync([
        1 => [
            'name' => 'victor',
        ],
    ]);
    $message = 'Victor linked role 1 for user 1';

    return $message;
});
id role_id user_id name
1 1 1 victor

Created_at & updated_at removed for ease of reading.

To update the relationship between the User and Role models and extra ->withPivot('name') will need to be inserted as follows:

  • User.php model:
public function roles()
{
    return $this->belongsToMany(\App\Role::class)->withPivot(['name'])->withTimestamps();
}
  • Role.php model:
public function users()
{
    return $this->belongsToMany(User::class)
     ->withPivot(['name'])
     ->withTimestamps();
}

Back in the router web.php, the names of each of the roles can be retrieved as follows:

Route::get('/users', function () {
    // Get the first user:
    $user = \App\User::first();

    $message = 'Victor linked role 1 for user 1<br>';
    // remember this is a collection with an array of roles!
    $message .= 'Role 1 is ' . $user->roles->first()->name . '<br>';
    $message .= 'User 1 is ' . $user->name . '<br>';
    $message .= 'Added by ' . $user->roles->first()->pivot->name . '<br>';

    return $message;
}

Homework:

To reinforce this idea I created the following:

Route::get('/users', function () {
    // Get the first user:
    $user = \App\User::first();

    // sync clears any previous roles and syncs only these roles:
    $user->roles()->sync([
        2 => [
            'name' => 'Fred',
        ],
        3 => [
            'name' => 'Fred',
        ],
        5 => [
            'name' => 'Fred',
        ],
    ]);

    // attach adds to the existing roles (but can also duplicate a record):
    $user->roles()->attach([1 => ['name' => 'Harry']]);

    // Message to output:
    $message = 'Fred linked role 2,3&4 for user 1<br>';
    $message .= 'Harry added role 1 for user 1<br>';
    $message .= 'User ' . $user->id . ' is ' . $user->name . '<br>';
    $message .= 'Has the following roles:<br>';
    foreach ($user->roles as $role) {
        $message .= 'Role '. $role->id. ' is ' . $role->name. '<br>';
        $message .= 'Added by ' . $role->pivot->name . '<br>';
    }

    return $message;

});

Actual output (refreshing the database will give a different seed for the user name):

Fred linked role 2,3&4 for user 1
Harry added role 1 for user 1
User 1 is Ms. Liza Daniel III
Has the following roles:
Role 2 is add_user
Added by Fred
Role 3 is modify_user
Added by Fred
Role 5 is edit_comments
Added by Fred
Role 1 is delete_user
Added by Harry

One other point, if the user has no roles then.

49. Laravel 5.8 Tutorial From Scratch - e49 - Testing 101 Using PHPUnit

My setup for VS Code info: to setup VS Code, with PHP Code Sniffer, to ignore comment blocks and camel case, create a file in the test folder called tests\phpcs.ruleset.xml

<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="Laravel no comments Standard">
  <description>The coding standard for Laravel with no comments</description>
  <rule ref="PSR2">
    <!-- PSR-2 but Doc Comment stuff removed -->
    <!-- Include rules related to Doc Comment I don't want -->
    <exclude name="PSR1.Methods.CamelCapsMethodName.NotCamelCaps"/>
    <exclude ref="Generic.Commenting.DocComment.ShortNotCapital" />
    <exclude ref="Generic.Commenting.DocComment.SpacingBeforeTags" />
    <exclude ref="Generic.Commenting.DocComment.TagValueIndent" />
    <exclude ref="Generic.Commenting.DocComment.NonParamGroup" />
    <exclude ref="PEAR.Commenting.FileComment.Missing" />
    <exclude ref="PEAR.Commenting.FileComment.MissingPackageTag" />
    <exclude ref="PEAR.Commenting.FileComment.PackageTagOrder" />
    <exclude ref="PEAR.Commenting.FileComment.MissingAuthorTag" />
    <exclude ref="PEAR.Commenting.FileComment.InvalidAuthors" />
    <exclude ref="PEAR.Commenting.FileComment.AuthorTagOrder" />
    <exclude ref="PEAR.Commenting.FileComment.MissingLicenseTag" />
    <exclude ref="PEAR.Commenting.FileComment.IncompleteLicense" />
    <exclude ref="PEAR.Commenting.FileComment.LicenseTagOrder" />
    <exclude ref="PEAR.Commenting.FileComment.MissingLinkTag" />
    <exclude ref="PEAR.Commenting.ClassComment.Missing" />
    <exclude ref="PEAR.Commenting.FunctionComment.Missing" />
    <exclude ref="PEAR.Commenting.FunctionComment.Missing" />
    <exclude ref="PEAR.Commenting.FunctionComment.MissingParamTag" />
    <exclude ref="PEAR.Commenting.FunctionComment.MissingParamName" />
    <exclude ref="PEAR.Commenting.FunctionComment.MissingParamComment" />
    <exclude ref="PEAR.Commenting.FunctionComment.MissingReturn" />
    <exclude ref="PEAR.Commenting.FunctionComment.SpacingAfter" />
  </rule>
</ruleset>

This lesson will cover the basics of testing the application.

Explanation of unit and feature testing, benefits of automated testing.

Note: Telescope need to be disabled before tests are run.

open PHPUnit.xml

  • in the php section add TELESCOPE_ENABLED and set to false
<php>
    <server name="APP_ENV" value="testing"/>
    <server name="BCRYPT_ROUNDS" value="4"/>
    <server name="CACHE_DRIVER" value="array"/>
    <server name="MAIL_DRIVER" value="array"/>
    <server name="QUEUE_CONNECTION" value="sync"/>
    <server name="SESSION_DRIVER" value="array"/>
    <server name="TELESCOPE_ENABLED" value="false"/> <!--Add this line -->
</php>

If you run phpunit without disabling Telescope this error will display:

2) Tests\Feature\ExampleTest::testBasicTest
ReflectionException: Class env does not exist
...
C:\laragon\www\YouTube\Laravel-5-8-Tutorial-From-Scratch\my-first-project\app\Providers\TelescopeServiceProvider.php:24
...

If this doesn't fix the problem clear teh cache:

php artisan clear
php artisan config:clear

The tests should all pass:

λ vendor\bin\phpunit.bat
PHPUnit 7.5.8 by Sebastian Bergmann and contributors.

...                                                                 3 / 3 (100%)

Time: 2.14 seconds, Memory: 18.00 MB

OK (3 tests, 3 assertions)

Inside the tests folder there are two folders

  • feature
  • unit

Tip: test names should be descriptive of what the test does, e.g. a_new_user_gets_an_email_when_it_registers

Normally tests are written along side the implementation, in this lesson tests will be written for an existing Customers controller, this is called back filling the test.

  • rename the exampleTest to CustomersTest.php
  • rename the class CustomersTest
  • remove the existing test
  • The first test will simulate when an anonymous user clicks Customer List they will be redirected to the login page
  • create a new test, only_logged_in_users_can_see_the_customers_list
<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;

class CustomersTest extends TestCase
{
    /**
     * @test
     */
    public function only_logged_in_users_can_see_the_customers_list(): void
    {
        $response = $this->get('/customers')
            ->assertRedirect('/login');
    }
}

Run the test and it passes:

vendor\bin\phpunit.bat --filter CustomersTest
PHPUnit 7.5.8 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 1.23 seconds, Memory: 18.00 MB

OK (1 test, 2 assertions)

To test what would have happened if this wasn't configure properly open the CustomersController.php

  • Comment out the line // $this->middleware('auth');, in the __construct method.
public function __construct()
{
    // $this->middleware('auth');
}

Run the test and it fails:

vendor\bin\phpunit.bat --filter CustomersTest
PHPUnit 7.5.8 by Sebastian Bergmann and contributors.

F                                                                   1 / 1 (100%)

Time: 5.03 seconds, Memory: 20.00 MB

There was 1 failure:

1) Tests\Feature\CustomersTest::only_logged_in_users_can_see_the_customers_list
Response status code [200] is not a redirect status code.
Failed asserting that false is true.

C:\laragon\www\YouTube\Laravel-5-8-Tutorial-From-Scratch\my-first-project\vendor\laravel\framework\src\Illuminate\Foundation\Testing\TestResponse.php:148
C:\laragon\www\YouTube\Laravel-5-8-Tutorial-From-Scratch\my-first-project\tests\Feature\CustomersTest.php:14

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

The expected assertion failed, as it was actually a success status code of 200, instead of a redirect status code.

vendor\bin\phpunit.bat --filter CustomersTest
PHPUnit 7.5.9 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 235 ms, Memory: 16.00 MB

OK (1 test, 2 assertions)

Next test in inverse

  • write a new test authenticated_user_can_see_the_customers_list
  • This test will use a helper method actingAs.
  • To run quick tests setup the app to use a sqlite in memory database.

Edit the phpunit.xml file and insert two lines

  • <server name="DB_CONNECTION" value="sqlite"/>
  • <server name="DB_DATABASE" value=":memory:"/>
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="vendor/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>

        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./app</directory>
        </whitelist>
    </filter>
    <php>
        <server name="APP_ENV" value="testing"/>
        <server name="DB_CONNECTION" value="sqlite"/> <!-- Add this line -->
        <server name="DB_DATABASE" value=":memory:"/> <!-- Add this line -->
        <server name="BCRYPT_ROUNDS" value="4"/>
        <server name="CACHE_DRIVER" value="array"/>
        <server name="MAIL_DRIVER" value="array"/>
        <server name="QUEUE_CONNECTION" value="sync"/>
        <server name="SESSION_DRIVER" value="array"/>
        <server name="TELESCOPE_ENABLED" value="false"/>
    </php>
</phpunit>

Back in the test

  • Model factory can be used to create a new user
  • Copy the code from the previous test and modify it to assertOK
  • Import the Users class.
/** @test */
public function authenticated_users_can_see_customers_list(): void
{
    $this->actingAs(factory(User::class)->create());

    $response = $this->get('/customers')
        ->assertOK();
}

Run the test and there is an error about no users table.

General error: 1 no such table: users
  • There is a fresh in memory database ready for tests, but no migrations.
  • User the use RefreshDatabase; trait
class CustomersTest extends TestCase
{
    use RefreshDatabase;
// ...

Run the tests and they pass:

PHPUnit 7.5.9 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 333 ms, Memory: 22.00 MB

OK (2 tests, 3 assertions)

Next test is to confirm a customer can be added.

  • Check php artisan route:list
  • There is a POST route for customers on the store method:
 POST      | customers   | customers.store   | App\Http\Controllers\CustomersController@store
  • create a new test a_customer_can_be_added_through_the_form
  • The user needs to be authenticated, so copy $this->actingAs(factory(User::class)->create()); from the previous test.
  • This time the endpoint is a post request to customers, which needs to pass in the data as an array.
  • Checking the validateRequest method in the CustomersController we need:
// ...
'name' => 'required|min:3',
'email' => 'required|email',
'active' => 'required',
'company_id' => 'required',
// ...
  • Once the customer has been created there should be 1 customer in the Customer table.
  • Double check the Customer class has been imported (otherwise a Customer not found error will display)
/** @test */
public function a_customer_can_be_added_through_the_form(): void
{
    $this->actingAs(factory(User::class)->create());

    $response = $this->post('/customers', [
        'name' => 'Test User',
        'email' => 'test@test.local',
        'active' => 1,
        'company_id' => 1,
    ])->assertStatus(302);

    $this->assertCount(1, Customer::all());
}

Run the test:

  • 1 failure: Failed asserting that actual size 0 matches expected size 1.
  • The error doesn't tell us why, this can be fixed with a helper method withoutExceptionHandling()
public function a_customer_can_be_added_through_the_form(): void
    {
        $this->withoutExceptionHandling();;
        // ...

Re-run the test and we have the full error:

  • failure:
Illuminate\Auth\Access\AuthorizationException: This action is unauthorized.
  • A new user is being created.
  • In CustomersPolicy.php only the user with email admin@admin.test is allowed.
  • The fist line of the store method in the CustomersController.php is to authorize the request.
  • Amend the test, in the create call override the email of the user created by the factory, change it to admin@admin.test
// ...
$this->actingAs(factory(User::class)->create([
    'email' => 'admin@admin.test',
]));
// ...

Rerun the test and it passes:

vendor\bin\phpunit.bat --filter a_customer_can_be_added_through_the_form
PHPUnit 7.5.9 by Sebastian Bergmann and contributors.

"Register to newsletter"
"Slack message to Admin"
.                                                                   1 / 1 (100%)

Time: 30.98 seconds, Memory: 26.00 MB

OK (1 test, 2 assertions)

Notice there are two messages "Register to newsletter" & "Slack message to Admin", this is due to the events being triggered when a new customer is registered.

  • To turn off these events an Event handling can override the call using fake.
  • Use the Event::fake(); class and method
  • Double check the use Illuminate\Support\Facades\Event; is imported
vendor\bin\phpunit.bat --filter a_customer_can_be_added_through_the_form
PHPUnit 7.5.9 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 311 ms, Memory: 22.00 MB

OK (1 test, 2 assertions)

Notice how there are no notices and it ran in 0.3s.

Run all tests by filtering on the class:

vendor\bin\phpunit.bat --filter CustomersTest
PHPUnit 7.5.9 by Sebastian Bergmann and contributors.

...                                                                 3 / 3 (100%)

Time: 390 ms, Memory: 24.00 MB

OK (3 tests, 5 assertions)

Each of the validateRequest items can be tested, to help this the test can be refactored to take the test user account into its own private method.

The validateRequest:

return request()->validate([
    'name' => 'required|min:3',
    'email' => 'required|email',
    'active' => 'required',
    'company_id' => 'required',
    'image' => 'sometimes|file|image|max:5000',
]);
// Was:
$response = $this->post('/customers', [
    'name' => 'Test User',
    'email' => 'test@test.local',
    'active' => 1,
    'company_id' => 1,
])->assertStatus(302);

// Now:
$response = $this->post('/customers', $this->data())
    ->assertStatus(302);
// ...
private function data()
{
    return [
        'name' => 'Test User',
        'email' => 'test@test.local',
        'active' => 1,
        'company_id' => 1,
    ];
}

Run the test and it passes.

The next test is to confirm a name is required.

  • Start off with a new test a_customer_name_is_required
  • Copy the previous test in, as allot of the code is the same
  • use array_merge function to override the user name.
/** @test */
public function a_customer_name_is_required(): void
{
    // $this->withoutExceptionHandling();
    Event::fake();

    $this->actingAs(factory(User::class)->create([
        'email' => 'admin@admin.test',
    ]));

    $response = $this->post('/customers', array_merge($this->data(), ['name' => '']))
        ->assertStatus(302);

    $response->assertSessionHasErrors(['name']);

    $this->assertCount(1, Customer::all());
}

Run the test: Failed asserting that actual size 0 matches expected size 1.

Amend the test as it should test the assertCount is 0, the data hasn't been saved to the database.

// ...
$this->assertCount(0, Customer::all());
vendor\bin\phpunit.bat --filter a_customer_name_is_required
PHPUnit 7.5.9 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 320 ms, Memory: 22.00 MB

OK (1 test, 4 assertions)

The next test is to confirm the name is at least 3 characters.

  • Copy the previous test
  • Rename the new test a_customer_name_must_be_at_lest_3_characters
  • Override the name with a
/** @test */
public function a_customer_name_must_be_at_lest_3_characters(): void
{
    // $this->withoutExceptionHandling();
    Event::fake();

    $this->actingAs(factory(User::class)->create([
        'email' => 'admin@admin.test',
    ]));

    $response = $this->post(
        '/customers',
        array_merge($this->data(), ['name' => 'a'])
    )->assertStatus(302);

    $response->assertSessionHasErrors(['name']);

    $this->assertCount(0, Customer::all());
}

Run the test and it passes.

vendor\bin\phpunit.bat --filter a_customer_name_must_be_at_lest_3_characters
PHPUnit 7.5.9 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 326 ms, Memory: 22.00 MB

OK (1 test, 4 assertions)

Refactor the test

  • Most are starting Event::fake();, this can go in a setUp method, which is run before each test.
  • Extract the actingAs(...) code to its own private method.
  • Update the methods to call the new method called actingAsAdmin.
protected function setUp(): void
{
    parent::setUp();

    Event::fake();
}
// ...
// replace all actingAs... with a call to the method
$this->actingAsAdmin();
// ...
protected function actingAsAdmin()
{
    $this->actingAs(factory(User::class)->create([
        'email' => 'admin@admin.test',
    ]));
}

Re-run the tests and they pass.

vendor\bin\phpunit.bat --filter CustomersTest
PHPUnit 7.5.9 by Sebastian Bergmann and contributors.

.....                                                               5 / 5 (100%)

Time: 470 ms, Memory: 26.00 MB

OK (5 tests, 13 assertions)

Next test a_customer_email_is_required

  • Acting as an admin
  • A blank email field will be sent
  • Assert an error for email is generated
  • The database should not have any records
/** @test */
public function a_customer_email_is_required(): void
{
    $this->actingAsAdmin();

    $response = $this->post(
        '/customers',
        array_merge($this->data(), ['email' => ''])
    )->assertStatus(302);

    $response->assertSessionHasErrors(['email']);

    $this->assertCount(0, Customer::all());
}
vendor\bin\phpunit.bat --filter a_customer_email_is_required
PHPUnit 7.5.9 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 323 ms, Memory: 22.00 MB

OK (1 test, 4 assertions)

Next test for a valid email

  • copy the previous test and call the copy a_customer_email_must_be_valid
  • Enter an invalid email address for email
/** @test */
public function a_customer_email_is_valid(): void
{
    $this->actingAsAdmin();

    $response = $this->post(
        '/customers',
        array_merge($this->data(), ['email' => 'testtesttest'])
    )->assertStatus(302);

    $response->assertSessionHasErrors(['email']);

    $this->assertCount(0, Customer::all());
}

Run the test and it passes:

vendor\bin\phpunit.bat --filter a_customer_email_must_be_valid
PHPUnit 7.5.9 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 334 ms, Memory: 22.00 MB

OK (1 test, 4 assertions)

Testing will be continued in a new course.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment