-
- 29 8:57 Laravel 5.8 Tutorial From Scratch - e29 - Queues: Database Driver
-
- 30 5:40 Laravel 5.8 Tutorial From Scratch - e30 - queue:work In The Background
-
- 31 16:11 Laravel 5.8 Tutorial From Scratch - e31 - Deployment: Basic Server Setup - SSH, UFW, Nginx - Part 1
-
- 32 21:52 Laravel 5.8 Tutorial From Scratch - e32 - Deployment: Basic Server Setup - MySQL, PHP 7 - Part 2
-
- 33 12:52 Laravel 5.8 Tutorial From Scratch - e33 - Deployment: Basic Server Setup - SSL, HTTPS - Part 3
-
- 40 11:24 Laravel 5.8 Tutorial From Scratch - e40 - Image Upload: Cropping & Resizing - Part 2
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
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
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.
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
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');
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)
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.
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.
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.
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.
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.
Have a go at adding an email field to the input field, database and controller.
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.
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
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
- Add
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')
);
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();
}
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('[email protected]')->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
[email protected]
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.
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.
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">
// ..
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:
- 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
) npm install && npm run dev
- Configure your favourite database (mysql, sqlite etc.)
php artisan migrate
to create basic user tables.php artisan serve
(or equivalent) to run server and test preset.
There is also a scaffolding to create your own preset.
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));
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,
],
];
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.
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.
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.
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.
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
orphp 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 |
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.
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
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: "[email protected]",
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();
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_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
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:
- http://127.0.0.1:8000/public/storage/uploads/v48jR6eWt1wgDrZKHDVPDQ0TCihmdJCaLf4Hq1Qz.jpeg
- Note: Files in the storage directory are not part of source control.
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
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.
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:
- View the 50 customer have been created by going to http://127.0.0.1:8000/customers
- View the Queries, there is a query for every customer.
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" => "[email protected]"
"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" => "[email protected]"
"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.
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.
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, [
'[email protected]',
]);
}
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, [
'[email protected]',
]);
}
-
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 = ['[email protected]',];
// 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: "[email protected]",
# 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.
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' => '[email protected]',
'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 [email protected] 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 [email protected]
// ...
$this->actingAs(factory(User::class)->create([
'email' => '[email protected]',
]));
// ...
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' => '[email protected]',
'active' => 1,
'company_id' => 1,
])->assertStatus(302);
// Now:
$response = $this->post('/customers', $this->data())
->assertStatus(302);
// ...
private function data()
{
return [
'name' => 'Test User',
'email' => '[email protected]',
'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' => '[email protected]',
]));
$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' => '[email protected]',
]));
$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' => '[email protected]',
]));
}
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.