Laravel, Lumen
Intro
- Lumen was chosen as a compact alternative to Laravel, and this section presents copies of Laravel documentation. Only some specific cases, like installation and configuration are related to Lumen, in rest, all is fully compatible with Laravel and can be used together
composer global require "laravel/lumen-installer"
PATH=$PATH:~/.composer/vendor/bin
OR PATH=~/.composer/vendor/bin:$PATH
lumen new project-name
- OR via Composer
composer create-project --prefer-dist laravel/lumen project-name
- serve /public folder with
php -S localhost:8000 -t public
or server
composer install
composer require vlucas/phpdotenv
- to work with env files
Configuration
- .env file - variables listed in this file will be loaded into the $_ENV PHP super-global when application receives a request, env function may be used to retrieve the values, file should not be committed to source control, each developer/server using application could require a different environment configuration
- rename the .env.example file to .env and set application key (APP_KEY) to a random string:
php artisan key:generate
, in Lumen, generate with str_random(32)
- load configuration file within bootstrap/app.php file: $app->configure('app');
- copy/create app/vendor/laravel/lumen-framework/config/*.phpin app/config for separate configs
php artisan config:cache
- cache all of your configuration files into a single file, run the command as part of production deployment process, be sure that you only call the env function from within your configuration files, once cached, the .env file will not be loaded; therefore, env function will only return external, system level environment variables
// configuration values may be accessed using "dot" syntax
// includes the name of the file and option you wish to access
$value = config('app.locale');
// to set configuration values at runtime, pass an array to the config helper
config(['app.locale' => 'en']);
// retrieve the values of environment variables
$debug = env('APP_DEBUG', true); // true is default value
// current application environment,
// determined in .env file via APP_ENV variable
$environment = app()->environment();
// environment is local
if (app()->environment('local')) { /* ... */ }
// environment is either local OR staging
if (app()->environment('local', 'staging')) { /* ... */ }
Routing
- routes/web.php
- route parameters cannot contain the "-" character, use an "_" instead
- Route::fallback(function () { }); - route that will be executed when no other route matches the incoming request
- Rate Limiting - restrict the amount of traffic for a given route or group of routes
php artisan route:cache
- generate route cache, during project deployment, php artisan route:clear
- clear cache
$router->get($uri, $callback);
$router->post($uri, $callback);
$router->put($uri, $callback);
$router->patch($uri, $callback);
$router->delete($uri, $callback);
$router->options($uri, $callback);
// most basic Lumen routes simply accept a URI and a Closure
$router->get('foo', function () {
return 'Hello World';
});
$router->post('foo', function () {
//
});
// --- REQUIRED PARAMETERS
$router->get(
'user/{id}',
function ($id) { return 'User '.$id; }
);
$router->get(
'posts/{postId}/comments/{commentId}',
function ($postId, $commentId) { /* ... */ }
);
// --- OPTIONAL PARAMETERS
// enclosing part of the route URI definition in [...]
// /foo[bar] will match both /foo and /foobar
// only supported in a trailing position of the URI
// dont place an optional parameter in the middle of a route definition:
$router->get('user[/{name}]', function ($name = null) {
return $name;
});
// --- RegEx Constraints
$router->get(
'user/{name:[A-Za-z]+}',
function ($name) { /* ... */ }
);
// --- NAMED ROUTES
// convenient generation of URLs or redirects for specific routes
// specify a name for a route using the as array key when defining the route
$router->get(
'profile', [
'as' => 'profile',
function () { /* ... */ }
]);
// route names for controller actions
$router->get('profile', [
'as' => 'profile',
'uses' => 'UserController@showProfile'
]);
// generating URLs/redirects to named routes
$url = route('profile');
return redirect()->route('profile')
// with parameters
$router->get('user/{id}/profile', [
'as' => 'profile',
function ($id) { /* ... */ }
]);
// ...
$url = route('profile', ['id' => 1]);
// --- --- ROUTE GROUPS
// share route attributes: middleware, namespaces, across number of routes
// without needing to define those attributes on each individual route
// --- middleware
$router->group(
['middleware' => 'auth'],
function () use ($router) {
$router->get('/', function () {
// Uses Auth Middleware
});
$router->get('user/profile', function () {
// Uses Auth Middleware
});
});
// --- NAMESPACES
$router->group(
['namespace' => 'Admin'],
function() use ($router) {
// Using The "App\Http\Controllers\Admin" Namespace...
$router->group(['namespace' => 'User'], function() use ($router) {
// Using The "App\Http\Controllers\Admin\User" Namespace...
});
});
// --- ROUTE PREFIXES
$router->group(
['prefix' => 'admin'],
function () use ($router) {
$router->get('users', function () {
// Matches The "/admin/users" URL
});
});
// specify common parameters for grouped routes
$router->group(
['prefix' => 'accounts/{accountId}'],
function () use ($router) {
$router->get('detail', function ($accountId) {
// Matches The "/accounts/{accountId}/detail" URL
});
});
// ...
Route::prefix('admin')->group(function () {
Route::get('users', function () {
// matches The "/admin/users" URL
});
});
// route name prefixes - prefix each route name in the group with a given string
Route::name('admin.')->group(function () {
Route::get('users', function () {
// route assigned name "admin.users"...
})->name('users');
});
// --- ROUTE MODEL BINDING
// instead of injecting a ID, inject the entire model instance that matches
// Laravel automatically resolves Eloquent models defined in routes or controller actions
// whose type-hinted variable names match a route segment name
// otherwise 404 HTTP response generated
// --- IMPLICIT BINDING
Route::get('api/users/{user}', function (App\User $user) {
return $user->email;
});
// specify the column in the route parameter definition
Route::get('api/posts/{post:slug}', function (App\Post $post) {
return $post;
});
// getRouteKeyName, in the Eloquent model
// use a database column other than id when retrieving a given model class
// ...
public function getRouteKeyName() {
return 'slug';
}
// scope the second Eloquent model such that it must be a child of the previous
// retrieves a blog post by slug for a specific user:
// retrieve nested model by its parent using conventions to guess the relationship
// User model has a relationship named posts (the plural form of the route parameter name) used to retrieve
Route::get('/users/{user}/posts/{post:slug}', function (User $user, Post $post) {
return $post;
});
// resolveChildRouteBinding method will be used to resolve the child binding of the parent model:
// ...
// retrieves the child model for a bound value
public function resolveChildRouteBinding($childType, $value, $field) {
return parent::resolveChildRouteBinding($childType, $value, $field);
}
// missing model behavior
Route::get('/locations/{location:slug}', [LocationsController::class, 'show'])
->name('locations.view')
->missing(function (Request $request) {
return Redirect::route('locations.index');
});
// --- EXPLICIT BINDING
// define in boot method of the RouteServiceProvider class
// ...
public function boot() {
parent::boot();
Route::model('user', App\User::class);
}
// ...
// next, define a route that contains a {user} parameter
Route::get(
'profile/{user}',
function (App\User $user) {
// ...
}
);
// we have bound all {user} parameters to the App\User model,
// a User instance will be injected into the route
// request to profile/1 will inject the User instance from the database with ID of 1
// --- Route::bind - own resolution logic
// ...
public function boot() {
parent::boot();
Route::bind('user', function ($value) { // value of the URI segment
// return instance of the class that should be injected into the route
return App\User::where('name', $value)->firstOrFail();
}
);
}
// ...
// alternatively, override the resolveRouteBinding method on Eloquent model
// retrieve the model for a bound value
public function resolveRouteBinding$value, $field) { // value of the URI segment
// return instance of the class that should be injected into the route
return $this->where('name', $value)->firstOrFail();
}
// --- information about the route handling the incoming request
$route = Route::current();
$name = Route::currentRouteName();
$action = Route::currentRouteAction();
Middleware (CORS)
- stored in the app/Http/Middleware directory
- filtering HTTP requests entering application
- to create a new middleware, copy the app/Http/Middleware/ExampleMiddleware
- CORS middleware might be responsible for adding the proper headers to all responses leaving application, logging middleware might log all incoming requests to application
- Laravel Sanctum - authentication system for SPA, mobile applications, and simple, token based APIs, generate multiple API tokens for account, grant abilities/scopes which specify which actions the tokens are allowed to perform
- Since Laravel 7.0 CORS support is integrated by default:
composer require fruitcake/laravel-cors
- add the \Fruitcake\Cors\HandleCors::class middleware to your App\Http\Kernel global middleware list
Definition
namespace App\Http\Middleware;
use Closure;
class HowOldMiddleware {
public function handle($request, Closure $next) {
// if the given age is less than or equal to 200
if ($request->input('age') <= 200) {
// return an HTTP redirect to the client
return redirect('home');
}
// otherwise, the request will be passed further into the application
return $next($request);
}
}
// ...
public function handle($request, Closure $next)
{
// ... perform action before the request is handled
$response = $next($request);
// ... perform action after the request is handled
return $response;
}
// ...
// --- receive additional custom parameters, passed after $next
// ...
public function handle($request, Closure $next, $role) {
if (! $request->user()->hasRole($role)) {
// Redirect...
}
return $next($request);
}
// ...
// OR specified when defining the route by separating the middleware name
// and parameters with a : , multiple parameters should be delimited by commas:
$router->put(
'post/{id}',
['middleware' => 'role:editor', function ($id) { /* ... */ }
]);
Route::put('post/{id}', function ($id) {
// ...
})->middleware('role:editor');
// --- terminable Middleware
// do some work after the HTTP response has already been sent to the browser.
// add it to the list of global middleware in bootstrap/app.php
namespace Illuminate\Session\Middleware;
use Closure;
class StartSession {
public function handle($request, Closure $next) {
return $next($request);
}
public function terminate($request, $response) {
// Store the session data...
}
}
// when calling the terminate method on middleware,
// Lumen will resolve a fresh instance of the middleware from the service container
// if you want to use the same middleware instance
// when the "handle" and "terminate" methods are called,
// register the middleware with the container using the container "singleton" method
Usage
// --- global middleware
// run during every HTTP request to application,
// list in call to the $app->middleware() method in bootstrap/app.php
$app->middleware([
App\Http\Middleware\HowOldMiddleware::class
]);
// OR, in Laravel, list the middleware class
// in the $middleware property of your app/Http/Kernel.php class
// --- assigning middleware to routes
// first assign the middleware a short-hand key in bootstrap/app.php
$app->routeMiddleware([
// protected $routeMiddleware = [ // within App\Http\Kernel class
'auth' => App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
]);
// use the middleware key in the route options array:
$router->get( 'admin/profile', [
'middleware' => 'auth',
function () { /* ... */ }
]);
// use an array to assign multiple middleware to the route:
$router->get( '/', [
'middleware' => ['first', 'second'],
function () { /* ... */ }
]);
// in Laravel
Route::get('admin/profile', function () {
// ...
})->middleware('auth');
Route::get('/', function () {
// ...
})->middleware('first', 'second');
// pass the fully qualified class name
use App\Http\Middleware\CheckAge;
Route::get('admin/profile', function () {
// ...
})->middleware(CheckAge::class);
// --- middleware groups
protected $middlewareGroups = [
// out of the box, the web middleware group is automatically applied
// to routes/web.php file by the RouteServiceProvider
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:60,1',
'auth:api',
],
];
// ...
Route::get('/', function () {
// ...
})->middleware('web');
Route::group(['middleware' => ['web']], function () {
// ...
});
// --- sorting middleware - execute in a specific order
protected $middlewarePriority = [
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\Authenticate::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Illuminate\Auth\Middleware\Authorize::class,
];
CORS (5.8-6.0)
// --- app/app/Http/Middleware/CorsMiddleware.php
namespace App\Http\Middleware;
use Closure;
class CorsMiddleware {
public function handle($request, Closure $next) {
$headers = [
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Methods' => 'POST, GET, OPTIONS, PUT, DELETE',
'Access-Control-Allow-Credentials' => 'true',
'Access-Control-Max-Age' => '86400',
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, X-Requested-With'
];
if ($request->isMethod('OPTIONS')) {
return response()->json('{"method":"OPTIONS"}', 200, $headers);
}
$response = $next($request);
foreach($headers as $key => $value) {
$response->header($key, $value);
}
return $response;
}
}
// --- app/bootstrap/app.php
$app->middleware([
App\Http\Middleware\CorsMiddleware::class
]);
Controller
- stored in the app/Http/Controllers directory, "root" namespace, no needto specify the full controller namespace when defining the controller route
- can group related HTTP request handling logic into a class
- should extend the base controller class included with the default Lumen installation
- service container is used to resolve all Lumen controllers
- type-hint any dependencies controller may need in its constructor, dependencies will automatically be resolved and injected into the controller instance
- also type-hint dependencies on controller action methods
php artisan make:controller PhotoController --resource
- generate resource controllers, will contain a method for each of the available resource operations
php artisan make:controller PhotoController --resource --model=Photo
- specifying the resource model
php artisan make:controller API/PhotoController --api
- generate an API resource controller that does not include the create or edit methods
namespace App\Http\Controllers;
use App\User;
class UserController extends Controller {
// retrieve the user for the given ID
public function show($id) {
return User::findOrFail($id);
}
}
// ... its route
$router->get(
'user/{id}',
'UserController@show'
);
// --- nest or organize controllers deeper
// using PHP namespaces into the App\Http\Controllers directory
// use the specific class name relative to the App\Http\Controllers root namespace
$router->get(
'foo',
// App\Http\Controllers\Photos\AdminController
'Photos\AdminController@method'
);
// --- naming controller routes
$router->get(
'foo', [
'uses' => 'FooController@method',
'as' => 'named'
]);
// generate URL to named controller route
$url = route('named');
// --- RESOURCE ROUTES
// register a resourceful route to the controller
// add route to new method separately, before resource registration
Route::get('photos/bar', 'PhotoController@bar');
Route::resource(
'photos',
'PhotoController'
);
// register many resource controllers at once
Route::resources([
'photos' => 'PhotoController',
'posts' => 'PostController'
]);
// specify a subset of actions the controller should handle
// instead of the full set of default actions
Route::resource(
'photos',
'PhotoController'
)->only([ 'index', 'show' ]);
Route::resource(
'photos',
'PhotoController'
)->except([ 'create', 'store', 'update', 'destroy' ]);
// for API resource routes,
// exclude routes that present HTML templates: create, edit
Route::apiResource(
'photos',
'PhotoController'
);
Route::apiResources([
'photos' => 'PhotoController',
'posts' => 'PostController'
]);
// --- naming resource routes
Route::resource('photos', 'PhotoController')->names([
'create' => 'photos.build'
]);
// naming resource route parameters
Route::resource('users', 'AdminUserController')->parameters([
'users' => 'admin_user' // resource names and parameter names
]);
// generates /users/{admin_user} for the resource show route
// --- localizing resource URIs in the boot method of AppServiceProvider
// ...
Route::resourceVerbs([
'create' => 'crear',
'edit' => 'editar',
]);
// ...
// Route::resource('fotos', 'PhotoController'), will produce:
// /fotos/crear
// /fotos/{foto}/editar
// --- ASSIGN MIDDLEWARE
$router->get('profile', [
'middleware' => 'auth',
'uses' => 'UserController@showProfile'
]);
// OR within controller constructor
class UserController extends Controller {
// instantiate a new UserController instance
public function __construct() {
$this->middleware('auth');
$this->middleware('log', ['only' => [
'fooAction',
'barAction',
]]);
$this->middleware('subscribed', ['except' => [
'fooAction',
'barAction',
]]);
}
}
// --- SINGLE ACTION CONTROLLERS
namespace App\Http\Controllers;
use App\User;
use App\Http\Controllers\Controller;
class ShowProfile extends Controller {
// how the profile for the given user
public function __invoke($id) {
return view('user.profile', ['user' => User::findOrFail($id)]);
}
}
// then, no need to specify a method
Route::get('user/{id}', 'ShowProfile');
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 |
Dependency Injection & Controllers
// --- constructor injection
namespace App\Http\Controllers;
use App\Repositories\UserRepository;
class UserController extends Controller {
// user repository instance
protected $users;
// create a new controller instance
// type-hint dependency
public function __construct(UserRepository $users) {
$this->users = $users;
}
}
// --- method injection
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class UserController extends Controller {
// store a new user
// type-hint the Illuminate\Http\Request instance
public function store(Request $request) {
$name = $request->input('name');
// ...
}
// update specified user
// type-hint the Illuminate\Http\Request
// and access route parameter id
// for $router->put('user/{id}', 'UserController@update')
public function update(Request $request, $id) {
// ...
}
}
Request
- type-hint the Illuminate\Http\Request class on controller constructor or method to obtain an instance of the current HTTP request via dependency injection, current request instance will automatically be injected by the service container
- Illuminate\Http\Request extends Symfony\Component\HttpFoundation\Request
- object returned by the file method is an instance of the Symfony\Component\HttpFoundation\File\UploadedFile class, which extends the PHP SplFileInfo class and provides a variety of methods for interacting with the file
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class UserController extends Controller {
public function store(Request $request) {
$name = $request->input('name'); // ...
}
// also expecting input from a route parameter
// $router->put('user/{id}', 'UserController@update')
// Route::put('user/{id}', 'UserController@update');
public function update(Request $request, $id) {
// ...
}
}
// --- path - request URI
$uri = $request->path(); // for http://domain.com/foo/bar is foo/bar
// --- is - verify URI fo pattern match
// use the * character as a wildcard
if ($request->is('admin/*')) { /* ... */ }
// --- full URL without query string
$url = $request->url();
// --- full URL with query string
$url = $request->fullUrl();
// --- HTTP verb for the request
$method = $request->method();
// --- verify for HTTP verb
if ($request->isMethod('post')) { /* ... */ }
// --- RETRIEVING INPUT
$name = $request->input('name'); // same way for all verbs
$name = $request->input('name', 'Default Value');
// use "dot" notation to access the arrays
$name = $request->input('products.0.name');
$names = $request->input('products.*.name');
// determine value presense
if ($request->has('name')) { /* ... */ }
// determine if all of the specified values are present in array
if ($request->has(['name', 'email'])) { /* ... */ }
// if a value is present on the request and is not empty
if ($request->filled('name')) { /* ... */ }
// all input data as an array
$input = $request->all();
// input from the query string
$name = $request->query('name');
$name = $request->name; // using dynamic properties
$name = $request->query('name', 'Helen'); // with default value
$query = $request->query(); // all string values as an associative array
// dig into JSON arrays
$name = $request->input('user.name');
// retrieve a sub-set of the input data: "only" and "except"
// accept a single array or a dynamic list of arguments:
$input = $request->only(['username', 'password']);
$input = $request->only('username', 'password');
$input = $request->except(['credit_card']);
$input = $request->except('credit_card');
// --- RETRIEVING FILES
$file = $request->file('photo');
$file = $request->photo;
// if a file is present on the request using the hasFile method
if ($request->hasFile('photo')) { /* ... */ }
// verify that there were no problems uploading the file
if ($request->file('photo')->isValid()) { /* ... */ }
// fully-qualified path and its extension
$path = $request->photo->path();
// guess the file extension based on its contents
$extension = $request->photo->extension();
// move the uploaded file to a new location
$request->file('photo')->move($destinationPath);
$request->file('photo')->move($destinationPath, $fileName);
$path = $request->photo->store('images');
$path = $request->photo->store('images', 's3'); // with disk that should be used
$path = $request->photo->storeAs('images', 'filename.jpg');
$path = $request->photo->storeAs('images', 'filename.jpg', 's3');
// --- PSR-7 requests:
// composer require symfony/psr-http-message-bridge
// composer require zendframework/zend-diactoros
use Psr\Http\Message\ServerRequestInterface;
$router->get(
'/',
function (ServerRequestInterface $request) { /* ... */ }
);
// if you return a PSR-7 response instance from a route or controller,
// it will automatically be converted back to a Laravel response instance
// and be displayed by the framework
Response
- Illuminate\Http\Response inherits from the Symfony\Component\HttpFoundation\Response
- response() helper may be used to generate other types of response instances, when called without arguments, an implementation of the Laravel\Lumen\Http\ResponseFactory class is returned, provides several helpful methods for generating responses
use Illuminate\Http\Response;
$router->get('home', function () {
return (new Response($content, $status))
->header('Content-Type', $value);
// OR response helper
return response($content, $status)
->header('Content-Type', $value);
});
// --- CHAIN response methods
return response($content)
->header('Content-Type', $type)
->header('X-Header-One', 'Header Value')
->header('X-Header-Two', 'Header Value');
return response($content)
->withHeaders([
'Content-Type' => $type,
'X-Header-One' => 'Header Value',
'X-Header-Two' => 'Header Value',
]);
// --- return as JSON
return response()->json(
['name' => 'Abigail', 'state' => 'CA']
);
// provide a status code and an array of additional headers
return response()->json(
['error' => 'Unauthorized'],
401,
['X-Header-One' => 'Header Value']
);
// create a JSONP response
return response()
->json(['name' => 'Abigail', 'state' => 'CA'])
->setCallback($request->input('callback'));
// --- DOWNLOAD the file at the given path
return response()->download($pathToFile);
return response()->download(
$pathToFile,
$name_to_show,
$headers
);
return response()->download($pathToFile)->deleteFileAfterSend();
// file Response
return response()->file($pathToFile);
return response()->file($pathToFile, $headers);
// --- REDIRECT
$router->get('dashboard', function () {
return redirect('home/dashboard');
});
// redirecting to named routes
return redirect()->route('login');
return redirect()->route('profile', ['id' => 1]); // profile/{id}
// redirecting to a route with an "id" parameter
// that is being populated from an Eloquent model
// pass the model itself, "id" will be extracted automatically:
return redirect()->route('profile', [$user]);
// simple response, in route
$router->get('/', function () {
return 'Hello World';
});
Authentication
- for feature, uncomment the call to register the AuthServiceProvider service provider in bootstrap/app.php file and modify app/Providers/AuthServiceProvider.php boot method as shown
- since Lumen does not support session state, incoming requests that you wish to authenticate must be authenticated via a stateless mechanism such as API tokens
- you may retrieve the authenticated user however you wish: use an API token in the request headers or query string, a bearer token on the request, or using any other approach application requires
- if project does not use Eloquent, you may return an instance of the Illuminate\Auth\GenericUser class
- use the Auth::user() method to retrieve the current use (and uncomment the $app->withFacades() method in bootstrap/app.php), or $request->user() method on an Illuminate\Http\Request instance
// AuthServiceProvider
// ...
$this->app['auth']->viaRequest('api', function ($request) {
// return User or null...
if ($request->input('api_token')) {
return User::where('api_token', $request->input('api_token'))->first();
}
});
// ...
// return an instance of the Illuminate\Auth\GenericUser class
use Illuminate\Auth\GenericUser;
// ...
return new GenericUser(['id' => 1, 'name' => 'Taylor']);
// authenticated User
$router->get(
'/post/{id}',[
'middleware' => 'auth',
function (Request $request, $id) {
$user = Auth::user(); // or ...
$user = $request->user();
}]
);
Authorization
- Lumen does not have a $policies array on its AuthServiceProvider, call the policy method on the Gate facade from within the provider boot method
- Laravel
- gates - closure based approach to authorization, is like routes, should be used when you wish to authorize an action for a particular model or resource
- defined in App\Providers\AuthServiceProvider class using the Gate facade
- always receive a user instance as their first argument, and may optionally receive additional arguments such as a relevant Eloquent model
- policies - (better approach) organize authorization logic around a particular model or resource, is like controllers
- if application is a blog, you may have a Post model and a corresponding PostPolicy to authorize user actions such as creating or updating posts
- generate using
make:policy
artisan command, will be placed in the app/Policies directory
php artisan make:policy PostPolicy --model=Post
- generate a class with the basic "CRUD" policy methods (view, create, update, delete, restore, and forceDelete)
- all policies are resolved via the Laravel service container, allowing you to type-hint any needed dependencies in the policy constructor to have them automatically injected
register policies
- instruct Laravel which policy to utilize when authorizing actions against a given mode, AuthServiceProvider included with fresh Laravel applications contains a policies property which maps Eloquent models to their corresponding policiesl
- Laravel can auto-discover policies as long as the model and policy follow standard Laravel naming conventions
- policies must be in a Policies directory below the directory that contains the models: for example, the models may be placed in the app directory while the policies may be placed in the app/Policies directory
- policy name must match the model name and have a Policy suffix: a User model would correspond to a UserPolicy class
- register a custom callback using the Gate::guessPolicyNamesUsing method if you would like to provide own policy discovery logic, this method should be called from the boot method of application AuthServiceProvide
- any policies that are explicitly mapped in AuthServiceProvider will take precedence over any potential auto-discovered policies
- once the policy has been registered, add methods for each action it authorizes
- some policy methods only receive the currently authenticated user and not an instance of the model they authorize, common when authorizing "create" action, define the method as only expecting the authenticated user
- User model included in Laravel application includes authorizing actions: can (receives the action you wish to authorize and the relevant model) and cant
- if a policy is registered for the given model, the can method will automatically call the appropriate policy and return the boolean result, if no policy is registered for the model, method will attempt to call the Closure based Gate matching the given action name
- define a "before" method on the policy to authorize all actions within a given policy for certain users, commonly used for authorizing application administrators
- return false from the "before" method to deny all authorizations for a user you should, if null is returned, the authorization will fall through to the policy method
- "before" method of a policy class will not be called if the class doesnt contain a method with a name matching the name of the ability being checked
- by default, all gates and policies automatically return false if the incoming HTTP request was not initiated by an authenticated user, declare an "optional" type-hint or supplying a null default value for the user argument definition
// --- optional user (guest)
// registered policy "update" method
// declaring an "optional" type-hint
// or supplying a null default value for the user argument definition
space App\Policies;
use App\User;
use App\Post;
class PostPolicy {
// determine if the given post can be updated by the user
public function update(?User $user, Post $post) {
return $user->id === $post->user_id;
}
}
gates
use App\Models\User;
use App\Models\Category;
use Illuminate\Auth\Access\Response;
use Illuminate\Support\Facades\Gate;
// register any authentication / authorization services
// app/Providers/AuthServiceProvider
public function boot() {
$this->registerPolicies();
Gate::define(
'update-post',
function ($user, $post) {
return $user->id == $post->user_id;
});
// OR
Gate::define(
'update-post',
'App\Policies\PostPolicy@update'
);
}
// - allows , denies - authorize an action using gates,
// currently authenticated user is automatically passed to callback
if (Gate::allows('update-post', $post)) {
// current user can update the post
}
if (Gate::denies('update-post', $post)) {
// current user can't update the post
abort(403);
}
// if (Gate::denies(['update-post','...'], $post)) {
// - any , none - authorize multiple actions at a time using the any or none methods:
if (Gate::any(['update-post', 'delete-post'], $post)) {
// user can update or delete the post
}
if (Gate::none(['update-post', 'delete-post'], $post)) {
// user can't update or delete the post
}
// - authorize - throws an AuthorizationException (403 HTTP) if the action is not authorized
Gate::authorize('edit-settings');
Gate::authorize('update-post', $post);
// - forUser - determine if a particular user is authorized to perform an action
if (Gate::forUser($user)->allows('update-post', $post)) {
// user can update the post
}
if (Gate::forUser($user)->denies('update-post', $post)) {
// user can't update the post
}
// - supplying additional context
// allows, denies, check, any, none, authorize, can, cannot and Blade directives @can, @cannot, @canany
// can receive an array as their second argument
// they are passed as parameters to the gate closure, can be used for additional context when making authorization decisions
Gate::define('create-post', function (User $user, Category $category, $pinned) {
if (! $user->canPublishToGroup($category->group)) {
return false;
} elseif ($pinned && ! $user->canPinPosts()) {
return false;
}
return true;
});
if (Gate::check('create-post', [$category, $pinned])) {
// The user can create the post...
}
// - before - define a callback that is run before all other authorization checks
// non-null result, considered the result of the check
Gate::before(function ($user, $ability) {
if ($user->isSuperAdmin()) {
return true;
}
});
// - after - define a callback to be executed after all other authorization checks
Gate::after(function ($user, $ability, $result, $arguments) {
if ($user->isSuperAdmin()) {
return true;
}
});
// --- gates responses - return detailed response, including an error message
// ...
Gate::define('edit-settings', function (User $user) {
return $user->isAdmin
? Response::allow()
: Response::deny('You must be an administrator.');
});
// get the full authorization response returned by the gate
$response = Gate::inspect('edit-settings');
// ...
if ($response->allowed()) {
// The action is authorized...
} else {
echo $response->message();
}
policies
- controller methods are mapped to their corresponding policy method, when requests routed to the given controller method, the corresponding policy method will automatically be invoked before the controller method is executed:
Controller Method |
Policy Method |
index |
viewAny |
show |
view |
create |
create |
store |
create |
edit |
update |
update |
update |
destroy |
delete |
// php artisan make:policy PostPolicy
// php artisan make:policy PostPolicy --model=Post
namespace App\Providers;
use App\Post;
use App\Policies\PostPolicy;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider {
// policy mappings for the application
protected $policies = [
Post::class => PostPolicy::class,
];
// register any application authentication / authorization services
public function boot() {
$this->registerPolicies();
// ...
// in Lumen, where no $policies available
Gate::policy(Post::class, PostPolicy::class);
}
}
// --- before - executed before any other methods on the policy
public function before($user, $ability) {
if ($user->isSuperAdmin()) {
return true;
}
}
// own policy discovery logic
use Illuminate\Support\Facades\Gate;
// ...
Gate::guessPolicyNamesUsing(function ($modelClass) {
// return policy class name...
});
// ...
// --- authorizing "create" action
// only receive the currently authenticated user
// and not an instance of the model they authorize
// determine if the given user can create posts
public function create(User $user) {
// ...
}
// --- AUTHORIZING ACTIONS USING POLICIES
// - User model included with app has "can" and "cant" authorizing actions
// receives authorize action and relevant model
// determine if a user is authorized to update a given Post model
if ($user->can('update', $post)) {
//
}
// pass a class name to the can method
// used to determine which policy to use when authorizing the action
if ($user->can('create', Post::class)) { // use App\Post;
// executes the "create" method on the relevant policy...
}
if ($request->user()->can('update-post', $post)) {
// user is allowed to update the post...
}
if ($request->user()->cannot('update-post', $post)) {
abort(403);
}
// - included Laravel middleware
// authorize actions before the incoming request reaches routes or controllers
// by default Illuminate\Auth\Middleware\Authorize middleware
// is assigned the "can" key in App\Http\Kernel class
use App\Post;
// ...
Route::put('/post/{post}', function (Post $post) {
// current user may update the post...
})->middleware(
// name of the actionto authorize
// and route parameter to pass to the policy method
'can:update,post'
);
// actions like "create" may not require a model instance
// pass a class name, to determine which policy for action authorization
Route::post('/post', function () {
// The current user may create posts...
})->middleware('can:create,App\Post');
// - controller helpers, "authorize" method of any controllers
// which extend the App\Http\Controllers\Controller base class
// accepts the name of the action you wish to authorize and the relevant model
// throws an Illuminate\Auth\Access\AuthorizationException (403 status code)
namespace App\Http\Controllers;
use App\Post;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class PostController extends Controller {
// update the given blog post
public function update(Request $request, Post $post) {
$this->authorize('update', $post);
// current user can update the blog post...
}
// pass a class name to the authorize method in "create"
// which not requires a model instance
// create a new blog post
public function create(Request $request) {
$this->authorize('create', Post::class);
// current user can create blog posts...
}
// use authorizeResource method in resource controllers constructor
// will attach the appropriate "can" middleware definition
// accepts the model class name as its first argument,
// and the name of the route/request parameter
// that will contain the model ID as its second argument
public function __construct() {
$this->authorizeResource(Post::class, 'post');
}
}
// - blade templates
// display a portion of the page if user is authorized to perform action
@can('update', $post)
<!-- current user can update the post -->
@elsecan('create', App\Post::class)
<!-- current user can create new post -->
@endcan
@cannot('update', $post)
<!-- current user cant update the post -->
@elsecannot('create', App\Post::class)
<!-- current user cant create new post -->
@endcannot
// ... are shortcuts for writing @if and @unless statements
@if (Auth::user()->can('update', $post))
<!-- current user can update the post -->
@endif
@unless (Auth::user()->can('update', $post))
<!-- current user cant update the post -->
@endunless
// pass a class name to the @can and @cannot directives
// if the action does not require a model instance
@can('create', App\Post::class)
<!-- current user can create posts -->
@endcan
@cannot('create', App\Post::class)
<!-- current user cant create posts -->
@endcannot
Cache
- cache configuration is located in the .env file
- specify which cache driver to use by default throughout
- by default, is used "file" cache driver, which stores the serialized, cached objects in the filesystem
- Memcached and Redis are supported out of the box, configure multiple cache configurations for the same driver
- uncommented the $app->withFacades() method call in bootstrap/app.php
- Memcached - requires the Memcached PECL package to be installed, you may list all of Memcached servers in the config/cache.php configuration file
- Redis - ...
- Illuminate\Contracts\Cache\Factory and Illuminate\Contracts\Cache\Repository contracts provide access to cache services
// setup a table to contain the cache items
Schema::create('cache', function ($table) {
$table->string('key')->unique();
$table->text('value');
$table->integer('expiration');
});
// "php artisan cache:table" - generate a migration with the proper schema
namespace App\Http\Controllers;
// using Cache facade, convenient, terse access
// to the underlying implementations of the cache contracts
use Illuminate\Support\Facades\Cache;
class UserController extends Controller {
// show a list of all users of the application
public function index() {
$value = Cache::get('key');
//
}
}
// --- accessing multiple cache stores
// Cache::store(key) - addresses one of the stores
// listed in the stores configuration array in cache configuration file
$value = Cache::store('file')->get('foo');
Cache::store('redis')->put('bar', 'baz', 600); // 10 Minutes
// --- RETRIEVE
$value = Cache::get('key'); // null, if does not exists
$value = Cache::get('key', 'default_value');
// with Closure
$value = Cache::get('key', function () {
return DB::table(...)->get();
});
// checking
if (Cache::has('key')) { /* ... */ }
// incrementing / decrementing
Cache::increment('key');
Cache::increment('key', $amount);
Cache::decrement('key');
Cache::decrement('key', $amount);
// retrieve & store
$value = Cache::remember('users', $seconds, function () {
return DB::table('users')->get();
});
// retrieve an item from the cache or store it forever
$value = Cache::rememberForever('users', function () {
return DB::table('users')->get();
});
// retrieve & delete, null returned if the item does not exist
$value = Cache::pull('key');
// --- STORE
Cache::put('key', 'value', $seconds);
Cache::put('key', 'value'); // store indefinitely
// expiration time of the cached item
Cache::put('key', 'value', now()->addMinutes(10));
// store if not present
// return true if item is added, otherwise return false
Cache::add('key', 'value', $seconds);
// store an item in the cache permanently
// must be manually removed from the cache using the "forget" method
// for Memcached driver, this items may be removed when the cache reaches its size limit
Cache::forever('key', 'value');
// --- REMOVE
Cache::forget('key');
Cache::put('key', 'value', 0);
Cache::put('key', 'value', -5); // negative TTL
// clear the entire cache
Cache::flush();
atomic locks
// manipulation of distributed locks without worrying about race conditions
// use memcached, dynamodb, or redis cache driver as default cache driver
// all servers must be communicating with the same central cache server
use Illuminate\Support\Facades\Cache;
// ...
$lock = Cache::lock('foo', 10);
if ($lock->get()) {
// lock acquired for 10 seconds...
$lock->release();
}
// release the lock automatically after the Closure is executed
Cache::lock('foo')->get(function () {
// lock acquired indefinitely and automatically released...
});
// --- if the lock is not available at the request moment
// instruct Laravel to wait for a specified number of seconds
// if the lock can not be acquired within the specified time limit,
// an Illuminate\Contracts\Cache\LockTimeoutException will be thrown
use Illuminate\Contracts\Cache\LockTimeoutException;
// ...
$lock = Cache::lock('foo', 10);
try {
$lock->block(5);
// lock acquired after waiting maximum of 5 seconds...
} catch (LockTimeoutException $e) {
// unable to acquire lock...
} finally {
optional($lock)->release();
}
Cache::lock('foo', 10)->block(5, function () {
// lock acquired after waiting maximum of 5 seconds...
});
// --- acquire a lock in one process and release it in another process
// for example, during a web request and release at the end of a queued job
// that is triggered by that request
// pass the lock scoped "owner token" to the queued job
// so that the job can re-instantiate the lock using the given token
// ... within controller ...
$podcast = Podcast::find($id);
$lock = Cache::lock('foo', 120);
if ($result = $lock->get()) {
ProcessPodcast::dispatch($podcast, $lock->owner());
}
// ... within ProcessPodcast job...
Cache::restoreLock('foo', $this->owner)->release();
// release a lock without respecting its current owner
Cache::lock('foo')->forceRelease();
cache helper, global cache() function
// use the Cache::shouldReceive method just as if you were testing a facade
$value = cache('key');
cache(['key' => 'value'], $seconds);
cache(['key' => 'value'], now()->addMinutes(10));
// cache function call without any arguments
// returns instance of Illuminate\Contracts\Cache\Factory implementation
// allows other caching methods call
cache()->remember('users', $seconds, function () {
return DB::table('users')->get();
});
cache tags
// not supported when using the file or database cache drivers !!!
// when using multiple tags with caches that are stored "forever",
// performance will be best with a driver such as "memcached",
// which automatically purges stale records.
// --- store
Cache::tags(['people', 'artists'])->put('John', $john, $seconds);
Cache::tags(['people', 'authors'])->put('Anne', $anne, $seconds);
// --- access
$john = Cache::tags(['people', 'artists'])->get('John');
$anne = Cache::tags(['people', 'authors'])->get('Anne');
// --- remove
Cache::tags(['people', 'authors'])->flush();
// remove only caches tagged with authors
Cache::tags('authors')->flush();
adding custom cache drivers
// --- writing the driver
// implement the Illuminate\Contracts\Cache\Store contract
// MongoDB cache implementation would look something like this:
namespace App\Extensions;
use Illuminate\Contracts\Cache\Store;
class MongoStore implements Store {
public function get($key) {}
public function many(array $keys);
public function put($key, $value, $seconds) {}
public function putMany(array $values, $seconds);
public function increment($key, $value = 1) {}
public function decrement($key, $value = 1) {}
public function forever($key, $value) {}
public function forget($key) {}
public function flush() {}
public function getPrefix() {}
}
// --- implement each of these methods using a MongoDB connection
for an example look at the Illuminate\Cache\MemcachedStore
// --- driver registration
// could be done in the "boot" method of the default App\Providers\AppServiceProvider
Cache::extend('mongo', function ($app) {
return Cache::repository(new MongoStore);
});
// OR you may create own service provider to house the extension
// dont forget to register the provider in the config/app.php provider array
App\Providers;
use App\Extensions\MongoStore;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\ServiceProvider;
class CacheServiceProvider extends ServiceProvider {
// register bindings in the container
public function register() { /* ... */ }
// bootstrap any application services
public function boot() {
Cache::extend(
'mongo', // name of the driver, correspond to driver option in config/cache.php
// Closure that returns an Illuminate\Cache\Repository instance
// will be passed an $app instance, which is an instance of the service container
function ($app) {
return Cache::repository(new MongoStore);
});
}
}
// update config/cache.php configuration file driver option to the name of extension
events
// https://laravel.com/docs/5.8/events
// execute code on every cache operation,
// listen for the events fired by the cache
// place event listeners within EventServiceProvider
// event listener mappings for the application
protected $listen = [
'Illuminate\Cache\Events\CacheHit' => [
'App\Listeners\LogCacheHit',
],
'Illuminate\Cache\Events\CacheMissed' => [
'App\Listeners\LogCacheMissed',
],
'Illuminate\Cache\Events\KeyForgotten' => [
'App\Listeners\LogKeyForgotten',
],
'Illuminate\Cache\Events\KeyWritten' => [
'App\Listeners\LogKeyWritten',
],
];
Memcached
// configure Memcached in Lumen Api Framework
// 1 - enable memcache in .env file:
CACHE_DRIVER=memcached
// 2 - change configuration option in cache config file:
// copy cache.php file
// from lumen_project\vendor\vendor\laravel\lumen-framework\config\
// and put into lumen_project\config\ folder.
// if config folder is not present in Lumen root, create and put cache.php file.
// change value of default cache driver to memcached in config\cache.php file
'default' => env('CACHE_DRIVER', 'memcached'),
// 3 - install Memcached server and PHP extension (yum)
yum -y install memcached
yum install php-pecl-memcached
// 5 - start Memcached server service
service php-fpm restart
service memcached restart
service nginx restart
// check with phpinfo(), then use ...
// list all of Memcached servers in the config/cache.php
'memcached' => [
[
'host' => '127.0.0.1',
'port' => 11211,
'weight' => 100
],
],
// set the host option to a UNIX socket path
// the port option should be set to 0
'memcached' => [
[
'host' => '/var/run/memcached/memcached.sock',
'port' => 0,
'weight' => 100
],
],
// install on WAMP
Needed Files
memcached.exe Direct Link
MSVCP71.DLL Windows DLL Files
msvcr71.dll
php_memcache.dll Working memcache for PHP 5.3.4 OR REF
Steps
Copy MSVCP71.DLL, msvcr71.dll to C:\windows\sysWOW64
Copy memcached.exe into C:\memcached
Click Windows-Key
Type: CMD
press: Ctrl-Shift-Enter
Choose yes
type: C:\memcached\memcached.exe -d install
type: C:\memcached\memcached.exe -d start
Copy php_memcache.dll to C:\wamp\bin\php\php5.3.4\ext
Restart Apache using Wamp controls
Enable WAMP -> PHP -> PHP Extensions -> php_memcache
Database
- copy OR create app/vendor/laravel/lumen-framework/config/database.php in app/config
- OR use the DB_* configuration options in .env configuration file to configure database setting
- Lumen supports MySQL, Postgres, SQLite, and SQL Server
// uncomment $app->withFacades() in bootstrap/app.php
// to access the database connection via the DB facade
$results = DB::select("SELECT * FROM users");
// otherwise via the app helper:
$results = app('db')->select("SELECT * FROM users");
// Laravel supports single database "URL"
// as an alternative to configuring DB with multiple options:
// mysql://root:password@127.0.0.1/forge?charset=UTF-8
// driver://username:password@host:port/database?options
// read & write connections
// one DB connection for SELECT statements,
// and another for INSERT, UPDATE, and DELETE statements
'mysql' => [
// override the values from the main array
'read' => [
'host' => [
'192.168.1.1',
'196.168.1.2',
],
],
'write' => [
'host' => [
'196.168.1.3',
],
],
// shared credentials, prefix, character set,...
'driver' => 'mysql',
'database' => 'database',
'username' => 'root',
'password' => '',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
// optional value - allow immediate reading of records
// that have been written to the DB during current request cycle
'sticky' => true,
],
// --- multiple DB connections
// access each connections listed in config/database.php
// via the connection method on the DB facade
$users = DB::connection('foo')->select(...);
// access raw, underlying PDO instance
$pdo = DB::connection()->getPdo();
raw sql queries
// DB facade provides methods for each type of query:
// select, update, insert, delete, and statement
namespace App\Http\Controllers;
use Illuminate\Support\Facades\DB;
use App\Http\Controllers\Controller;
class UserController extends Controller {
// show a list of all of the application users
public function index() {
$users = DB::select('select * from users where active = ?', [1]);
return view('user.index', ['users' => $users]);
}
}
// "select" method always return an array of results
// each result within the array will be a PHP stdClass object,
// allowing you to access the values of the results:
foreach ($users as $user) {
echo $user->name;
}
// --- named bindings
$results = DB::select('select * from users where id = :id', ['id' => 1]);
// --- INSERT
DB::insert('insert into users (id, name) values (?, ?)', [1, 'Dayle']);
// --- UPDATE
$affected_rows_nr = DB::update('update users set votes = 100 where name = ?', ['John']);
// --- DELETE
$deleted = DB::delete('delete from users');
// --- GENERAL STATEMENT, which do not return any value
DB::statement('drop table users');
// --- listen - receive each SQL query executed by application
// useful for logging queries or debugging
// you may register query listener in a service provider
namespace App\Providers;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider {
// register any application services
public function register() { /* ... */ }
// bootstrap any application services
public function boot() {
DB::listen(function ($query) {
// $query->sql
// $query->bindings
// $query->time
});
}
}
transactions
// run a set of operations within a database transaction
// if an exception is thrown within the transaction Closure,
// transaction will automatically be rolled back
// if the Closure executes successfully
// transaction will automatically be committed
DB::transaction(function () {
DB::table('users')->update(['votes' => 1]);
DB::table('posts')->delete();
}, 5); // optional - number of transaction reattempts when a deadlock occurs
// --- manually using transactions
DB::beginTransaction();
// THEN
DB::commit();
// OR
DB::rollBack();
Query Builder
- Laravel query builder uses PDO parameter binding to protect application against SQL injection attacks, no need to clean strings being passed as bindings
- PDO does not support binding column names, never allow user input to dictate the column names referenced by queries, including "order by" columns, etc. , validate the column names against a white-list of allowed columns
namespace App\Http\Controllers;
use Illuminate\Support\Facades\DB;
use App\Http\Controllers\Controller;
class UserController extends Controller {
// sShow a list of all of the application users
public function index() {
// "table" method on the DB facade to begin a query
// returns a fluent query builder instance for the given table
// allows chaining more constraints onto the query
// and then finally get the results using the "get" method
$users = DB::table('users')->get();
// return view('user.index', ['users' => $users]);
foreach ($users as $user) {
// Illuminate\Support\Collection, instances of the PHP stdClass object
// access each column value as a property of the object
echo $user->name;
}
// --- first - return a single stdClass object
$user = DB::table('users')->where('name', 'John')->first();
echo $user->name;
// --- value - return the value of the column directly
$email = DB::table('users')->where('name', 'John')->value('email');
// --- find - retrieve a single row by its id column value
$user = DB::table('users')->find(3);
// --- pluck - retrieve a Collection containing the values of a single column
$titles = DB::table('roles')->pluck('title');
foreach ($titles as $title) { echo $title; }
// specify a custom key column for the returned Collection
$roles = DB::table('roles')->pluck('title', 'name');
foreach ($roles as $name => $title) { echo $title; }
// --- chunk - retrieves a small chunk of the results at a time
// and feeds each chunk into a Closure for processing.
// useful for writing Artisan commands that process thousands of records.
// work with the entire users table in chunks of 100 records at a time:
DB::table('users')->orderBy('id')->chunk(100, function ($users) {
// keep processing the records...
foreach ($users as $user) {
//...
}
// OR stop further chunks
return false;
});
// --- chunkById - when updating records while chunking
// will automatically paginate the results based on the record primary key
DB::table('users')->where('active', false)
->chunkById(100, function ($users) {
foreach ($users as $user) {
DB::table('users')
->where('id', $user->id)
->update(['active' => true]);
}
});
// AVOID updating or deleting records inside the chunk callback !
// --- aggregate with count, max, min, avg, sum
// call any of these methods after constructing query
$users = DB::table('users')->count();
$price = DB::table('orders')->max('price');
$price = DB::table('orders')
->where('finalized', 1)
->avg('price'); // combine with other clauses
// --- exists , doesntExist - to determine if any records exist
// instead of using the count method
return DB::table('orders')->where('finalized', 1)->exists();
return DB::table('orders')->where('finalized', 1)->doesntExist();
}
}
SELECT
// select - define required columns
$users = DB::table('users')
->select('name', 'email as user_email')
->get();
// --- addSelect - add a column to existing query builder select clause
$query = DB::table('users')->select('name');
$users = $query->addSelect('age')->get();
// --- distinct - force the query to return distinct results
$users = DB::table('users')
->distinct()
->get();
// --- count - count the total
$posts = DB::table('users')->count();
// select all destinations
// and the name of the flight that most recently arrived at that destination, using a single query:
return Destination::addSelect(['last_flight' => Flight::select('name')
->whereColumn('destination_id', 'destinations.id')
->orderBy('arrived_at', 'desc')
->limit(1)
])->get();
Raw Expressions
// DB::raw - injected into the query as strings, careful, to avoid SQL injection
$users = DB::table('users')
->select(DB::raw('count(*) as user_count, status'))
->where('status', '<>', 1)
->groupBy('status')
->get();
// --- selectRaw - used in place of select(DB::raw(...))
// accepts an optional array of bindings as its second argument
$orders = DB::table('orders')
->selectRaw('price * ? as price_with_tax', [1.0825])
->get();
// --- whereRaw , orWhereRaw - inject a raw WHERE clause into query
$orders = DB::table('orders')
->whereRaw('price > IF(state = "TX", ?, 100)', [200])
->get();
// --- havingRaw , orHavingRaw - set a raw string as the value of the having clause
$orders = DB::table('orders')
->select('department', DB::raw('SUM(price) as total_sales'))
->groupBy('department')
->havingRaw('SUM(price) > ?', [2500])
->get();
// --- orderByRaw - set a raw string as the value of the order by clause
$orders = DB::table('orders')
->orderByRaw('updated_at - created_at DESC')
->get();
JOIN
// --- join - basic "inner join"
$users = DB::table('users')
->join('contacts', 'users.id', '=', 'contacts.user_id')
->join('orders', 'users.id', '=', 'orders.user_id')
->select('users.*', 'contacts.phone', 'orders.price')
->get();
// --- leftJoin , rightJoin - same signature as the join
$users = DB::table('users')
->leftJoin('posts', 'users.id', '=', 'posts.user_id')
->get();
$users = DB::table('users')
->rightJoin('posts', 'users.id', '=', 'posts.user_id')
->get();
// --- crossJoin - cartesian product between first and joined table
$users = DB::table('sizes')
->crossJoin('colours')
->get();
// --- specify constraints on the join clause inside Closure
DB::table('users')
->join('contacts', function ($join) {
$join->on('users.id', '=', 'contacts.user_id')
->orOn(...)
// --- where , orWhere - compare the column against a value
->where('contacts.user_id', '>', 5)
;
})
->get();
// --- joinSub , leftJoinSub , rightJoinSub
// join a query to a sub-query, receive three arguments:
// sub-query, its table alias, Closure that defines the related columns
$latestPosts = DB::table('posts')
->select('user_id', DB::raw('MAX(created_at) as last_post_created_at'))
->where('is_published', true)
->groupBy('user_id');
$users = DB::table('users')
->joinSub($latestPosts, 'latest_posts', function ($join) {
$join->on('users.id', '=', 'latest_posts.user_id');
})->get();
UNION
// --- union - union with a second query
$first = DB::table('users')
->whereNull('first_name');
$users = DB::table('users')
->whereNull('last_name')
->union($first)
->get();
// --- unionAll - is also available and has the same method signature
WHERE
$users = DB::table('users')
->where(
'votes', // name of the column
'=', // operator
100 // value to evaluate against the column
)->get();
->where('votes', '>=', 100)
->where('votes', '<>', 100)
->where('name', 'like', 'T%')
// verify if a column is equal to a given value
// pass the value directly as the second argument
->where('votes', 100)
// --- array of conditions
$users = DB::table('users')->where([
['status', '=', '1'],
['subscribed', '<>', '1'],
])->get();
// --- orWhere - chain where constraints together
->where('votes', '>', 100)->orWhere('name', 'John')
->where('a', 1)->orWhere(['b' => 2, 'c' => 3]);
// --- whereBetween , orWhereBetween
->whereBetween('votes', [1, 100])
// --- whereNotBetween , orWhereNotBetween
->whereNotBetween('votes', [1, 100])
// --- whereIn , whereNotIn , orWhereIn , orWhereNotIn
->whereIn('id', [1, 2, 3])
->whereNotIn('id', [1, 2, 3])
// --- whereNull , whereNotNull , orWhereNull , orWhereNotNull
->whereNull('updated_at')
->whereNotNull('updated_at')
->whereNull(['id', 'expires_at']);
->whereNotNull(['id', 'expires_at']);
// --- whereDate , whereMonth , whereDay , whereYear , whereTime
->whereDate('created_at', '2016-12-31')
->whereMonth('created_at', '12')
->whereDay('created_at', '31')
->whereYear('created_at', '2016')
->whereTime('created_at', '=', '11:20:45')
// --- PARAMETER GROUPING
// select * from users
// where name = 'John' and (votes > 100 or title = 'Admin')
DB::table('users')
->where('name', '=', 'John')
->where(function ($query) {
$query->where('votes', '>', 100)
->orWhere('title', '=', 'Admin');
})->get();
// WHERE (gender = 'Male' and age >= 18) or (gender = 'Female' and age >= 65)
$q->where(function ($query) {
$query->where('gender', 'Male')
->where('age', '>=', 18);
})->orWhere(function($query) {
$query->where('gender', 'Female')
->where('age', '>=', 65);
})
// --- whereExists
DB::table('users')
->whereExists(function ($query) {
$query->select(DB::raw(1))
->from('orders')
// ->whereRaw('orders.user_id = users.id');
->whereColumn('orders.user_id', 'users.id');
})->get();
// select * from users
// where exists (
// select 1 from orders
// where orders.user_id = users.id
// )
// --- query a JSON column "->" operator
// MySQL 5.7, PostgreSQL, SQL Server 2016, SQLite 3.9.0 (JSON1 extension)
->where('options->language', 'en')
->where('preferences->dining->meal', 'salad')
// --- whereJsonContains - query JSON arrays, not supported on SQLite
->whereJsonContains('options->languages', 'en')
// with multiple values, in MySQL and PostgreSQL
->whereJsonContains('options->languages', ['en', 'de'])
// --- whereJsonLength - query JSON arrays by their length
->whereJsonLength('options->languages', 0)
->whereJsonLength('options->languages', '>', 1)
// --- subquery where clauses
// compares the results of a subquery to a given value
$users = User::where(function ($query) {
$query->select('type')
->from('membership')
// all users who have a recent membership of a given type
->whereColumn('membership.user_id', 'users.id')
->orderByDesc('membership.start_date')
->limit(1);
}, 'Pro')->get();
// OR, compare a column to the results of a subquery
$incomes = Income::where('amount', '<', function ($query) {
// all income records where the amount is less than average
$query->selectRaw('avg(i.amount)')->from('incomes as i');
})->get();
Ordering , Grouping , Limit & Offset
// --- orderBy
->orderBy('name', 'desc')
// --- latest / oldest
// order results by date
// by default, result will be ordered by the created_at column, or pass the column name
->latest()->first()
// --- inRandomOrder - sort the query results randomly
// fetch a random user:
->inRandomOrder()->first()
// --- groupBy , having - group the query results
// having method signature is similar to that of the where method
->groupBy('account_id')->having('account_id', '>', 100);
// pass multiple arguments to the groupBy method to group by multiple columns
->groupBy('first_name', 'status')->having('account_id', '>', 100);
// see the havingRaw method
// --- limit query results
// skip , take
->skip(10)->take(5)
// limit , offset - alternative
->offset(10)->limit(5)
// sort all destinations based on when the last flight arrived at that destination
// executing a single query
return Destination::orderByDesc(
Flight::select('arrived_at')
->whereColumn('destination_id', 'destinations.id')
->orderBy('arrived_at', 'desc')
->limit(1)
)->get();
Conditional Clauses
// apply to a query only when something else is true.
// apply a where statement if a given input value is presentin request
$role = $request->input('role');
$users = DB::table('users')
->when($role, function ($query, $role) {
return $query->where('role_id', $role);
})->get();
// "when" method Closure only executes when the first parameter is true.
// another Closure passed as the third parameter to the when method
// is executed if the first parameter evaluates as false
$sortBy = null;
$users = DB::table('users')
->when($sortBy, function ($query, $sortBy) {
return $query->orderBy($sortBy);
}, function ($query) {
return $query->orderBy('name');
})->get();
INSERT
// --- insert - inserting records into the database table
// accepts an array of column names and values
DB::table('users')->insert(
['email' => 'john@example.com', 'votes' => 0]
);
// insert several records into the table
DB::table('users')->insert([
['email' => 'taylor@example.com', 'votes' => 0],
['email' => 'dayle@example.com', 'votes' => 0]
]);
// --- insertGetId - auto-incrementing IDs (if table has)
// insert a record and then retrieve the ID
$id = DB::table('users')->insertGetId(
['email' => 'john@example.com', 'votes' => 0]
);
// in PostgreSQL, method expects the auto-incrementing column to be named "id"
// if you would like to retrieve the ID from a different "sequence"
// pass the column name as the second parameter to the insertGetId method
UPDATE
// like the "insert" method, accepts an array of column and value pairs
// containing the columns to be updated
DB::table('users')
->where('id', 1)
->update(['votes' => 1]);
// --- updateOrInsert
// update an existing record in the database or create it if no matching record exists
// accepts two arguments:
// an array of conditions by which to find the record,
// and an array of column and value pairs containing the columns to be updated
// 1 - attempt to locate a matching database record using the first argument column and value pairs
// and update with the values in the second argument
// 2 - OR new record will be inserted with the merged attributes of both arguments
DB::table('users')
->updateOrInsert(
['email' => 'john@example.com', 'name' => 'John'],
['votes' => '2']
);
// --- updating JSON Columns , only supported on MySQL 5.7+
// "->" - to access the appropriate key in the JSON object
// "." - when updating a JSON column
DB::table('users')
->where('id', 1)
->update(['options->enabled' => true]);
// --- increment , decrement
->increment('votes');
->increment('votes', 5);
->decrement('votes');
->decrement('votes', 5);
// specify additional columns to update during the operation:
->increment('votes', 1, ['name' => 'John']);
DELETE
DB::table('users')->delete();
DB::table('users')->where('votes', '>', 100)->delete();
// truncate the entire table
// remove all rows and reset the auto-incrementing ID to zero
DB::table('users')->truncate();
Pessimistic Locking
// --- sharedLock
// prevents selected rows from being modified until transaction commits
DB::table('users')
->where('votes', '>', 100)
->sharedLock()->get();
// --- lockForUpdate
// prevents rows modification or selection with another shared lock
DB::table('users')->where('votes', '>', 100)->lockForUpdate()->get();
Debugging
// --- dd - display the debug information and stop executing the request
DB::table('users')->where('votes', '>', 100)->dd();
// --- dump - display debug info but allow the request to keep executing:
DB::table('users')->where('votes', '>', 100)->dump();
// --- toSql
DB::table('users')->where('votes', '>', 100)->toSql();
// --- getBindings
- paginator is integrated with the query builder and Eloquent ORM
- generated HTML is compatible with the Bootstrap CSS framework OR define own views to render these links
- if you need groupBy with a paginated result set, query the database and create a paginator manually
- paginator instances are iterators and may be looped as an array
- paginate returns instance of Illuminate\Pagination\LengthAwarePaginator
- simplePaginate returns instance of Illuminate\Pagination\Paginator
// --- paginate - sets the proper limit and offset based on the current page
// the current page is detected by HTTP request query string argument
// this value is automatically detected and inserted into generated links
namespace App\Http\Controllers;
use Illuminate\Support\Facades\DB;
use App\Http\Controllers\Controller;
class UserController extends Controller {
public function index() {
$users = DB::table('users')->paginate(15);
return view('user.index', ['users' => $users]);
}
}
// --- simplePaginate - display simple "Next" and "Previous" links
// useful for large datasets
$users = DB::table('users')->simplePaginate(15);
Eloquent
$users = App\User::paginate(15);
$users = User::where('votes', '>', 100)->paginate(15);
$users = User::where('votes', '>', 100)->simplePaginate(15);
// --- create a pagination instance manually, passing it an array of items
// array_slice the array of results you pass to the paginator
// --- Illuminate\Pagination\Paginator (corresponds to the "simplePaginate")
// does not need to know the total number of items in the result set
// does not have methods for retrieving the index of the last page
// --- Illuminate\Pagination\LengthAwarePaginator
// corresponds to the "paginate"
// accepts almost the same arguments as the Paginator
// does require a count of the total number of items in the result set
Displaying
// --- in Blade template
<div class="container">
@foreach ($users as $user)
{{ $user->name }}
@endforeach
</div>
{{ $users->links() }}
// --- withPath - customize generating links URI
Route::get('users', function () {
$users = App\User::paginate(15);
// http://example.com/custom/url?page=N
$users->withPath('custom/url');
// ...
});
// --- appends - append to the query string of pagination links
// append sort=votes:
{{ $users->appends(['sort' => 'votes'])->links() }}
// --- fragment - append a "hash fragment"
// append #foo:
{{ $users->fragment('foo')->links() }}
// --- onEachSide - control how many additional links are displayed
// by default, three links are displayed on each side of the primary paginator links
{{ $users->onEachSide(5)->links() }}
// --- Converting Results To JSON
// paginator result classes implement the Illuminate\Contracts\Support\Jsonable
// convert a paginator instance to JSON from a route or controller action:
Route::get('users', function () {
return App\User::paginate();
});
// example of the JSON created by returning a paginator instance from a route
{
"total": 50,
"per_page": 15,
"current_page": 1,
"last_page": 4,
"first_page_url": "http://laravel.app?page=1",
"last_page_url": "http://laravel.app?page=4",
"next_page_url": "http://laravel.app?page=2",
"prev_page_url": null,
"path": "http://laravel.app",
"from": 1,
"to": 15,
"data":[
{
// Result Object
},
{
// Result Object
}
]
}
Customizing The Pagination View
// define own views to render links
// call links method on a paginator instance with first argument
{{ $paginator->links('view.name') }}
// passing data to the view...
{{ $paginator->links('view.name', ['foo' => 'bar']) }}
// OR customize pagination views with export to resources/views/vendor directory
php artisan vendor:publish --tag=laravel-pagination
// bootstrap-4.blade.php file corresponds to the default pagination view
// edit to modify the pagination HTML
// to designate a different file as the default pagination view,
// use the paginator defaultView and defaultSimpleView within AppServiceProvider
use Illuminate\Pagination\Paginator;
public function boot() {
Paginator::defaultView('view-name');
Paginator::defaultSimpleView('view-name');
Paginator::useBootstrap(); // keep using bootstrap (Laravel 8+)
}
Paginator Instance Methods
Method |
Description |
$results->count() |
Get the number of items for the current page |
$results->currentPage() |
Get the current page number |
$results->firstItem() |
Get the result number of the first item in the results |
$results->getOptions() |
Get the paginator options |
$results->getUrlRange($start, $end) |
Create a range of pagination URLs |
$results->hasMorePages() |
Determine if there are enough items to split into multiple pages |
$results->lastItem() |
Get the result number of the last item in the results |
$results->lastPage() |
Get the page number of the last available page (Not available when using simplePaginate ) |
$results->nextPageUrl() |
Get the URL for the next page |
$results->onFirstPage() |
Determine if the paginator is on the first page |
$results->perPage() |
The number of items to be shown per page |
$results->previousPageUrl() |
Get the URL for the previous page |
$results->total() |
Determine the total number of matching items in the data store (Not available when using simplePaginate ) |
$results->url($page) |
Get the URL for a given page number |
|
paginate / simplePaginate |
database table has only few rows and does not grow large |
paginate / simplePaginate |
database table has so many rows and grows quickly |
simplePaginate |
it is mandatory to provide the user option to jump to specific pages |
paginate |
it is mandatory to show the user total no of results |
paginate |
not actively using pagination links |
simplePaginate |
UI/UX does not affect from switching numbered pagination links to next / previous pagination links |
simplePaginate |
Using "load more" button or "infinite scrolling" for pagination |
simplePaginate |
Migrations
php artisan make:migration create_users_table
- will be placed in database/migrations
- migration file name contains a timestamp which allows Laravel to determine the order
php artisan make:migration create_users_table --create=users
- indicate the name of the table
php artisan make:migration add_votes_to_users_table --table=users
- whether the migration will be creating a new table
- use the
--path
option to specify a custom output path for the generated migration, should be relative to app base path
--fullpath
- output jump link
- migration class contains two methods
- up - add new tables, columns, or indexes to database
- down - reverse the operations performed by the up method
- within both, you may use the Laravel schema builder to expressively create and modify table
php artisan migrate
- run all outstanding migrations
php artisan migrate --force
- run without a prompt about destructive operations, which means they may cause you to lose data
php artisan migrate:rollback
- rolls back the last "batch" of migrations
php artisan migrate:rollback --step=5
- rollback a limited number of migrations
php artisan migrate:reset
- roll back ALL app migrations
php artisan migrate:refresh
- roll back all migrations and then execute the migrate command, effectively re-creates entire database
php artisan migrate:refresh --seed
- refresh the database and run all database seeds
php artisan migrate:refresh --step=5
- rollback & re-migrate a limited number of migrations
php artisan migrate:fresh
- drop all tables and execute the migrate command
php artisan migrate:fresh --seed
php artisan schema:dump
- write a schema file to database/schema directory, then, when you attempt to migrate db and no other migrations have been executed, Laravel will execute the schema file SQL first, then any remaining migrations that were not part of the schema dump, php artisan schema:dump --prune
- dump the current database schema and prune all existing migrations
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateFlightsTable extends Migration {
// run the migrations
public function up() {
Schema::create('flights', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->string('airline');
$table->timestamps();
});
}
// reverse the migrations
public function down() {
Schema::drop('flights');
}
}
Tables
Schema::create('users', function (Blueprint $table) {
$table->bigIncrements('id');
});
if (Schema::hasTable('users')) {
// ...
}
if (Schema::hasColumn('users', 'email')) {
// ...
}
Schema::connection('db_connection_name')
->create('users', function (Blueprint $table) {
$table->bigIncrements('id');
});
$table->engine = 'InnoDB';// MySQL
$table->charset = 'utf8'; // MySQL
$table->collation = 'utf8_unicode_ci'; // MySQL
$table->temporary(); // except SQL Server
Schema::rename($from, $to);
Schema::drop('users');
Schema::dropIfExists('users');
// before renaming a table,
// verify that any foreign key constraints on the table
// have an explicit name in migration files
// instead of letting Laravel assign a convention based name
// otherwise, the foreign key constraint name will refer to the old table name
Columns
Schema::table('users', function (Blueprint $table) {
$table->string('email');
});
// --- COLUMN TYPES
// auto-incrementing (primary key)
->increments('id') // UNSIGNED INTEGER
->bigIncrements('id') // UNSIGNED BIGINT
->mediumIncrements('id') // UNSIGNED MEDIUMINT
->smallIncrements('id') // UNSIGNED SMALLINT
->tinyIncrements('id') // UNSIGNED TINYINT
->integer('votes') // INTEGER
->bigInteger('votes') // BIGINT
->mediumInteger('votes') // MEDIUMINT
->smallInteger('votes') // SMALLINT
->tinyInteger('votes') // TINYINT
->unsignedInteger('votes') // UNSIGNED INTEGER
->unsignedBigInteger('votes') // UNSIGNED BIGINT
->unsignedMediumInteger('votes') // UNSIGNED MEDIUMINT
->unsignedSmallInteger('votes') // UNSIGNED SMALLINT
->unsignedTinyInteger('votes') // UNSIGNED TINYINT
->decimal('amount', 8, 2) // DECIMAL, precision (total digits) and scale (decimal digits)
->unsignedDecimal('amount', 8, 2) // UNSIGNED DECIMAL, precision (total digits) and scale (decimal digits)
->double('amount', 8, 2) // DOUBLE, precision (total digits) and scale (decimal digits)
->float('amount', 8, 2) // FLOAT, precision (total digits) and scale (decimal digits)
->char('name', 100) // CHAR with an optional length
->string('name', 100) // VARCHAR with a optional length
->lineString('positions') // LINESTRING
->multiLineString('positions') // MULTILINESTRING
->text('description') // TEXT
->longText('description') // LONGTEXT
->mediumText('description') // MEDIUMTEXT
->boolean('confirmed') // BOOLEAN
->year('birth_year') // YEAR
->date('created_at') // DATE
->dateTime('created_at') // DATETIME
->dateTimeTz('created_at') // DATETIME (with timezone)
->time('sunrise') // TIME
->timeTz('sunrise') // TIME (with timezone)
->timestamps() // nullable created_at and updated_at TIMESTAMPs
->nullableTimestamps() // alias of timestamps() method
->timestampsTz() // nullable created_at and updated_at TIMESTAMP (with timezone)s
->timestamp('added_on') // TIMESTAMP
->timestampTz('added_on') // TIMESTAMP (with timezone)
->softDeletes() // nullable deleted_at TIMESTAMP for soft deletes
->softDeletesTz() // nullable deleted_at TIMESTAMP (with timezone) for soft deletes
->enum('level', ['easy', 'hard']) // ENUM
->set('flavors', ['strawberry', 'vanilla']) // SET
->ipAddress('visitor') // IP address
->macAddress('device') // MAC address
->json('options') // JSON
->jsonb('options') // JSONB
->rememberToken() // nullable remember_token VARCHAR(100)
->uuid('id') // UUID
->binary('data') // BLOB
->geometry('positions') // GEOMETRY
->geometryCollection('positions') // GEOMETRYCOLLECTION
->morphs('taggable') // taggable_id UNSIGNED BIGINT and taggable_type VARCHARs
->multiPoint('positions') // MULTIPOINT
->multiPolygon('positions') // MULTIPOLYGON
->nullableMorphs('taggable') // nullable versions of morphs() columns
->point('position') // POINT
->polygon('positions') // POLYGON
Column Modifiers
// add the doctrine/dbal dependency to composer.json
// and run the composer update command in terminal to install the library
Schema::table('users', function (Blueprint $table) {
$table->string('email')->nullable();
});
->after('column') // place the column "after" another column (MySQL)
->autoIncrement() // set INTEGER columns as auto-increment (primary key)
->charset('utf8') // specify a character set for the column (MySQL)
->collation('utf8_unicode_ci') // specify a collation for the column (MySQL/SQL Server)
->comment('my comment') // add a comment to a column (MySQL/PostgreSQL)
->default($value) // specify a "default" value for the column
->first() // place the column "first" in the table (MySQL)
->nullable($value = true) // allows (by default) NULL values to be inserted into the column
->storedAs($expression) // reate a stored generated column (MySQL)
->unsigned() // set INTEGER columns as UNSIGNED (MySQL)
->useCurrent() // set TIMESTAMP columns to use CURRENT_TIMESTAMP as default value
->virtualAs($expression) // create a virtual generated column (MySQL)
->generatedAs($expression) // create an identity column with specified sequence options (PostgreSQL)
->always() // defines the precedence of sequence values over input for an identity column (PostgreSQL)
// --- UPDATING COLUMN ATTRIBUTES
// only the following column types can be "changed":
// integer, bigInteger, smallInteger
// unsignedBigInteger, unsignedInteger, unsignedSmallInteger
// date, dateTime, dateTimeTz,
// text, longText, mediumText, string
// decimal, binary, boolean, json, time
// increase the size of the name column from 25 to 50
Schema::table('users', function (Blueprint $table) {
$table->string('name', 50)->change();
});
// modify a column to be nullable:
Schema::table('users', function (Blueprint $table) {
$table->string('name', 50)->nullable()->change();
});
// --- RENAME
// table that also has a column of type enum is not currently supported
Schema::table('users', function (Blueprint $table) {
$table->renameColumn('from', 'to');
});
// --- DROP
// before dropping columns from a SQLite database
// add the doctrine/dbal dependency to composer.json
// and run the composer update command in terminal to install the library
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('votes');
});
// drop multiple columns from a table
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['votes', 'avatar', 'location']);
});
// dropping or modifying multiple columns within a single migration
// while using a SQLite database is not supported
->dropMorphs('morphable') // drop the morphable_id and morphable_type columns
->dropRememberToken() // drop the remember_token column
->dropSoftDeletes() // drop the deleted_at column
->dropSoftDeletesTz() // Alias of dropSoftDeletes() method
->dropTimestamps() // drop the created_at and updated_at columns
->dropTimestampsTz() // alias of dropTimestamps() method
Indexes
// concatenate table name, name of the indexed column and index type
// users_id_primary, users_email_unique, geo_state_index, geo_location_spatialindex
// chain onto the column definition
$table->string('email')->unique();
// create the index after defining the column
$table->unique('email');
// compound (or composite) index
$table->index(['account_id', 'created_at']);
// pass a second argument to specify index name
$table->unique('email', 'unique_email');
// each accepts an optional second argument to specify the name of the index
// otherwise name will be derived from the names of the table and column(s)
->primary('id') // primary key
->primary(['id', 'parent_id']) // composite keys
->unique('email') // unique index
->index('state') // plain index
->spatialIndex('location') // spatial index (except SQLite)
// --- RENAME
// accepts current index name and desired name
$table->renameIndex('from', 'to')
// --- DROP
// concatenate table name, name of the indexed column and index type
->dropPrimary('users_id_primary') // drop a primary key from the "users" table
->dropUnique('users_email_unique') // drop a unique index from the "users" table
->dropIndex('geo_state_index') // drop a basic index from the "geo" table
// drop a spatial index from the "geo" table (except SQLite)
->dropSpatialIndex('geo_location_spatialindex')
// pass an array of columns into a method that drops indexes,
// conventional index name will be generated: table name, columns and key type
Schema::table('geo', function (Blueprint $table) {
$table->dropIndex(['state']); // Drops index 'geo_state_index'
});
// foreign key constraints
Schema::table('posts', function (Blueprint $table) {
// user_id column on the posts table
$table->unsignedBigInteger('user_id');
// references the id column on a users table
$table->foreign('user_id')->references('id')->on('users');
});
// specify action for the "on delete" and "on update" properties of the constraint
$table->foreign('user_id')
->references('id')->on('users')
->onDelete('cascade');
// foreign key constraints use the same naming convention as indexes
// table name and columns then suffix the name with "_foreign":
$table->dropForeign('posts_user_id_foreign');
// OR pass an array value for automatic conventional constraint name when dropping
$table->dropForeign(['user_id']);
// enable or disable foreign key constraints within migrations
Schema::enableForeignKeyConstraints();
Schema::disableForeignKeyConstraints();
// SQLite disables foreign key constraints by default
// enable foreign key support in database configuration
// before attempting to create them in migrations
utf8mb4 character set (emojis)
// Laravel uses the utf8mb4 character set by default,
// which includes support for storing "emojis" in the database
// MySQL version older than the 5.7.7 or MariaDB older than the 10.2.2
// may need to manually configure the default string length generated by migrations
// call Schema::defaultStringLength method within AppServiceProvider:
use Illuminate\Support\Facades\Schema;
// ...
public function boot() {
Schema::defaultStringLength(191);
}
// ...
// alternatively, enable innodb_large_prefix option for database
Database Seeding
- seeding database with test data using seed classes
- stored in the database/seeders directory
- may have any name, but probably should follow some sensible convention, such as UsersTableSeeder, etc.
- by default, a DatabaseSeeder class is defined, from this class, use the "call" method to run other seed classes, allowing control of seeding order
php artisan make:seeder UsersTableSeeder
- seeder class only contains one method by default: run, called when the
db:seed
is executed, insert data into database however you wish, use the query builder to manually insert data or use Eloquent model factories
- type-hint any dependencies you need within the run method signature, will automatically be resolved via the Laravel service container
- Eloquent mass assignment protection is automatically disabled during database seeding
- use model factories to conveniently generate large amounts of database record, use the factory helper function to insert records into database
- running seeders
composer dump-autoload
- you may need to regenerate Composer autoloader
php artisan db:seed
- now seed database
php artisan db:seed --class=UsersTableSeeder
- specify a specific seeder class to run individually
php artisan db:seed --force
- avoid confirmation prompt before running seeders in 'production' env
php artisan migrate:refresh --seed
- rollback and re-run all migrations, completely re-build database
// modify the default DatabaseSeeder class
// and add a database insert statement to the run method
namespace Database\Seeders;
use Illuminate\Support\Str;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class DatabaseSeeder extends Seeder {
// run the database seeds
public function run() {
DB::table('users')->insert([
'name' => Str::random(10),
'email' => Str::random(10).'@gmail.com',
'password' => bcrypt('secret'),
]);
// Laravel <=7, using factory, create 50 users and attach a relationship to each user
factory(App\User::class, 50)->create()->each(function ($user) {
$user->posts()->save(factory(App\Post::class)->make());
});
// Laravel 8+, using factory, create 50 users and attach a relationship to each user
User::factory()
->count(50)
->hasPosts(1)
->create();
// execute additional seed classes
$this->call([
UsersTableSeeder::class,
PostsTableSeeder::class,
CommentsTableSeeder::class,
]);
}
}
Redis
- advanced key-value store, data structure server, keys can contain: strings, hashes, lists, sets, and sorted sets
composer require predis/predis
OR install phpredis PHP extension via PECL (better performance for heavy use)
- install the illuminate/redis package via Composer
- register the Illuminate/Redis/RedisServiceProvider in bootstrap/app.php:
- $app->register(Illuminate/Redis/RedisServiceProvider::class);
- if you have not called $app->withEloquent(); in bootstrap/app.php file, call $app->configure('database'); in the bootstrap/app.php to ensure the Redis database configuration is properly loaded
- interact with Redis by calling various methods on the Redis facade, supports dynamic methods, call any Redis command on the facade and the command will be passed directly to Redis
- nginx.com/resources/wiki/modules/redis/
// config/database.php
'redis' => [
'client' => 'predis',
'default' => [
// 'scheme' => 'tls', // default is tcp
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_DB', 0),
],
'cache' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_CACHE_DB', 1),
],
],
// OR
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'default' => [
'url' => 'tcp://127.0.0.1:6379?database=0',
],
'cache' => [
'url' => 'tls://user:password@127.0.0.1:6380?database=1',
],
],
// --- cluster of Redis servers
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'clusters' => [
'default' => [
[
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
],
],
],
],
// client-side sharding does not handle failover
// primarily suited for cached data available from another primary data store
// use native Redis clustering
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => 'redis',
],
'clusters' => [
// ...
],
],
// --- Predis
// supports additional connection parameters defined for each Redis servers
// https://github.com/nrk/predis/wiki/Connection-Parameters
'default' => [
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
'read_write_timeout' => 60,
],
// --- PhpRedis
// change the client option of Redis configuration to phpredis
// found in config/database.php configuration file
'redis' => [
'client' => 'phpredis',
// rest of Redis configuration...
],
// supports the following additional connection parameters:
// persistent, prefix, read_timeout and timeout.
// add any of these in config/database.php
'default' => [
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
'read_timeout' => 60,
],
Interacting
// call Redis GET command with Redis facade
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Redis;
class UserController extends Controller {
// how the profile for the given user
public function showProfile($id) {
$user = Redis::get('user:profile:'.$id);
return view('user.profile', ['user' => $user]);
}
}
// --- pass commands to the Redis server
// ...
Redis::set('name', 'Taylor');
$values = Redis::lrange('names', 5, 10);
// ...
// alternatively, use "command" method
// first argument - name of the command
// second argument - array of values
$values = Redis::command('lrange', ['name', 5, 10]);
// --- multiple redis connections
// get a default Redis server instance
$redis = Redis::connection();
// pass the connection or cluster name to the connection method
// to get a specific server or cluster as defined in Redis configuration
$redis = Redis::connection('my-connection');
// --- pipelining commands
// to send many commands to the server in one operation
// Closure as argument that receives a Redis instance
// issue all commands to this instance and they will all be executed
// within a single operation
Redis::pipeline(function ($pipe) {
for ($i = 0; $i < 1000; $i++) {
$pipe->set("key:$i", $i);
}
});
Pub , Sub (publish and subscribe)
// listen for messages on a given "channel"
// publish messages to the channel from another application
// or even using another programming language
// communication between applications and processes
// --- subscribe - setup a channel listener
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
class RedisSubscribe extends Command {
// name and signature of the console command
protected $signature = 'redis:subscribe';
// console command description.
protected $description = 'Subscribe to a Redis channel';
// eExecute the console command
public function handle() {
Redis::subscribe(['test-channel'], function ($message) {
echo $message;
});
}
}
// --- publish
Route::get('publish', function () {
// Route logic...
Redis::publish('test-channel', json_encode(['foo' => 'bar']));
});
// --- psubscribe - subscribe to a wildcard channel
// useful for catching all messages on all channels
// $channel is passed as the second argument to the provided callback Closure
Redis::psubscribe(['*'], function ($message, $channel) {
echo $message;
});
Redis::psubscribe(['users.*'], function ($message, $channel) {
echo $message;
});
Eloquent
- uncomment $app->withEloquent() in bootstrap/app.php
- ORM, simple ActiveRecord implementation for working with DB
- each database table has a corresponding "Model" which is used to interact with that table: query for data, insert new records, ...
- Models typically live in the app directory, but you are free to place them anywhere that can be auto-loaded according to composer.json file
- models extend Illuminate\Database\Eloquent\Model class
php artisan make:model Flight
- easiest way to create a model instance
php artisan make:model Flight --migration
OR with -m
- generate model and a database migration for model
php artisan make:model Company -mcr
- create migration and resourceful controller
- use any of available query builder methods with Eloquent model
- mass-assignment vulnerability occurs when a user passes an unexpected HTTP parameter through a request, and that parameter changes a column in database you did not expect
- is - determine if two models are the "same": if($post->is($anotherPost)){...
- API Resources
- transformation layer that sits between Eloquent models and the JSON responses that are actually returned
- only need to transform a given model into an array
- each resource contains a toArray method which translates model attributes into an API friendly array that can be returned to users
- transform individual models
php artisan make:resource User
- placed in the app/Http/Resources
- extend the Illuminate\Http\Resources\Json\JsonResource class
- or collections of models to include links and other meta information that is relevant to an entire collection of a given resource, use the --collection flag when creating the resource, or include the word Collection in the resource name
php artisan make:resource Users --collection
php artisan make:resource UserCollection
- polymorphic relationship allows the child model to belong to more than one type of model using a single association: where users can share blog posts and videos, a Comment model might belong to both the Post and Video models
Conventions
namespace App;
use Illuminate\Database\Eloquent\Model;
class TestModel extends Model {
protected $table = 'test_models'; // "snake case", plural name of the class
protected $primaryKey = 'id'; // or override with custom like test_model_id
// to use a non-incrementing or a non-numeric primary key:
public $incrementing = false;
// if primary key is not an integer:
protected $keyType = 'string';
// Eloquent expects created_at and updated_at columns to exist
// if you do not wish to have these columns automatically managed:
public $timestamps = false;
// customize timestamps format
// determine how date attributes are stored in the database
// and format when the model is serialized to an array or JSON
protected $dateFormat = 'U';
// customize columns names used to store the timestamps
const CREATED_AT = 'creation_date';
const UPDATED_AT = 'last_update';
// specify a different connection, instead of default
protected $connection = 'connection-name';
// define default values for some attributes
protected $attributes = [
'delayed' => false,
];
// which fields will be Carbon-ized
protected $dates = ['created_at', 'deleted_at'];
// additional values returned in JSON
protected $appends = ['field1', 'field2'];
// mass assignable "white list"
protected $fillable = ['name']; // then you can use "create" or "fill"
// mass assignable "black list"
protected $guarded = ['price'];
// use either $fillable or $guarded - not both !!!
protected $guarded = []; // all attributes are mass assignable
// attributes excluded from the model JSON form
protected $hidden = [
'password',
'api_token',
];
protected $perPage = 25; // override pagination count (default 15)
// override default behavior
public static function boot() {
parent::boot();
static::updating(function($model) {
// do some logging
// override some property like $model->something = transform($something);
});
// setting some field value at the moment of creating the model object
self::creating(function ($model) {
$model->uuid = (string)Uuid::generate();
});
}
// booting()/booted() - any actions required before/after model boots
}
SELECT
// --- all - return all of the results in the model table
$flights = App\Flight::all();
foreach ($flights as $flight) {
echo $flight->name;
}
$flights = App\Flight::where('active', 1)
->orderBy('name', 'desc')
->take(10)
->get();
// --- fresh - reload a fresh model instance from the database
// existing model instance will not be affected
$flight = App\Flight::where('number', 'FR 900')->first();
$freshFlight = $flight->fresh();
// --- refresh - re-hydrate the existing model using fresh data from the database
// all of its loaded relationships will be refreshed
$flight = App\Flight::where('number', 'FR 900')->first();
$flight->number = 'FR 456';
$flight->refresh();
$flight->number; // "FR 900"
// --- chunk - process thousands, retrieves based on offset and limit (slower)
// use if db instance does not have enough memory
Flight::chunk(200, function ($flights) {
foreach ($flights as $flight) {
// ...
}
});
// --- chunkById - retrieves db results based on an primary field (faster)
$posts = Post::chunkById(100, function($posts){
foreach ($posts as $post){
// Process posts
}
});
// --- cursor - iterate through database records using a cursor
// will only execute a single query
// for processing large amounts of data, greatly reduce memory usage
// returns instance of Illuminate\Support\LazyCollection
// use if web app running application has less memory
foreach (Flight::where('foo', 'bar')->cursor() as $flight) {
//
}
// --- find - model by its primary key
$flight = App\Flight::find(1);
// with an array of primary keys
$flights = App\Flight::find([1, 2, 3]);
// --- first - first model matching the query constraints
$flight = App\Flight::where('active', 1)->first();
// --- findOrFail , firstOrFail - throw an exception if a model is not found
$model = App\Flight::findOrFail(1);
$model = App\Flight::where('legs', '>', 100)->firstOrFail();
// not necessary to write explicit checks to return 404 responses
Route::get('/api/flights/{id}', function ($id) {
return App\Flight::findOrFail($id);
});
// specify the attributes to select as the second argument
$user = App\User::find(1, ['name', 'age']);
$user = App\User::findOrFail(1, ['name', 'age']);
// --- where[field]
$users = User::whereApproved(1)->get(); // same as: User::where('approved', 1)->get();
User::whereDate('created_at', date('Y-m-d'));
User::whereDay('created_at', date('d'));
User::whereMonth('created_at', date('m'));
User::whereYear('created_at', date('Y'));
// --- avoiding IF-ELSE
$query = Author::query();
$query->when(request('filter_by') == 'likes', function ($q) {
return $q->where('likes', '>', request('likes_amount', 0));
});
$query->when(request('filter_by') == 'date', function ($q) {
return $q->orderBy('created_at', request('ordering_rule', 'desc'));
});
// passing of the parameters
$query->when(request('role', false), function ($q, $role) {
return $q->where('role_id', $role);
});
// --- count, max, min, avg, and sum aggregates
// return the appropriate scalar value instead of a full model instance
$count = App\Flight::where('active', 1)->count();
$posts = Post::count();
$max = App\Flight::where('active', 1)->max('price');
// --- replicate() - clone a model using
// create a copy of the model into a new, non-existing instance
$user = App\User::find(1);
$newUser = $user->replicate();
$newUser->save();
INSERT , UPDATE
// --- save - create a new record
// create a new model instance, set attributes on the model
// created_at and updated_at timestamps will automatically be set
ace App\Http\Controllers;
use App\Flight;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class FlightController extends Controller {
public function store(Request $request) {
// Validate the request...
$flight = new Flight;
$flight->name = $request->name;
$flight->save();
}
}
// also used to update models that already exist
$flight = App\Flight::find(1);
$flight->name = 'New Flight Name';
$flight->save();
// overriding default updated_at with our pre-defined one
$product = Product::find($id);
$product->updated_at = '2019-01-01 10:00:00';
$product->save(['timestamps' => false]);
// --- update - mass updates, returns affected rows count
App\Flight::where('active', 1)
->where('destination', 'San Diego')
->update(['delayed' => 1]);
// mass update via Eloquent will not fire
// saving, saved, updating, and updated model events
// models are never actually retrieved when issuing a mass update
// Eloquent models protect against mass-assignment by default
// specify either a "fillable" or "guarded" attribute on the model
// allow with "protected $fillable = ['name'];" in model class
// disallow with "protected $guarded = ['name'];"
// then you can use this methods:
// --- create - save a new model in a single line
// inserted model instance will be returned
$flight = App\Flight::create(['name' => 'Flight 10']);
// --- fill - populate already received model with attributes
$flight->fill(['name' => 'Flight 22']);
// --- firstOrCreate
// try to locate a record matching the given attributes
// if the model is not found,
// a record will be inserted with the attributes from the first parameter,
// along with those in the optional second parameter.
// --- firstOrNew
// try to locate a record matching the given attributes
// if a model is not found, new model instance will be returned.
// if returned model has not yet been persisted to the database
// call save() manually to persist it.
// retrieve flight by name, or create it if it doesnt exist:
$flight = App\Flight::firstOrCreate(['name' => 'Flight 10']);
// retrieve flight by name
// or create it with the name, delayed, arrival_time attributes:
$flight = App\Flight::firstOrCreate(
['name' => 'Flight 10'],
['delayed' => 1, 'arrival_time' => '11:30']
);
// retrieve by name, or instantiate:
$flight = App\Flight::firstOrNew(['name' => 'Flight 10']);
// retrieve by name,
// or instantiate with name, delayed, arrival_time attributes:
$flight = App\Flight::firstOrNew(
['name' => 'Flight 10'],
['delayed' => 1, 'arrival_time' => '11:30']
);
// second argument array will be merged with the first argument to the method ($attributes)
// when creating the related model if one does not already exist
$user->roles()->firstOrCreate([
'name' => 'Administrator',
], [
'created_by' => $user->id,
]);
// --- updateOrCreate
// update an existing model or create a new model if none exists
// call save() to persists the model.
// if there is a flight from Oakland to San Diego, set the price to $99,
// if no matching model exists, create one:
$flight = App\Flight::updateOrCreate(
['departure' => 'Oakland', 'destination' => 'San Diego'],
['price' => 99, 'discounted' => 1]
);
// --- getOriginal
// get the original attributes after mutating an Eloquent record
$user = App\User::first();
$user->name; //John
$user->name = "Peter"; //Peter
$user->getOriginal('name'); //John
$user->getOriginal(); //Original $user record
$user->getRawOriginal();
// --- isDirty - if the model or given attribute have been modified
$user = App\User::first();
$user->isDirty(); //false
$user->name = "Peter";
$user->isDirty(); //true
// check if a particular attribute is changed
$user->isDirty('name'); //true
$user->isDirty('age'); //false
// --- getChanges - changed attributes
// only if you save the model or sync the changes using syncChanges()
$user->getChanges()//[ "name" => "Peter", ... ]
DELETE
$flight = App\Flight::find(1);
$flight->delete();
// --- destroy - delete by primary key
// accepts array or a collection of primary keys
App\Flight::destroy(1);
App\Flight::destroy(1, 2, 3);
App\Flight::destroy([1, 2, 3]);
App\Flight::destroy(collect([1, 2, 3]));
// --- delete by query
// will not fire any model events ("deleting", "deleted")
$deletedRows = App\Flight::where('active', 0)->delete();
Soft Delete
// not removed from database
// deleted_at attribute is set on the model and inserted into the database
// if a model has a non-null deleted_at value,
// the model has been soft deleted
// enable soft deletes for a model,
// use the Illuminate\Database\Eloquent\SoftDeletes trait on the model
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Flight extends Model {
use SoftDeletes;
}
// also add the deleted_at column to database table
// schema builder contains a helper method
Schema::table('flights', function (Blueprint $table) {
$table->softDeletes();
});
// now, when you call the "delete" method on the model,
// deleted_at column will be set to the current date and time
// soft deleted models will automatically be excluded from all query results.
// to determine if a given model instance has been soft deleted
if ($flight->trashed()) {
// ...
}
// --- withTrashed - include soft deleted models
$flights = App\Flight::withTrashed()
->where('account_id', 1)
->get();
// used on a relationship query
$flight->history()->withTrashed()->get();
// --- onlyTrashed - retrieve ONLY soft deleted models
$flights = App\Flight::onlyTrashed()
->where('airline_id', 1)
->get();
// --- restore - restoring soft deleted models, will not fire any model events
$flight->restore();
// in a query
App\Flight::withTrashed()
->where('airline_id', 1)
->restore();
// on relationships
$flight->history()->restore();
// --- forceDelete - permanently remove a soft deleted model
// single model
$flight->forceDelete();
// all related models.
$flight->history()->forceDelete();
Query Global Scopes
// add constraints to all queries for a given model
// make sure every query for a given model receives certain constraints
// define a class that implements the Illuminate\Database\Eloquent\Scope
// requires to "apply" and may add "where" constraints to the query
namespace App\Scopes;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
class AgeScope implements Scope {
public function apply(Builder $builder, Model $model) {
$builder->where('age', '>', 200);
}
}
// if global scope is adding columns to the select clause of the query
// use the addSelect method instead of select
// prevent unintentional replacement of the query existing select clause
// --- assign a global scope to a model
// override a given model boot method and use the addGlobalScope method
namespace App;
use App\Scopes\AgeScope;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
protected static function booted() {
static::addGlobalScope(new AgeScope);
}
}
// then query to User::all() will produce the following SQL:
// SELECT * FROM `users` WHERE `age` > 200
// --- anonymous global scopes
// do not need a separate clas
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
class User extends Model {
protected static function booted() {
static::addGlobalScope('age', function (Builder $builder) {
$builder->where('age', '>', 200);
});
// always be ordered by name field
static::addGlobalScope('order', function (Builder $builder) {
$builder->orderBy('name', 'asc');
});
}
}
// --- removing global scopes for a quary
User::withoutGlobalScope(AgeScope::class)->get();
User::withoutGlobalScope('age')->get(); // for anonymous
// remove all of the global scopes
User::withoutGlobalScopes()->get();
// remove some of the global scopes
User::withoutGlobalScopes([
FirstScope::class, SecondScope::class
])->get();
Query Local Scopes
// define common sets of constraints that you may easily re-use
// should always return a query builder instance
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
// only include popular users
public function scopePopular($query) {
return $query->where('votes', '>', 100);
}
// only include active users
public function scopeActive($query) {
return $query->where('active', 1);
}
}
// --- call scope methods when querying the model
// dont include the scope prefix when calling the method
// chain calls to various scopes
$users = App\User::popular()->active()->orderBy('created_at')->get();
// --- or - combining multiple Eloquent model scopes
$users = App\User::popular()
->orWhere(
function (Builder $query) {
$query->active();
})->get();
// higher order orWhere method
$users = App\User::popular()
->orWhere
->active()->get();
// --- dynamic scopes, scope that accepts parameters
// scope parameters should be defined after the $query parameter
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
// scope a query to only include users of a given type
public function scopeOfType($query, $type) {
return $query->where('type', $type);
}
}
// now pass the parameters when calling the scope
$users = App\User::ofType('admin')->get();
Events , Observers
// execute code each time a specific model class is saved or updated
// each event receives the instance of the model through its constructor
// hook into the following points in a model lifecycle:
// retrieved, creating, created, updating, updated,
// saving, saved, deleting, deleted, restoring, restored
// saved and updated model events will not be fired for mass update
// use event listeners to handle the events
// --- $dispatchesEvents
// maps various points of the Eloquent model lifecycle to own event classes
namespace App;
use App\Events\UserSaved;
use App\Events\UserDeleted;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable {
use Notifiable;
protected $dispatchesEvents = [
'saved' => UserSaved::class,
'deleted' => UserDeleted::class,
];
}
// --- Observers - group all of listeners into a single class
// php artisan make:observer UserObserver --model=User
// will place the new observer in App/Observers directory
namespace App\Observers;
use App\User;
class UserObserver {
// handle the User "created" event
public function created(User $user) {
//
}
// handle the User "updated" event
public function updated(User $user) {
//
}
// handle the User "deleted" event
public function deleted(User $user) {
//
}
}
// --- observe - register an observe on the model
// in the boot method of one of service providers
namespace App\Providers;
use App\User;
use App\Observers\UserObserver;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider {
// register any application services
public function register() {
// ...
}
// bootstrap any application services
public function boot() {
User::observe(UserObserver::class);
}
}
Relationships
--- ONE TO ONE
// --- hasOne
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
// get the phone record associated with the user
public function phone() {
return $this->hasOne('App\Phone');
// Phone model is automatically assumed to have a user_id foreign key
// override this convention with second argument
return $this->hasOne('App\Phone', 'foreign_key');
// use relationship value other than id for User with third argument
return $this->hasOne('App\Phone', 'foreign_key', 'local_key');
}
public function latestPost() {
return $this->hasOne(\App\Post::class)->latest();
}
}
// ...
$phone = User::find(1)->phone;
$users = User::with('latestPost')->get()->sortByDesc('latestPost.created_at');
// attach a phone to the user, no need to define user_id
$user = User::find(1);
$phone = new Phone;
$phone->address = "Some address in New York";
$user->phone()->save($phone);
// delete related relationship entry
$user = User::find(1);
$user->phone()->delete();
// --- belongsTo - inverse of a hasOne
namespace App;
use Illuminate\Database\Eloquent\Model;
class Phone extends Model {
// get the user that owns the phone
public function user() {
// Eloquent will try to match
// the user_id from the Phone model to an id on the User model
return $this->belongsTo('App\User');
// override foreign key on the Phone model (when is not user_id)
return $this->belongsTo('App\User', 'foreign_key');
// if Phone primary key is not id
// or you wish to join the child model to a different column
return $this->belongsTo('App\User', 'foreign_key', 'other_key');
}
}
// ...
$phone_user = Phone::find(1)->user;
// update a model attached via a BelongsTo relationship and receive mass assignment update protection and events
$post->user()->update(['foo' => 'bar']); // ad-hoc query, no mass assignment protection or events
$post->user->update(['foo' => 'bar']); // model update, provides mass assignment protection and events
--- ONE TO MANY
// single model owns any amount of other models
// --- hasMany
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model {
// get the comments for the blog post
public function comments() {
// Eloquent will assume the foreign key on the Comment model is post_id
return $this->hasMany('App\Comment');
// override the foreign and local keys
return $this->hasMany('App\Comment', 'foreign_key');
return $this->hasMany('App\Comment', 'foreign_key', 'local_key');
return $this->hasMany('App\User')->where('approved', 1)->orderBy('email');
}
// separate relationship
function publishedBooks() {
return $this->hasMany('App\Book')->where('published', 1);
// ->orderBy('title');
}
}
// ...
$comments = App\Post::find(1)->comments;
foreach ($comments as $comment) {
// ...
}
$comment = App\Post::find(1)->comments()->where('title', 'foo')->first();
// ...
$post = App\Post::find(1);
$post->comments()->create([ 'title' => 'Harry Potter' ]);
$post->comments()->createMany([
[ 'title' => 'Harry Potter' ],
[ 'title' => 'Harry Potter Returns' ]
]);
// --- belongsTo
// inverse of a hasMany relationship, defined on the child model
namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model {
// get the post that owns the comment
public function post() {
return $this->belongsTo('App\Post');
return $this->belongsTo('App\Post', 'foreign_key');
return $this->belongsTo('App\Post', 'foreign_key', 'other_key');
}
}
// ...
$comment = App\Comment::find(1);
echo $comment->post->title;
// ...
// levels of relationships
class Author extends Model {
function books() {
return $this->hasMany('App\Book');
}
}
class Book extends Model {
function chapters() {
return $this->hasMany('App\Chapter');
}
}
// load several relationships in one sentence
// result will contain all chapters for every book,
// which you can loop through, without any additional SQL queries
$author = Author::with('books.chapters')->find(1);
--- MANY TO MANY
// --- belongsToMany
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
// the roles that belong to the user
public function roles() {
// many users may have the role of "Admin"
// to define this relationship, three database tables are needed:
// users, roles, and role_user
// role_user table is derived from the alphabetical order
// of the related model names, and contains the user_id and role_id columns
return $this->belongsToMany('App\Role');
// override alphabetical order of two related model name join
return $this->belongsToMany('App\Role',
'role_user');
// customize the column names of the keys on the table
return $this->belongsToMany('App\Role',
'role_user', 'user_id', 'role_id');
}
}
// ...
$user = App\User::find(1);
foreach ($user->roles as $role) {
// ...
}
$roles = App\User::find(1)->roles()->orderBy('name')->get();
// ...
// to define the inverse of a many-to-many relationship
// place another call to belongsToMany on related model
namespace App;
use Illuminate\Database\Eloquent\Model;
class Role extends Model {
// the users that belong to the role
public function users() {
// usual table and key customization options are available
return $this->belongsToMany('App\User');
}
}
// --- pivot - access the intermediate table
// may be used like any other Eloquent model
$user = App\User::find(1);
foreach ($user->roles as $role) {
echo $role->pivot->created_at;
}
// --- specify pivot table extra attributes (if not only model keys)
return $this->belongsToMany('App\Role')
->withPivot('column1', 'column2');
// --- to automatically maintain created_at and updated_at timestamps
return $this->belongsToMany('App\Role')->withTimestamps();
// --- rename intermediate table
return $this->belongsToMany('App\Podcast')
->as('subscription')
->withTimestamps();
// ...
// access using customized name
$users = User::with('podcasts')->get();
foreach ($users->flatMap->podcasts as $podcast) {
echo $podcast->subscription->created_at;
}
// ...
// --- filter the results returned by belongsToMany
return $this->belongsToMany('App\Role')
->wherePivot('approved', 1);
return $this->belongsToMany('App\Role')
->wherePivotIn('priority', [1, 2]);
// --- custom many-to-many pivot model, should extend
// Illuminate\Database\Eloquent\Relations\Pivot
// custom polymorphic many-to-many pivot models should extend
// Illuminate\Database\Eloquent\Relations\MorphPivot
namespace App;
use Illuminate\Database\Eloquent\Model;
class Role extends Model {
// users that belong to the role
public function users() {
return $this->belongsToMany('App\User')
->using('App\RoleUser');
}
}
// defining the RoleUser model, we will extend the Pivot
namespace App;
use Illuminate\Database\Eloquent\Relations\Pivot;
class RoleUser extends Pivot {
// ...
// indicates if the IDs are auto-incrementing
public $incrementing = true;
}
// --- combine using and withPivot to retrieve columns from the intermediate table
// retrieve the created_by and updated_by columns from the RoleUser
namespace App;
use Illuminate\Database\Eloquent\Model;
class Role extends Model {
// the users that belong to the role
public function users() {
return $this->belongsToMany('App\User')
->using('App\RoleUser')
->withPivot([
'created_by',
'updated_by'
]);
}
}
// --- Pivot models may not use the SoftDeletes trait
// to do so, convert pivot model to an actual Eloquent model
--- hasOneThrough - links models through a single intermediate relation
// each supplier has one user,
// each user is associated with one user history record,
// supplier model may access the user history through the user
users
id - integer
supplier_id - integer
suppliers
id - integer
history
id - integer
user_id - integer
// ---
namespace App;
use Illuminate\Database\Eloquent\Model;
class Supplier extends Model {
// get the user history
public function userHistory() {
return $this->hasOneThrough(
'App\History', // final model to access
'App\User' // intermediate model
// typical foreign key conventions will be used for relationship queries
// customize relationship keys:
'supplier_id', // foreign key on the intermediate model (users table)
'user_id', // foreign key on the final model (history table)
'id', // local key (suppliers table)
'id' // local key of the intermediate model (users table)
);
}
}
--- hasManyThrough - access distant relations via an intermediate relation
// Country model might have many Post models
// through an intermediate User model
countries
id - integer
name - string
users
id - integer
country_id - integer
name - string
posts
id - integer
user_id - integer
title - string
// gather all blog posts for a given country via $country->posts
// ---
namespace App;
use Illuminate\Database\Eloquent\Model;
class Country extends Model {
public function posts() {
return $this->hasManyThrough(
'App\Post', // final model to access
'App\User', // intermediate model
// typical foreign key conventions will be used for relationship queries
// customize relationship keys:
'country_id', // foreign key on the intermediate model (users table)
'user_id', // foreign key on the final model (posts table)
'id', // local key (countries table)
'id' // local key of the intermediate model (users table)
);
}
}
// getting all the comments through posts
User hasManyThrough Comment, PostController
// ...
public function comments() {
return $this->hasManyThrough(Comment::class, Post::class);
}
// ...
// then
$user->comments; // collection of all the comments
--- morphOne - One To One (Polymorphic)
// target model can belong to more than one type of model on a single association
// ---
// blog Post and a User may share a polymorphic relation to an Image model.
// single list of unique images that are used for both blog posts and user accounts
posts
id - integer
name - string
users
id - integer
name - string
images
id - integer
url - string
imageable_id - integer
imageable_type - string // class name of the parent model
// ---
namespace App;
use Illuminate\Database\Eloquent\Model;
class Image extends Model {
// get all of the owning imageable models
public function imageable() {
// parent from the polymorphic model
return $this->morphTo();
}
}
class Post extends Model {
// get the post image
public function image() {
return $this->morphOne('App\Image', 'imageable');
}
}
class User extends Model {
// get the user image
public function image() {
return $this->morphOne('App\Image', 'imageable');
}
}
// ...
// retrieve the image for a post
$post = App\Post::find(1);
$image = $post->image;
$image = App\Image::find(1);
// parent who asks for image, Post or User instance
$imageable = $image->imageable;
--- morphMany - One To Many (Polymorphic)
// target model can belong to more than one type of model on a single association
// ---
// users can "comment" on both posts and videos
// use a single comments table for both of these scenarios
posts
id - integer
title - string
body - text
videos
id - integer
title - string
url - string
comments
id - integer
body - text
commentable_id - integer
commentable_type - string
// ---
namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model {
// get all of the owning commentable models
public function commentable() {
// owner of a polymorphic relation from the polymorphic model
return $this->morphTo();
}
}
class Post extends Model {
// get all of the post comments
public function comments() {
return $this->morphMany('App\Comment', 'commentable');
}
}
class Video extends Model {
// get all of the video comments
public function comments() {
return $this->morphMany('App\Comment', 'commentable');
}
}
// ...
// access all of the comments for a post
$post = App\Post::find(1);
foreach ($post->comments as $comment) {
// ...
}
$comment = App\Comment::find(1);
// return Post or Video instance, depending on which type of model owns the comment
$commentable = $comment->commentable;
--- morphedByMany - Many To Many (Polymorphic)
// blog Post and Video model could share a polymorphic relation to a Tag model
// use a single list of unique tags that are shared across blog posts and videos
// ---
posts
id - integer
name - string
videos
id - integer
name - string
tags
id - integer
name - string
taggables
tag_id - integer
taggable_id - integer
taggable_type - string
// ---
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model {
// get all of the tags for the post
public function tags() {
return $this->morphToMany('App\Tag', 'taggable');
}
}
// define a method for each of its related models
class Tag extends Model {
// get all of the posts that are assigned this tag
public function posts() {
return $this->morphedByMany('App\Post', 'taggable');
}
// get all of the videos that are assigned this tag
public function videos() {
return $this->morphedByMany('App\Video', 'taggable');
}
}
// ...
// access all of the tags for a post
$post = App\Post::find(1);
foreach ($post->tags as $tag) {
// ...
}
// owner of a polymorphic relation from the polymorphic model
// posts or videos methods on the Tag model
$tag = App\Tag::find(1);
foreach ($tag->videos as $video) {
// ...
}
// custom polymorphic many-to-many pivot models should extend
// Illuminate\Database\Eloquent\Relations\MorphPivot
--- Custom Polymorphic Types
// define a "morph map" to instruct Eloquent
// to use a custom name for each model instead of the class name.
// Comment may belong to a Post or a Video,
// the default commentable_type would be either App\Post or App\Video.
// decouple database from application internal structure
use Illuminate\Database\Eloquent\Relations\Relation;
Relation::morphMap([
'posts' => 'App\Post',
'videos' => 'App\Video',
]);
// register morphMap in the boot function of AppServiceProvider
// or create a separate service provider
// custom polymorphic many-to-many pivot models should extend
// Illuminate\Database\Eloquent\Relations\MorphPivot
Querying Relations
// Eloquent relationships are defined via methods,
// call those methods to obtain an instance of the relationship
// without actually executing the relationship queries.
// all types of Eloquent relationships also serve as query builders
// use any of the query builder methods
// continue to chain constraints onto the relationship query
// before finally executing the SQL against database
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
// get all of the posts for the user
public function posts() {
return $this->hasMany('App\Post');
}
}
// ...
$user = App\User::find(1);
$user->posts()->where('active', 1)->get(); // ->first(), ->count()
// --- access the relationship as if it were a property
// do not add additional constraints to an Eloquent relationship query
$user = App\User::find(1);
foreach ($user->posts as $post) {
// ...
}
// "lazy loaded", relationship only loads data when you actually access them
// pre-load relationships they know will be accessed after loading the model
// significant reduction in SQL queries for loading a model relations
// --- has , orHas - existence of a relationship
// posts that have at least one comment
$posts = App\Post::has('comments')->get();
// posts that have three or more comment
$posts = App\Post::has('comments', '>=', 3)->get();
// "dot" notation - posts that have at least one comment and vote
$posts = App\Post::has('comments.votes')->get();
// --- whereHas , orWhereHas
// add customized constraints to a relationship constraint
use Illuminate\Database\Eloquent\Builder;
// posts with at least one comment containing words like foo%
$posts = App\Post::whereHas('comments', function ($query) {
$query->where('content', 'like', 'foo%');
})->get();
// posts with at least ten comments containing words like foo%
$posts = App\Post::whereHas('comments', function ($query) {
$query->where('content', 'like', 'foo%');
}, '>=', 10)->get();
// --- whereHasMorph
use Illuminate\Database\Eloquent\Builder;
// comments associated to posts or videos with a title like foo%
$comments = App\Comment::whereHasMorph(
'commentable',
['App\Post', 'App\Video'],
function (Builder $query) {
$query->where('title', 'like', 'foo%');
}
)->get();
// comments associated to posts with a title not like foo%
$comments = App\Comment::whereDoesntHaveMorph(
'commentable',
'App\Post',
function (Builder $query) {
$query->where('title', 'like', 'foo%');
}
)->get();
// use the $type parameter to add different constraints depending on the related model
use Illuminate\Database\Eloquent\Builder;
$comments = App\Comment::whereHasMorph(
'commentable',
['App\Post', 'App\Video'],
function (Builder $query, $type) {
$query->where('title', 'like', 'foo%');
if ($type === 'App\Post') {
$query->orWhere('content', 'like', 'foo%');
}
}
)->get();
// provide * instead of passing an array as a wildcard
// retrieve all the possible polymorphic types from the db
// Laravel executes an additional query in order to perform this operation
use Illuminate\Database\Eloquent\Builder;
$comments = App\Comment::whereHasMorph('commentable', '*', function (Builder $query) {
$query->where('title', 'like', 'foo%');
})->get();
// --- doesntHave , orDoesntHave - absence of a relationship
// all blog posts that dont have any comments
$posts = App\Post::doesntHave('comments')->get();
// --- orWhere clauses will be logically grouped
// at the same level as the relationship constraint
$user->posts()
->where('active', 1)
->orWhere('votes', '>=', 100)
->get();
// select * from posts
// where user_id = ? and active = 1 or votes >= 100
// use constraint groups to logically group the conditional checks
$user->posts()
->where(function ($query) {
return $query->where('active', 1)
->orWhere('votes', '>=', 100);
})->get();
// select * from posts
// where user_id = ? and (active = 1 or votes >= 100)
// --- whereDoesntHave , orWhereDoesntHave - doesntHave with conditions
$posts = App\Post::whereDoesntHave('comments', function (Builder $query) {
// checking the content of a comment
$query->where('content', 'like', 'foo%');
})->get();
// "dot" notation with nested relationship
$posts = App\Post::whereDoesntHave('comments.author', function (Builder $query) {
$query->where('banned', 1);
})->get();
// --- withCount - count results from a relationship without actually loading them
// will place a {relation}_count column on resulting models
$posts = App\Post::withCount('comments')->get();
foreach ($posts as $post) {
echo $post->comments_count;
}
// count multiple relations and add constraints to the queries
$posts = App\Post::withCount(['votes', 'comments' => function ($query) {
$query->where('content', 'like', 'foo%');
}])->get();
echo $posts[0]->votes_count;
echo $posts[0]->comments_count;
// alias the relationship count result, allow multiple counts on the same relationship
$posts = App\Post::withCount([
'comments',
'comments as pending_comments_count' => function ($query) {
$query->where('approved', false);
}
])->get();
echo $posts[0]->comments_count;
echo $posts[0]->pending_comments_count;
// call withCount after the select method !
$posts = App\Post::select(['title', 'body'])->withCount('comments')->get();
echo $posts[0]->title;
echo $posts[0]->body;
echo $posts[0]->comments_count;
// --- any level far relation
// User belongsToMany Tag - User subscribed to many tags
// Tag belongsToMany Post - Post migh be tagged by many tags
// User hasMany Post - User is the author of many posts
// Post hasMany Comment - each Post has many comments
$user->load([
// it can be any relation type here and any level of nesting
'tags.posts'
// passing $posts by reference, and,
// if it was never declared, it will be created now
=> function ($q) use (&$posts) {
// simply execute the query on the posts table
// and store its result, a collection, in our $posts variable
$posts = $q->get()
// duplicates are likely to occur for a m-m relation,
// remove them with collections unique()
->unique();
}]);
// also:
$user = User::with('tags.posts')->find($someId);
$postsArray = $user->tags->pluck('posts');
$posts = (new Collection($postsArray))->collapse()->unique();
// ...
$user->tags->pluck('posts')->collapse()->unique();
Eager Loading
// avoid multiple queries execution in case:
// ...
public function author() {
return $this->belongsTo('App\Author');
}
//...
$books = App\Book::all();
foreach ($books as $book) {
echo $book->author->name; // query for each book to retrieve the author !!!
}
// --- with - specify which relationships should be loaded
$books = App\Book::with('author')->get();
foreach ($books as $book) {
echo $book->author->name;
}
// only two queries will be executed
// SELECT * FROM books;
// SELECT * FROM authors WHERE id IN (1, 2, 3, 4, 5, ...);
// --- "dot" syntax - eager load nested relationships
// all of the book authors and all of the author personal contacts
$books = App\Book::with('author.contacts')->get();
// --- pecify which columns of the relationship to retrieve
// always include id column and any relevant foreign key columns in the list !!!
$books = App\Book::with('author:id,name')->get();
// --- $with - always load some relationships when retrieving a model
namespace App;
use Illuminate\Database\Eloquent\Model;
class Book extends Model {
// relationships that should always be loaded
protected $with = ['author'];
public function author() {
return $this->belongsTo('App\Author');
}
}
// then, to remove an item from the $with property for a single query
$books = App\Book::without('author')->get();
// --- eager load a relationship and specify additional query conditions
$users = App\User::with(['posts' => function ($query) {
// only eager load posts where title contains the word first
$query->where('title', 'like', '%first%');
}])->get();
// call other query builder methods to further customize the eager loading operation
$users = App\User::with(['posts' => function ($query) {
$query->orderBy('created_at', 'desc');
}])->get();
// limit and take query builder methods may not be used when constraining eager loads !!!
// --- load - lazy eager loading
// eager load a relationship after the parent model has already been retrieved
// dynamically decide whether to load related models
$books = App\Book::all();
if ($someCondition) {
$books->load('author', 'publisher');
}
// set additional query constraints on the eager loading query
$books->load(['author' => function ($query) {
$query->orderBy('published_date', 'asc');
}]);
// --- loadMissing - load a relationship only when it has not already been loaded
public function format(Book $book) {
$book->loadMissing('author');
return [
'name' => $book->name,
'author' => $book->author->name
];
}
// --- loadMorph - eager load a morphTo relationship,
// and nested relationships on the various entities
// that may be returned by that relationship
use Illuminate\Database\Eloquent\Model;
class ActivityFeed extends Model {
// get the parent of the activity feed record
public function parentable() {
return $this->morphTo();
}
}
// Event, Photo, and Post models may create ActivityFeed models
// Event models belong to a Calendar model,
// Photo models are associated with Tag models,
// Post models belong to an Author model
// retrieve ActivityFeed model instances and eager load all parentable models
// and their respective nested relationships
$activities = ActivityFeed::with('parentable')
->get()
->loadMorph('parentable', [
Event::class => ['calendar'],
Photo::class => ['tags'],
Post::class => ['author'],
]);
Inserting/Updating Related Models
// adding new models to relationships
// use also findOrNew, firstOrNew, firstOrCreate and updateOrCreate
// insert a new Comment for a Post model
// insert the Comment directly from the relationship save() method
// instead of manually setting the post_id attribute on the Comment
$comment = new App\Comment(['message' => 'A new comment.']);
$post = App\Post::find(1);
// obtain an instance of the relationship,
// method will automatically add the appropriate post_id
$post->comments()->save($comment);
// save() method accepts an array of additional intermediate table attribute
App\User::find(1)->roles()->save($role, ['expires' => $expires]);
// --- saveMany - save multiple related models
$post = App\Post::find(1);
$post->comments()->saveMany([
new App\Comment(['message' => 'A new comment.']),
new App\Comment(['message' => 'Another comment.']),
]);
// --- push - save model and all of its associated relationships
$post = App\Post::find(1);
$post->comments[0]->message = 'Message';
$post->comments[0]->author->name = 'Author Name';
$post->push();
// --- create
// accept an array of attributes, creates a model, and inserts it into the database
// difference between save is that accepts a plain PHP array
// take in consideration attribute mass assignment rules
$post = App\Post::find(1);
$comment = $post->comments()->create([
'message' => 'A new comment.',
]);
// --- createMany - create multiple related models
// use also findOrNew, firstOrNew, firstOrCreate, updateOrCreate
$post = App\Post::find(1);
$post->comments()->createMany([
[ 'message' => 'A new comment.' ],
[ 'message' => 'Another new comment.' ],
]);
// --- associate
// updating belongsTo relationship, will set the foreign key on the child model
$account = App\Account::find(10);
$user->account()->associate($account);
$user->save();
// --- dissociate
// removing belongsTo relationship, will set the relationship foreign key to null
$user->account()->dissociate();
$user->save();
// --- withDefault
// belongsTo, hasOne, hasOneThrough, and morphOne relationships
// allow you to define a default model that will be returned
// if the given relationship is null
// return an empty App\User model if no user is attached to the post
public function user() {
return $this->belongsTo('App\User')->withDefault();
}
// populate the default model with attributes
// get the author of the post
public function user() {
return $this->belongsTo('App\User')->withDefault([
'name' => 'Guest Author',
]);
}
public function user() {
return $this->belongsTo('App\User')->withDefault(function ($user) {
$user->name = 'Guest Author';
});
}
// --- attach
// inserting a record in the intermediate table that joins the models
// user can have many roles and a role can have many users
// attach a role to a user
$user = App\User::find(1);
$user->roles()->attach($roleId);
// pass an array of additional data to be inserted into the intermediate table
$user->roles()->attach(
$roleId, [
'expires' => $expires
]
);
// arrays of IDs as input
$user->roles()->attach([
1 => ['expires' => $expires],
2 => ['expires' => $expires]
]);
// --- detach - remove a many-to-many relationship record
// delete the appropriate record out of the intermediate table
// both models will remain in the database
// detach a single role from the user
$user->roles()->detach($roleId);
// detach all roles from the user
$user->roles()->detach();
// arrays of IDs as input
$user->roles()->detach([1, 2, 3]);
// --- sync - construct many-to-many associations
// accepts an array of IDs to place on the intermediate table
// any IDs that are NOT in the given array will be removed from the intermediate table
// after operation, only IDs in the given array will exist in the intermediate table
$user->roles()->sync([1, 2, 3]);
// pass additional intermediate table values
$user->roles()->sync([1 => ['expires' => true], 2, 3]);
// --- syncWithoutDetaching - to not detach existing IDs
$user->roles()->syncWithoutDetaching([1, 2, 3]);
// --- toggle - "toggles" the attachment status of the given IDs
// if the given ID is currently attached, it will be detached
// if it is currently detached, it will be attached
$user->roles()->toggle([1, 2, 3]);
// --- updateExistingPivot - update an existing row in pivot table
$user = App\User::find(1);
$user->roles()->updateExistingPivot($roleId, $attributes);
touch(), Touching Parent Timestamps
// --- touch($attribute = null)
// update the current timestamp, updated_at time of $article
$article = Article::find($id);
$article->touch();
// --- touches
// update parent timestamp when child model is updated
// when a Comment model is updated,
// "touch" the updated_at timestamp of the owning Post
namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model {
// all of the relationships to be touched
protected $touches = ['post'];
// get the post that the comment belongs to
public function post() {
return $this->belongsTo('App\Post');
}
}
// ...
$comment = App\Comment::find(1);
$comment->text = 'Edit to this comment!';
$comment->save();
Eloquent Collections
// all multi-result sets returned by Eloquent
// are instances of the Illuminate\Database\Eloquent\Collection object,
// including results retrieved via the get method or accessed via a relationship
// extends the Laravel base collection and inherits its methods
// all collections also serve as iterators,
// loop over them as if they were simple PHP arrays
$users = App\User::where('active', 1)->get();
foreach ($users as $user) {
echo $user->name;
}
// collections are much more powerful than arrays
// expose a variety of map / reduce operations that may be chained
// remove all inactive models and gather the first name for each remaining user:
$users = App\User::all();
$names = $users->reject(function ($user) {
return $user->active === false;
})
->map(function ($user) {
return $user->name;
});
// most Eloquent collection methods return a new instance of an Eloquent collection,
// pluck, keys, zip, collapse, flatten and flip methods return a base collection instance
// if a map operation returns a collection that does not contain any Eloquent models,
// it will be automatically cast to a base collection
// Illuminate\Database\Eloquent\Collection class provides superset of methods
// aids with managing model collections
// most methods return Illuminate\Database\Eloquent\Collection instances
// some methods return a base Illuminate\Support\Collection instance
// --- contains($key, $operator = null, $value = null)
// determine if a given model instance is contained by the collection
// accepts a primary key or a model instance
$users->contains(1);
$users->contains(User::find(1));
// --- diff($items)
// returns all of the models that are not present in the given collection
use App\User;
$users = $users->diff(User::whereIn('id', [1, 2, 3])->get());
// --- except($keys)
// returns all of the models that do not have the given primary keys
$users = $users->except([1, 2, 3]);
// --- find($key)
// finds a model that has a given primary key
// if $key is a model instance, attempt to return a model matching the primary key
// if $key is an array of keys, return all models which match the $keys using whereIn()
$users = User::all();
$user = $users->find(1);
// --- fresh($with = [])
// retrieves a fresh instance of each model in the collection from the database
// any specified relationships will be eager loaded
$users = $users->fresh();
$users = $users->fresh('comments');
// --- intersect($items)
// returns all of the models that are also present in the given collection
use App\User;
$users = $users->intersect(User::whereIn('id', [1, 2, 3])->get());
// --- load($relations)
// eager loads the given relationships for all models in the collection
$users->load('comments', 'posts');
$users->load('comments.author');
// --- loadMissing($relations)
// eager loads the given relationships for all models in the collection
// if the relationships are not already loaded
$users->loadMissing('comments', 'posts');
$users->loadMissing('comments.author');
// --- modelKeys
// returns the primary keys for all models in the collection
$users->modelKeys(); // [1, 2, 3, 4, 5]
// --- makeVisible($attributes)
// makes visible attributes that are typically "hidden" on each model in the collection
$users = $users->makeVisible(['address', 'phone_number']);
// --- makeHidden($attributes)
// hides attributes that are typically "visible" on each model in the collection
$users = $users->makeHidden(['address', 'phone_number']);
// --- only($keys)
// returns all of the models that have the given primary keys
$users = $users->only([1, 2, 3]);
// --- unique($key = null, $strict = false)
// returns all of the unique models in the collection
// any models of the same type with the same primary key as another model
// in the collection are removed
$users = $users->unique();
// --- CUSTOM COLLECTIONS
// override the newCollection method on model
namespace App;
use App\CustomCollection;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
// create a new Eloquent Collection instance
public function newCollection(array $models = []) {
return new CustomCollection($models);
}
}
// once you have defined a newCollection method,
// you will receive an instance of custom collection
// anytime Eloquent returns a Collection instance of that model
// if you would like to use a custom collection for every model in application,
// override the newCollection method on a base model class that is extended by all of models
Attribute Accessors & Mutators
// column value modification on retrieval OR set
// encrypt/decrypt a value stored in the database
// cast date fields to Carbon instances or even cast text fields to JSON
// --- ACCESSOR
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
// get the user first name
public function getFirstNameAttribute($value) {
// manipulate and return original value of the column
// which is passed to the accessor
return ucfirst($value);
}
// also use to return new, computed values from existing attributes
// get the user full name
public function getFullNameAttribute() {
return "{$this->first_name} {$this->last_name}";
}
}
// access the first_name attribute on a model instance
$user = App\User::find(1);
$firstName = $user->first_name;
// append computed values to the array/JSON representations of model
// --- MUTATOR
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
// set the user first name
public function setFirstNameAttribute($value) {
// apply the strtolower function to the name
// and set its resulting value in the internal $attributes array
$this->attributes['first_name'] = strtolower($value);
}
function getFullNameAttribute() {
return $this->attributes['first_name'].' '.$this->attributes['last_name'];
}
}
$user = App\User::find(1);
$user->first_name = 'Sally';
// order the results after we get them, with sortBy
$clients = Client::get()->sortBy('full_name');
// --- Date Mutators
// by default, Eloquent will convert the created_at and updated_at columns
// to instances of Carbon, which extends the PHP DateTime class
// add additional date attributes by setting the $dates property of model
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
// attributes that should be mutated to dates
protected $dates = [
'seen_at',
];
}
// disable the default created_at and updated_at timestamps
// by setting the public $timestamps property of model to false
// when a column is considered a date, you may set its value to:
// UNIX timestamp, date string (Y-m-d), date-time string, or a DateTime/Carbon instance
// date value will be correctly converted and stored in database:
$user = App\User::find(1);
$user->deleted_at = now();
$user->save();
// when retrieving attributes that are listed in $dates property,
// they will automatically be cast to Carbon instances,
// allowing you to use any of Carbon methods on attributes:
$user = App\User::find(1);
return $user->deleted_at->getTimestamp();
// by default, timestamps are formatted as 'Y-m-d H:i:s'
// to customize the timestamp format, set the $dateFormat property on model
// determines how date attributes are stored in the database,
// and their format when the model is serialized to an array or JSON:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model {
// storage format of the model date columns
protected $dateFormat = 'U';
}
// --- Attribute Casting
// $casts property on model provides method of converting attributes to common data types
// should be an array where the key is the name of the attribute being cast
// and the value is the type you wish to cast the column to
// supported cast types are:
// integer, real, float, double, decimal:<digits> (decimal:2),
// string, boolean, object, array, collection, date, datetime, and timestamp
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
// cast the is_admin attribute, as an integer (0 or 1) to a boolean value
protected $casts = [
'is_admin' => 'boolean',
];
}
// will always be cast to a boolean when accessed,
// even if the underlying value is stored in the database as an integer:
$user = App\User::find(1);
if ($user->is_admin) {
// ...
}
// --- Array & JSON Casting
// add "array" cast to database JSON or TEXT field type that contains serialized JSON
// automatically deserialize the attribute to a PHP array
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
// attributes that should be cast to native types
protected $casts = [
'options' => 'array',
];
}
// automatically deserialized from JSON into a PHP array on access of options attribute
// and serialized back into JSON for storage when you set
$user = App\User::find(1);
$options = $user->options;
$options['key'] = 'value';
$user->options = $options;
$user->save();
// --- Date Casting
// when using the date or datetime cast type, specify the date format,
// format will be used when the model is serialized to an array or JSON
// attributes that should be cast to native types
protected $casts = [
'created_at' => 'datetime:Y-m-d',
];
API Resources - transform a model into an array for JSON response
// --- RESOURCE CLASS
// represents a single model that needs to be transformed into JSON
// defines a toArray($request) method which returns the array of attributes to convert
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class User extends JsonResource {
// transform the resource into an array
public function toArray($request) {
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
// use Post resource collection method
// to add the user blog posts to the resource response
'posts' => PostResource::collection($this->posts),
// when - conditional attributes
'secret' => $this->when(Auth::user()->isAdmin(), 'secret-value'),
// calculate the resulting value only if the given condition is true
'secret' => $this->when(Auth::user()->isAdmin(), function () {
return 'secret-value';
}),
// mergeWhen - merging conditional attributes
// should not be used within arrays that mix string and numeric keys
// or within arrays with numeric keys that are not ordered sequentially
$this->mergeWhen(Auth::user()->isAdmin(), [
'first-secret' => 'value',
'second-secret' => 'value',
]),
// whenLoaded - conditionally load a relationship, accepts the name
// allows controller to decide which relationships should be loaded on the model
// resource can easily include them only when they have actually been loaded
// if the relationship has not been loaded,
// the posts key will be removed from the resource response entirely
'posts' => PostResource::collection($this->whenLoaded('posts')),
// whenPivotLoaded - conditionally include data from the intermediate tables
// of many-to-many relationships
'expires_at' => $this->whenPivotLoaded('role_user', function () {
return $this->pivot->expires_at;
}),
// whenPivotLoadedAs - if intermediate table is using an accessor
'expires_at' => $this->whenPivotLoadedAs('subscription', 'role_user', function () {
return $this->subscription->expires_at;
}),
];
}
// Laravel resets keys, they are in simple numerical order
// return a resource collection from a route and preserve collection keys
public $preserveKeys = true;
}
// return resource from route or controller
use App\User;
use App\Http\Resources\User as UserResource;
Route::get('/user', function () {
return new UserResource(User::find(1));
});
// with preserved collection keys
Route::get('/user', function () {
return UserResource::collection(User::all()->keyBy->id);
});
// collection() method for creating the resource instance in route or controller
// resource collection on the fly
Route::get('/user', function () {
return UserResource::collection(User::all());
});
// --- RESOURCE COLLECTIONS
// allow any addition of meta data needed to be returned with the collection
// create a dedicated resource to represent the collection:
// php artisan make:resource UserCollection
// $this->collection is automatically populated
// with the result of mapping each item of the collection to its singular resource class
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
// singular resource class is assumed to be the collection class name
// without the trailing Collection string
// attempt to map the given user instances into the User resource
class UserCollection extends ResourceCollection {
// transform the resource collection into an array
public function toArray($request) {
return parent::toArray($request);
// return additional meta data about a resource
// any additional links will be merged with the links provided by the paginator
return [
'data' => $this->collection,
'links' => [
'self' => 'link-value',
],
];
}
// wrapping nested resources
// transform the resource collection into an array
public function toArray($request) {
return ['data' => $this->collection];
}
// with - get additional data that should be returned with the resource array
// only when the resource is the outer-most resource being rendered
public function with($request) {
return [
'meta' => [
'key' => 'value',
],
];
}
// customize behavior
// resource that this resource collects
public $collects = 'App\Http\Resources\Member';
}
// resource collection returned from a route or controller
use App\User;
use App\Http\Resources\UserCollection;
Route::get('/users', function () {
return new UserCollection(User::all());
});
// --- ---
// additional - add top-level data when constructing resource instances
// in route or controller, available on all resources,
// accepts an array of data that should be added to the resource response
return (new UserCollection(User::all()->load('roles')))
->additional(['meta' => [
'key' => 'value',
]]);
// --- CUSTOMIZE HTTP RESPONSE BEFORE IT IS SENT TO THE CLIENT
// chain the response method onto the resource
// will return an Illuminate\Http\Response instance,
// allowing you full control of the response headers
use App\User;
use App\Http\Resources\User as UserResource;
Route::get('/user', function () {
return (new UserResource(User::find(1)))
->response()
->header('X-Value', 'True');
});
// OR, define a withResponse method within the resource itself
// called when the resource is returned as the outer-most resource in a response
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class User extends JsonResource {
// transform the resource into an array
public function toArray($request) {
return [
'id' => $this->id,
];
}
// customize the outgoing response for the resource
public function withResponse($request, $response) {
$response->header('X-Value', 'True');
}
}
// pass a paginator instance to the collection method of a resource
// or to a custom resource collection
use App\User;
use App\Http\Resources\UserCollection;
Route::get('/users', function () {
return new UserCollection(User::paginate());
});
// --- DISABLE WRAPPING OF THE OUTER-MOST RESOURCE
// avoid such form: {"data":[{"id":1,"name":...
// use the withoutWrapping method on the base resource class
// call from AppServiceProvider or another service provider loaded on every request
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Http\Resources\Json\JsonResource;
class AppServiceProvider extends ServiceProvider {
// register bindings in the container
public function register() {
// ...
}
// bootstrap any application services
public function boot() {
JsonResource::withoutWrapping();
}
}
// ---
{
"data": [
{
"id": 1, ... }
],
"links":{
"first": "http://example.com/pagination?page=1",
"last": "http://example.com/pagination?page=1",
"prev": null,
"next": null
},
"meta":{
"current_page": 1,
"from": 1,
"last_page": 1,
"path": "http://example.com/pagination",
"per_page": 15,
"to": 10,
"total": 10
}
}
Serialization - convert models and relationships to arrays or JSON
// --- toArray - convert a model and its loaded relationships to an array
// is recursive, cast any Illuminate\Contracts\Support\Arrayable attributes to an array
$user = App\User::with('roles')->first();
return $user->toArray();
// convert entire collections of models to arrays
$users = App\User::all();
return $users->toArray();
// --- attributesToArray - convert only a model attributes to an array
$user = App\User::first();
return $user->attributesToArray();
// --- toJson - convert a model to JSON
$user = App\User::find(1);
return $user->toJson();
return $user->toJson(JSON_PRETTY_PRINT);
// cast a model or collection to a string,
// will automatically call the toJson method on the model or collection
$user = App\User::find(1);
return (string) $user;
// return Eloquent objects directly from routes or controllers
// since models and collections are converted to JSON when cast to a string
Route::get('users', function () {
return App\User::all();
});
// relationship JSON attribute will be "snake_case"
// --- $hidden - hiding attributes from json
// when hiding relationships, use relationship method name
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
// attributes that should be hidden for arrays
protected $hidden = ['password'];
}
// --- $visible
// white-list attributes that should be included in array and JSON representation
// all other attributes will be hidden
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
// attributes that should be visible in arrays
protected $visible = ['first_name', 'last_name'];
}
// --- makeVisible , makeHidden
// temporarily modifying attribute visibility
return $user->makeVisible('attribute')->toArray();
return $user->makeHidden('attribute')->toArray();
// --- $appends - add attributes that do not have a corresponding column in database
// first define an accessor
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
// get the administrator flag for the user
public function getIsAdminAttribute() {
return $this->attributes['admin'] == 'yes';
}
}
// then add the attribute name to the appends property on the model
// attribute names are in "snake case", even accessor is in "camel case"
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
// accessors to append to the model array form
// attribute will be included in both the model array and JSON representations
// will respect the $visible and $hidden settings
protected $appends = ['is_admin'];
}
// --- append , setAppends
// instruct a single model instance to append attributes
// or, override the entire array of appended properties
return $user->append('is_admin')->toArray();
return $user->setAppends(['is_admin'])->toArray();
// --- $casts - customizing date format per attribute
protected $casts = [
'birthday' => 'date:Y-m-d',
'joined_at' => 'datetime:Y-m-d H:00',
];
// --- Carbon::serializeUsing
// returns string representation of the date for JSON
namespace App\Providers;
use Illuminate\Support\Carbon;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider {
// register bindings in the container
public function register() {
// ...
}
// bootstrap any application services
public function boot() {
Carbon::serializeUsing(function ($carbon) {
return $carbon->format('U');
});
}
}
Collections
- Illuminate\Support\Collection, wrapper for working with arrays of data
- collections are immutable, every Collection method returns an entirely new Collection instance
- results of Eloquent queries are always returned as Collection instances, use empty collection (collect()) for later variable fulfilling in scenarios
- declare collection macros in a service provider
- higher order messages - short-cuts for performing common actions on collections
- each message can be accessed as a dynamic property on a collection instance
- methods that provide are: average , avg , contains , each , every , filter , first , flatMap , groupBy , keyBy , map , max , min , partition , reject , some , sortBy , sortByDesc , sum , unique
// --- collect - creating a collection
$collection = collect([1, 2, 3]);
// --- macro - extending collections
// add additional methods to the Collection class at run time
// add a toUpper method to the Collection class:
use Illuminate\Support\Str;
Collection::macro('toUpper', function () {
return $this->map(function ($value) {
return Str::upper($value);
});
});
$collection = collect(['first', 'second']);
$upper = $collection->toUpper();
// ['FIRST', 'SECOND']
// --- chain collection methods
// create a new collection instance from the array
// run the strtoupper function on each element
// then remove all empty elements
$collection = collect(['taylor', 'abigail', null])
->map(function ($name) {
return strtoupper($name);
})
->reject(function ($name) {
return empty($name);
});
Methods
all - returns the underlying array represented by the collection
collect([1, 2, 3])->all(); // [1, 2, 3]
avg , max , min , sum , median
$average = collect([
['foo' => 10],
['foo' => 10],
['foo' => 20],
['foo' => 40]
])->avg('foo'); // 20
$average = collect([1, 1, 2, 4])->avg(); // 2
// --- max - maximum value
$max = collect([['foo' => 10], ['foo' => 20]])->max('foo'); // 20
$max = collect([1, 2, 3, 4, 5])->max(); // 5
// --- min - minimum value
$min = collect([['foo' => 10], ['foo' => 20]])->min('foo'); // 10
$min = collect([1, 2, 3, 4, 5])->min(); // 1
// --- sum - sum of all items
collect([1, 2, 3, 4, 5])->sum(); // 15
// if the collection contains nested arrays or objects,
// pass a key to use for determining which values to sum
$collection = collect([
['name' => 'JavaScript: The Good Parts', 'pages' => 176],
['name' => 'JavaScript: The Definitive Guide', 'pages' => 1096],
]);
$collection->sum('pages'); // 1272
// pass own callback to determine which values of the collection to sum
$collection = collect([
['name' => 'Chair', 'colors' => ['Black']],
['name' => 'Desk', 'colors' => ['Black', 'Mahogany']],
['name' => 'Bookcase', 'colors' => ['Red', 'Beige', 'Brown']],
]);
$collection->sum(function ($product) {
return count($product['colors']);
}); // 6
// --- median - median value
$median = collect([
['foo' => 10],
['foo' => 10],
['foo' => 20],
['foo' => 40]
])->median('foo'); // 15
$median = collect([1, 1, 2, 4])->median(); // 1.5
chunk - breaks collection into smaller collections of a given size
$collection = collect([1, 2, 3, 4, 5, 6, 7]);
$chunks = $collection->chunk(4);
$chunks->toArray();
// [[1, 2, 3, 4], [5, 6, 7]]
// useful in views when working with a grid system such as Bootstrap
@foreach ($products->chunk(3) as $chunk)
<div class="row">
@foreach ($chunk as $product)
<div class="col-xs-4">{{ $product->name }}</div>
@endforeach
</div>
@endforeach
collapse - collapses a collection of arrays into a single, flat
$collection = collect([[1, 2, 3], [4, 5, 6], [7, 8, 9]]);
$collapsed = $collection->collapse();
$collapsed->all();
// [1, 2, 3, 4, 5, 6, 7, 8, 9]
combine - combines the values
// combines as keys, with the values of another array or collection
$collection = collect(['name', 'age']);
$combined = $collection->combine(['George', 29]);
$combined->all();
// ['name' => 'George', 'age' => 29]
concat - appends array or collection values onto the end
$collection = collect(['John Doe']);
$concatenated = $collection->concat(['Jane Doe'])->concat(['name' => 'Johnny Doe']);
$concatenated->all();
// ['John Doe', 'Jane Doe', 'Johnny Doe']
contains , some , containsStrict - whether the collection contains a given item
$collection = collect(['name' => 'Desk', 'price' => 100]);
$collection->contains('Desk'); // true
$collection->contains('New York'); // false
// pass a key/value to determine if it exists in the collection
$collection = collect([
['product' => 'Desk', 'price' => 200],
['product' => 'Chair', 'price' => 100],
]);
$collection->contains('product', 'Bookcase'); // false
// pass a callback to the contains method to perform own truth test
$collection = collect([1, 2, 3, 4, 5]);
$collection->contains(function ($value, $key) {
return $value > 5;
}); // false
// some - alias for the contains method
// contains method uses "loose" comparisons when checking item values,
// string with an integer value will be considered equal to an integer of the same value
// use the containsStrict method to filter using "strict" comparisons
count , countBy
$collection = collect([1, 2, 3, 4]);
$collection->count(); // 4
// --- countBy - counts the occurrences of values
// by default, counts occurrences of every element
$collection = collect([1, 2, 2, 2, 3]);
$counted = $collection->countBy();
$counted->all(); // [1 => 1, 2 => 3, 3 => 1]
// pass a callback to count all items by a custom value
$collection = collect([
'alice@gmail.com',
'bob@yahoo.com',
'carlos@gmail.com'
]);
$counted = $collection->countBy(function ($email) {
return substr(strrchr($email, "@"), 1);
});
$counted->all(); // ['gmail.com' => 2, 'yahoo.com' => 1]
crossJoin - Cartesian product with all possible permutations
$collection = collect([1, 2]);
$matrix = $collection->crossJoin(['a', 'b']);
$matrix->all();
/*
[
[1, 'a'],
[1, 'b'],
[2, 'a'],
[2, 'b'],
]
*/
$collection = collect([1, 2]);
$matrix = $collection->crossJoin(['a', 'b'], ['I', 'II']);
$matrix->all();
/*
[
[1, 'a', 'I'],
[1, 'a', 'II'],
[1, 'b', 'I'],
[1, 'b', 'II'],
[2, 'a', 'I'],
[2, 'a', 'II'],
[2, 'b', 'I'],
[2, 'b', 'II'],
]
*/
dd - dump items and ends execution of the script
$collection = collect(['John Doe', 'Jane Doe']);
$collection->dd();
/*
Collection {
#items: array:2 [
0 => "John Doe"
1 => "Jane Doe"
]
}
*/
// stop executing the script, use the dump method
dump - dump items
$collection = collect(['John Doe', 'Jane Doe']);
$collection->dump();
/*
Collection {
#items: array:2 [
0 => "John Doe"
1 => "Jane Doe"
]
}
*/
// to stop executing the script after dumping the collection, use the dd method
diff , diffAssoc , diffKeys
// compares collection against collection or plain PHP array
// returns values in the original collection
// that are not present in collection
$collection = collect([1, 2, 3, 4, 5]);
$diff = $collection->diff([2, 4, 6, 8]);
$diff->all(); // [1, 3, 5]
// --- diffAssoc
// return the key/value pairs in the original collection
// that are not present in the given collection
$collection = collect([
'color' => 'orange',
'type' => 'fruit',
'remain' => 6
]);
$diff = $collection->diffAssoc([
'color' => 'yellow',
'type' => 'fruit',
'remain' => 3,
'used' => 6
]);
$diff->all(); // ['color' => 'orange', 'remain' => 6]
// --- diffKeys
// compares based on keys
// will return the key/value pairs in the original collection
// that are not present in the given collection
$collection = collect([
'one' => 10,
'two' => 20,
'three' => 30,
'four' => 40,
'five' => 50,
]);
$diff = $collection->diffKeys([
'two' => 2,
'four' => 4,
'six' => 6,
'eight' => 8,
]);
$diff->all(); // ['one' => 10, 'three' => 30, 'five' => 50]
duplicates - retrieves and returns duplicate values
$collection = collect(['a', 'b', 'a', 'c', 'b']);
$collection->duplicates(); // [ 2 => 'a', 4 => 'b' ]
each , eachSpread - iterates over the items(nested) and pass each item to a callback
$collection->each(function ($item, $key) {
// ...
});
// stop iterating through the items, return false from callback
$collection->each(function ($item, $key) {
if ( /* some condition */ ) {
return false;
}
});
// --- eachSpread - iterate and pass each nested item value into the callback
$collection = collect([['John Doe', 35], ['Jane Doe', 33]]);
$collection->eachSpread(function ($name, $age) {
// ...
});
// stop iterating through the items by returning false from the callback
$collection->eachSpread(function ($name, $age) {
return false;
});
every - verify that all elements pass a given truth test
collect([1, 2, 3, 4])->every(function ($value, $key) {
return $value > 2;
}); // false
// if the collection is empty, every will return true
$collection = collect([]);
$collection->every(function($value, $key) {
return $value > 2;
}); // true
except , only - exclude/get items with the specified keys
$collection = collect([
'product_id' => 1,
'price' => 100,
'discount' => false
]);
$filtered = $collection->except(['price', 'discount']);
$filtered->all(); // ['product_id' => 1]
$collection = collect([
'product_id' => 1,
'name' => 'Desk',
'price' => 100,
'discount' => false
]);
$filtered = $collection->only(['product_id', 'name']);
$filtered->all(); // ['product_id' => 1, 'name' => 'Desk']
filter , reject - filter using callback
$collection = collect([1, 2, 3, 4]);
$filtered = $collection->filter(function ($value, $key) {
return $value > 2;
});
$filtered->all(); // [3, 4]
// if no callback is supplied,
// all entries that are equivalent to false will be removed
$collection = collect([1, 2, 3, null, false, '', 0, []]);
$collection->filter()->all(); // [1, 2, 3]
// inverse callback should return true if the item should be removed
$collection = collect([1, 2, 3, 4]);
$filtered = $collection->reject(function ($value, $key) {
return $value > 2;
});
$filtered->all(); // [1, 2]
first , firstWhere - element that passes test OR with key/value pair
collect([1, 2, 3, 4])->first(function ($value, $key) {
return $value > 2;
}); // 3
// call with no arguments to get the first element in the collection
// if the collection is empty, null is returned
collect([1, 2, 3, 4])->first(); // 1
// --- firstWhere - first element with key/value pair
$collection = collect([
['name' => 'Regena', 'age' => null],
['name' => 'Linda', 'age' => 14],
['name' => 'Diego', 'age' => 23],
['name' => 'Linda', 'age' => 84],
]);
$collection->firstWhere('name', 'Linda'); // ['name' => 'Linda', 'age' => 14]
// call with an operator
$collection->firstWhere('age', '>=', 18); // ['name' => 'Diego', 'age' => 23]
// pass one argument to the firstWhere method
$collection->firstWhere('age'); // ['name' => 'Linda', 'age' => 14]
flatten - multi-dimensional collection into a single dimension
$collection = collect(['name' => 'taylor', 'languages' => ['php', 'javascript']]);
$flattened = $collection->flatten();
$flattened->all(); // ['taylor', 'php', 'javascript'];
// pass the function a "depth" argument:
$collection = collect([
'Apple' => [ ['name' => 'iPhone 6S', 'brand' => 'Apple'] ],
'Samsung' => [ ['name' => 'Galaxy S7', 'brand' => 'Samsung'] ],
]);
$products = $collection->flatten(1);
$products->values()->all();
/*
[
['name' => 'iPhone 6S', 'brand' => 'Apple'],
['name' => 'Galaxy S7', 'brand' => 'Samsung'],
]
*/
flip - swap keys with their corresponding values
$collection = collect(['name' => 'taylor', 'framework' => 'laravel']);
$flipped = $collection->flip();
$flipped->all(); // ['taylor' => 'name', 'laravel' => 'framework']
forget - remove an item by its key
$collection = collect(['name' => 'taylor', 'framework' => 'laravel']);
$collection->forget('name');
$collection->all(); // ['framework' => 'laravel']
// unlike most other methods, does not return a new modified collection
// modifies the collection it is called on
forPage - return items that would be present on a given page number
$collection = collect([1, 2, 3, 4, 5, 6, 7, 8, 9]);
$chunk = $collection->forPage(2, 3); // page number and the number of items
$chunk->all(); // [4, 5, 6]
// forPageBeforeId(int $perPage = 15, int|null $lastId = 0, string $column = 'id')
get - item at a given key
// if the key does not exist, null is returned
$collection = collect(['name' => 'taylor', 'framework' => 'laravel']);
$value = $collection->get('name'); // taylor
// pass a default value as the second argument
$collection = collect(['name' => 'taylor', 'framework' => 'laravel']);
$value = $collection->get('foo', 'default-value'); // default-value
// pass a callback
// result will be returned if the specified key does not exist
$collection->get('email', function () {
return 'default-value';
}); // default-value
groupBy - group items by a given key
$collection = collect([
['account_id' => 'account-x10', 'product' => 'Chair'],
['account_id' => 'account-x10', 'product' => 'Bookcase'],
['account_id' => 'account-x11', 'product' => 'Desk'],
]);
$grouped = $collection->groupBy('account_id');
$grouped->toArray();
/*
[
'account-x10' => [
['account_id' => 'account-x10', 'product' => 'Chair'],
['account_id' => 'account-x10', 'product' => 'Bookcase'],
],
'account-x11' => [
['account_id' => 'account-x11', 'product' => 'Desk'],
],
]
*/
// pass a callback
// should return the value you wish to key the group by
$grouped = $collection->groupBy(function ($item, $key) {
return substr($item['account_id'], -3);
});
$grouped->toArray();
/*
[
'x10' => [
['account_id' => 'account-x10', 'product' => 'Chair'],
['account_id' => 'account-x10', 'product' => 'Bookcase'],
],
'x11' => [
['account_id' => 'account-x11', 'product' => 'Desk'],
],
]
*/
// multiple grouping criteria passed as an array
// each array element will be applied to the corresponding level
// within a multi-dimensional array
$data = new Collection([
10 => ['user' => 1, 'skill' => 1, 'roles' => ['Role_1', 'Role_3']],
20 => ['user' => 2, 'skill' => 1, 'roles' => ['Role_1', 'Role_2']],
30 => ['user' => 3, 'skill' => 2, 'roles' => ['Role_1']],
40 => ['user' => 4, 'skill' => 2, 'roles' => ['Role_2']],
]);
$result = $data->groupBy([
'skill',
function ($item) {
return $item['roles'];
},
], $preserveKeys = true);
/*
[
1 => [
'Role_1' => [
10 => ['user' => 1, 'skill' => 1, 'roles' => ['Role_1', 'Role_3']],
20 => ['user' => 2, 'skill' => 1, 'roles' => ['Role_1', 'Role_2']],
],
'Role_2' => [
20 => ['user' => 2, 'skill' => 1, 'roles' => ['Role_1', 'Role_2']],
],
'Role_3' => [
10 => ['user' => 1, 'skill' => 1, 'roles' => ['Role_1', 'Role_3']],
],
],
2 => [
'Role_1' => [
30 => ['user' => 3, 'skill' => 2, 'roles' => ['Role_1']],
],
'Role_2' => [
40 => ['user' => 4, 'skill' => 2, 'roles' => ['Role_2']],
],
],
];
*/
has - determines if a given key exists
$collection = collect(['account_id' => 1, 'product' => 'Desk', 'amount' => 5]);
$collection->has('product'); // true
$collection->has(['product', 'amount']); // true
$collection->has(['amount', 'price']); // false
implode - join items
// arguments depend on the type of items in the collection
// if the collection contains arrays or objects,
// pass the key of the attributes you wish to join,
// and the "glue" string you wish to place between the values
$collection = collect([
['account_id' => 1, 'product' => 'Desk'],
['account_id' => 2, 'product' => 'Chair'],
]);
$collection->implode('product', ', '); // Desk, Chair
// if the collection contains simple strings or numeric values,
// pass the "glue" as the only argument to the method:
collect([1, 2, 3, 4, 5])->implode('-'); // '1-2-3-4-5'
intersect , intersectByKeys - removes any values/keys that are not present
// resulting collection will preserve the original collection keys
$collection = collect(['Desk', 'Sofa', 'Chair']);
$intersect = $collection->intersect(['Desk', 'Chair', 'Bookcase']);
$intersect->all(); // [0 => 'Desk', 2 => 'Chair']
// --- intersectByKeys
$collection = collect([
'serial' => 'UX301', 'type' => 'screen', 'year' => 2009
]);
$intersect = $collection->intersectByKeys([
'reference' => 'UX404', 'type' => 'tab', 'year' => 2011
]);
$intersect->all(); // ['type' => 'screen', 'year' => 2009]
isEmpty , isNotEmpty - true, if collection is(not) empty; otherwise, false
collect([])->isEmpty(); // true
collect([])->isNotEmpty(); // false
join - joins values with a string
collect(['a', 'b', 'c'])->join(', '); // 'a, b, c'
collect(['a', 'b', 'c'])->join(', ', ', and '); // 'a, b, and c'
collect(['a', 'b'])->join(', ', ' and '); // 'a and b'
collect(['a'])->join(', ', ' and '); // 'a'
collect([])->join(', ', ' and '); // ''
keyBy - keys collection by key
// if multiple items have the same key,
// only the last one will appear in the new collection
$collection = collect([
['product_id' => 'prod-100', 'name' => 'Desk'],
['product_id' => 'prod-200', 'name' => 'Chair'],
]);
$keyed = $collection->keyBy('product_id');
$keyed->all();
/*
[
'prod-100' => ['product_id' => 'prod-100', 'name' => 'Desk'],
'prod-200' => ['product_id' => 'prod-200', 'name' => 'Chair'],
]
*/
// pass a callback to the method
// should return the value to key the collection by
$keyed = $collection->keyBy(function ($item) {
return strtoupper($item['product_id']);
});
$keyed->all();
/*
[
'PROD-100' => ['product_id' => 'prod-100', 'name' => 'Desk'],
'PROD-200' => ['product_id' => 'prod-200', 'name' => 'Chair'],
]
*/
keys - all keys
$collection = collect([
'prod-100' => ['product_id' => 'prod-100', 'name' => 'Desk'],
'prod-200' => ['product_id' => 'prod-200', 'name' => 'Chair'],
]);
$keys = $collection->keys();
$keys->all(); // ['prod-100', 'prod-200']
last - last element that passes a given truth test
collect([1, 2, 3, 4])->last(function ($value, $key) {
return $value < 3;
}); // 2
// call with no arguments to get the last element in the collection
// if the collection is empty, null is returned:
collect([1, 2, 3, 4])->last(); // 4
map , flatMap , mapInto , mapSpread , mapToGroups , mapWithKeys
// callback is free to modify the item and return it,
// forming a new collection of modified items
$collection = collect([1, 2, 3, 4, 5]);
$multiplied = $collection->map(function ($item, $key) {
return $item * 2;
});
$multiplied->all(); // [2, 4, 6, 8, 10]
// like most other collection methods,returns a new collection instance
// does not modify the collection it is called on
// use transform method to transform the original collection
// flatMap - array is flattened by a level
$collection = collect([
['name' => 'Sally'],
['school' => 'Arkansas'],
['age' => 28]
]);
$flattened = $collection->flatMap(function ($values) {
return array_map('strtoupper', $values);
});
$flattened->all();
// ['name' => 'SALLY', 'school' => 'ARKANSAS', 'age' => '28'];
// mapInto - iterate and create new instance of class
// pass the value into the constructor
class Currency {
// create a new currency instance
function __construct(string $code) {
$this->code = $code;
}
}
$collection = collect(['USD', 'EUR', 'GBP']);
$currencies = $collection->mapInto(Currency::class);
$currencies->all();
// [Currency('USD'), Currency('EUR'), Currency('GBP')]
// mapSpread - iterates items passing value into the callback
// callback is free to modify the item and return it,
// forming a new collection of modified items
$collection = collect([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
$chunks = $collection->chunk(2);
$sequence = $chunks->mapSpread(function ($even, $odd) {
return $even + $odd;
});
$sequence->all(); // [1, 5, 9, 13, 17]
// mapToGroups - groups items by callback
// callback should return an associative array containing a single key/value pair,
// forming a new collection of grouped values
$collection = collect([
[ 'name' => 'John Doe', 'department' => 'Sales' ],
[ 'name' => 'Jane Doe', 'department' => 'Sales' ],
[ 'name' => 'Johnny Doe', 'department' => 'Marketing' ]
]);
$grouped = $collection->mapToGroups(function ($item, $key) {
return [$item['department'] => $item['name']];
});
$grouped->toArray();
/*
[
'Sales' => ['John Doe', 'Jane Doe'],
'Marketing' => ['Johnny Doe'],
]
*/
$grouped->get('Sales')->all(); // ['John Doe', 'Jane Doe']
// mapWithKeys
// callback should return an associative array containing a single key/value pair
$collection = collect([
[
'name' => 'John',
'department' => 'Sales',
'email' => 'john@example.com'
],
[
'name' => 'Jane',
'department' => 'Marketing',
'email' => 'jane@example.com'
]
]);
$keyed = $collection->mapWithKeys(function ($item) {
return [$item['email'] => $item['name']];
});
$keyed->all();
/*
[
'john@example.com' => 'John',
'jane@example.com' => 'Jane',
]
*/
merge - merges with the original collection
// if a string key in the given items matches a string key in the original collection,
// the given items value will overwrite the value in the original collection
$collection = collect(['product_id' => 1, 'price' => 100]);
$merged = $collection->merge(['price' => 200, 'discount' => false]);
$merged->all(); // ['product_id' => 1, 'price' => 200, 'discount' => false]
// if the given items keys are numeric,
// the values will be appended to the end
$collection = collect(['Desk', 'Chair']);
$merged = $collection->merge(['Bookcase', 'Door']);
$merged->all(); // ['Desk', 'Chair', 'Bookcase', 'Door']
mode - mode value
$mode = collect([
['foo' => 10],
['foo' => 10],
['foo' => 20],
['foo' => 40]
])->mode('foo'); // [10]
$mode = collect([1, 1, 2, 4])->mode(); // [1]
nth - every n-th element
$collection = collect(['a', 'b', 'c', 'd', 'e', 'f']);
$collection->nth(4); // ['a', 'e']
// pass an offset as the second argument
$collection->nth(4, 1); // ['b', 'f']
pad - fill array with the value until specified size
// behaves like the array_pad PHP function
// to pad to the left, specify a negative size
// no padding will take place if the absolute value of the given size
// is less than or equal to the length of the array
$collection = collect(['A', 'B', 'C']);
$filtered = $collection->pad(5, 0);
$filtered->all(); // ['A', 'B', 'C', 0, 0]
$filtered = $collection->pad(-5, 0);
$filtered->all(); // [0, 0, 'A', 'B', 'C']
partition - separate elements that pass a given truth test
// may be combined with the list PHP function
$collection = collect([1, 2, 3, 4, 5, 6]);
list($underThree, $equalOrAboveThree) =
$collection->partition(function ($i) {
return $i < 3;
});
$underThree->all(); // [1, 2]
$equalOrAboveThree->all(); // [3, 4, 5, 6]
pipe - pass collection to callback and return the result
$collection = collect([1, 2, 3]);
$piped = $collection->pipe(function ($collection) {
return $collection->sum();
}); // 6
pluck - retrieves all of the values for a given key
$collection = collect([
['product_id' => 'prod-100', 'name' => 'Desk'],
['product_id' => 'prod-200', 'name' => 'Chair'],
]);
$plucked = $collection->pluck('name');
$plucked->all(); // ['Desk', 'Chair']
// specify how you wish the resulting collection to be keyed
$plucked = $collection->pluck('name', 'product_id');
$plucked->all(); // ['prod-100' => 'Desk', 'prod-200' => 'Chair']
// if duplicate keys exist,
// last matching element will be inserted into the plucked collection
$collection = collect([
['brand' => 'Tesla', 'color' => 'red'],
['brand' => 'Pagani', 'color' => 'white'],
['brand' => 'Tesla', 'color' => 'black'],
['brand' => 'Pagani', 'color' => 'orange'],
]);
$plucked = $collection->pluck('color', 'brand');
$plucked->all(); // ['Tesla' => 'black', 'Pagani' => 'orange']
prepend - adds an item to the beginning
$collection = collect([1, 2, 3, 4, 5]);
$collection->prepend(0);
$collection->all(); // [0, 1, 2, 3, 4, 5]
// set the key of the prepended item
$collection = collect(['one' => 1, 'two' => 2]);
$collection->prepend(0, 'zero');
$collection->all(); // ['zero' => 0, 'one' => 1, 'two' => 2]
pull , push , shift , pop
// --- pull - removes and returns an item by its key
$collection = collect(['product_id' => 'prod-100', 'name' => 'Desk']);
$collection->pull('name'); // 'Desk'
$collection->all(); // ['product_id' => 'prod-100']
// --- push - appends an item to the end
$collection = collect([1, 2, 3, 4]);
$collection->push(5);
$collection->all(); // [1, 2, 3, 4, 5]
// --- shift - removes and returns first item
$collection = collect([1, 2, 3, 4, 5]);
$collection->shift(); // 1
$collection->all(); // [2, 3, 4, 5]
// --- pop - removes and returns last item
$collection = collect([1, 2, 3, 4, 5]);
$collection->pop(); // 5
$collection->all(); // [1, 2, 3, 4]
put - sets the given key and value
$collection = collect(['product_id' => 1, 'name' => 'Desk']);
$collection->put('price', 100);
$collection->all(); // ['product_id' => 1, 'name' => 'Desk', 'price' => 100]
random - returns a random item
$collection = collect([1, 2, 3, 4, 5]);
$collection->random(); // 4 - (retrieved randomly)
// pass an integer to specify how many items you would like to randomly retrieve
// collection of items is always returned when explicitly passing the number
$random = $collection->random(3);
$random->all();// [2, 4, 5] - (retrieved randomly)
// if the Collection has fewer items than requested
// method will throw an InvalidArgumentException
reduce, reduceSpread - reduce collection
// --- reduce - reduce collection to a single value
// will pass the result of each iteration into the subsequent iteration
$collection = collect([1, 2, 3]);
$total = $collection->reduce(function ($carry, $item) {
return $carry + $item;
}); // 6
// value for $carry on the first iteration is null;
// specify its initial value by passing a second argument
$collection->reduce(function ($carry, $item) {
return $carry + $item;
}, 4); // 10
// --- reduceSpread - reduce collection to an array of values,
// passing the results of each iteration into the subsequent iteration
// is similar to the reduce method but can accept multiple initial values
[$creditsRemaining, $batch] = Image::where('status', 'unprocessed')
->get()
->reduceSpread(function ($creditsRemaining, $batch, $image) {
if ($creditsRemaining >= $image->creditsRequired()) {
$batch->push($image);
$creditsRemaining -= $image->creditsRequired();
}
return [$creditsRemaining, $batch];
}, $creditsAvailable, collect());
reverse - reverses the order of items
// preserves original keys
$collection = collect(['a', 'b', 'c', 'd', 'e']);
$reversed = $collection->reverse();
$reversed->all();
/*
[
4 => 'e',
3 => 'd',
2 => 'c',
1 => 'b',
0 => 'a',
]
*/
search - searches for value and returns its key, otherwise false
$collection = collect([2, 4, 6, 8]);
$collection->search(4); // 1
// search is done using a "loose" comparison,
// string with an integer value will be considered equal to an integer of the same value
// use "strict" comparison, pass true as the second argument
$collection->search('4', true); // false
// alternatively, pass own callback to search for the first item
// that passes truth test
$collection->search(function ($item, $key) {
return $item > 5;
}); // 2
shuffle - randomly shuffle items
$collection = collect([1, 2, 3, 4, 5]);
$shuffled = $collection->shuffle();
$shuffled->all(); // [3, 2, 5, 1, 4] - (generated randomly)
slice - slice starting at the given index
$collection = collect([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
$slice = $collection->slice(4);
$slice->all(); // [5, 6, 7, 8, 9, 10]
// limit size of the returned slice
$slice = $collection->slice(4, 2);
$slice->all(); // [5, 6]
// returned slice will preserve keys by default
// use the values method to avoid this
sort , sortBy , sortByDesc , sortKeys , sortKeysDesc
// sorted collection keeps the original array keys
// use the values method to reset the keys to consecutively numbered indexes
// --- sort
$collection = collect([5, 3, 1, 2, 4]);
$sorted = $collection->sort();
$sorted->values()->all(); // [1, 2, 3, 4, 5]
// pass a callback for advanced sorting
// calls PHP uasort
// --- sortBy
$collection = collect([
['name' => 'Desk', 'price' => 200],
['name' => 'Chair', 'price' => 100],
['name' => 'Bookcase', 'price' => 150],
]);
$sorted = $collection->sortBy('price');
$sorted->values()->all();
/*
[
['name' => 'Chair', 'price' => 100],
['name' => 'Bookcase', 'price' => 150],
['name' => 'Desk', 'price' => 200],
]
*/
// pass own callback to determine how to sort the collection values
$collection = collect([
['name' => 'Desk', 'colors' => ['Black', 'Mahogany']],
['name' => 'Chair', 'colors' => ['Black']],
['name' => 'Bookcase', 'colors' => ['Red', 'Beige', 'Brown']],
]);
$sorted = $collection->sortBy(function ($product, $key) {
return count($product['colors']);
});
$sorted->values()->all();
/*
[
['name' => 'Chair', 'colors' => ['Black']],
['name' => 'Desk', 'colors' => ['Black', 'Mahogany']],
['name' => 'Bookcase', 'colors' => ['Red', 'Beige', 'Brown']],
]
*/
// --- sortByDesc
// has the same signature as the sortBy, will sort in the opposite order
// --- sortKeys - sorts by the keys of the underlying associative array
$collection = collect([
'id' => 22345,
'first' => 'John',
'last' => 'Doe',
]);
$sorted = $collection->sortKeys();
$sorted->all();
/*
[
'first' => 'John',
'id' => 22345,
'last' => 'Doe',
]
*/
// --- sortKeysDesc - sort in the opposite order
sole - exactly one element element that passes a given truth test
// only if it exactly one element matches
collect([1, 2, 3, 4])->sole(function ($value, $key) {
return $value === 2;
}); // 2
// pass a key/value pair to the sole method,
// will return the first element in the collection that matches the given pair
$collection = collect([
['product' => 'Desk', 'price' => 200],
['product' => 'Chair', 'price' => 100],
]);
$collection->sole('product', 'Chair'); // ['product' => 'Chair', 'price' => 100]
// call with no argument to get the first element in the collection if there is only one element
// if there are no elements in the collection that should be returned by the sole method,
// an \Illuminate\Collections\ItemNotFoundException exception will be thrown
// if there is more than one element that should be returned,
// an \Illuminate\Collections\MultipleItemsFoundException will be thrown
$collection = collect([
['product' => 'Desk', 'price' => 200],
]);
$collection->sole(); // ['product' => 'Desk', 'price' => 200]
splice - removes and returns a slice of items starting at index
$collection = collect([1, 2, 3, 4, 5]);
$chunk = $collection->splice(2);
$chunk->all(); // [3, 4, 5]
$collection->all(); // [1, 2]
// pass a second argument to limit the size of the resulting chunk
$collection = collect([1, 2, 3, 4, 5]);
$chunk = $collection->splice(2, 1);
$chunk->all(); // [3]
$collection->all(); // [1, 2, 4, 5]
// pass a third argument containing the new items
// to replace the items removed from the collection:
$collection = collect([1, 2, 3, 4, 5]);
$chunk = $collection->splice(2, 1, [10, 11]);
$chunk->all(); // [3]
$collection->all(); // [1, 2, 10, 11, 4, 5]
split - breaks a collection into the given number of groups
$collection = collect([1, 2, 3, 4, 5]);
$groups = $collection->split(3);
$groups->toArray(); // [[1, 2], [3, 4], [5]]
take - returns new collection with specified number of items
$collection = collect([0, 1, 2, 3, 4, 5]);
$chunk = $collection->take(3);
$chunk->all(); // [0, 1, 2]
// pass a negative integer
// take the specified amount of items from the end of the collection
$collection = collect([0, 1, 2, 3, 4, 5]);
$chunk = $collection->take(-2);
$chunk->all(); // [4, 5]
tap - do something with the items while not affecting collection itself
collect([2, 4, 3, 1, 5])
->sort()
->tap(function ($collection) {
Log::debug('Values after sorting', $collection->values()->toArray());
})
->shift(); // 1
// without tap
$tappable = TappableClass::make();
$tappable->doSomething();
$tappable->doSomethingElse();
$result = $tappable->getResult();
// with tap method
$result = tap(TappableClass::make(), function ($tappable) {
$tappable->doSomething();
$tappable->doSomethingElse();
})->getResult();
// with Tappable trait
$result = TappableClass::make()->tap(function ($tappable) {
$tappable->doSomething();
$tappable->doSomethingElse();
})->getResult();
times - invoke callback a given amount of times and create new collection
$collection = Collection::times(10, function ($number) {
return $number * 9;
});
$collection->all(); // [9, 18, 27, 36, 45, 54, 63, 72, 81, 90]
// useful when combined with factories to create Eloquent models
$categories = Collection::times(3, function ($number) {
return factory(Category::class)->create(['name' => "Category No. $number"]);
});
$categories->all();
/*
[
['id' => 1, 'name' => 'Category #1'],
['id' => 2, 'name' => 'Category #2'],
['id' => 3, 'name' => 'Category #3'],
]
*/
toArray - converts collection into a plain PHP array
// if the collection values are Eloquent models,
// models will also be converted to arrays
$collection = collect(['name' => 'Desk', 'price' => 200]);
$collection->toArray();
/*
[
['name' => 'Desk', 'price' => 200],
]
*/
// also converts all of the collection nested objects
// that are an instance of Arrayable to an array
// if you want to get the raw underlying array, use the all method
toJson - converts collection into a JSON serialized string
$collection = collect(['name' => 'Desk', 'price' => 200]);
$collection->toJson(); // '{"name":"Desk", "price":200}'
transform - iterates and calls callback to replace items
// modifies the collection itself
// create a new collection with map method
$collection = collect([1, 2, 3, 4, 5]);
$collection->transform(function ($item, $key) {
return $item * 2;
});
$collection->all(); // [2, 4, 6, 8, 10]
union - adds the given array to the collection
// if the given array contains keys that are already in the original collection,
// the original collection values will be preferred
$collection = collect([1 => ['a'], 2 => ['b']]);
$union = $collection->union([3 => ['c'], 1 => ['b']]);
$union->all(); // [1 => ['a'], 2 => ['b'], 3 => ['c']]
unique , uniqueStrict - returns all of the unique items
// keeps the original array keys
// use the values method to reset the keys to consecutively numbered indexes
$collection = collect([1, 1, 2, 2, 3, 4, 2]);
$unique = $collection->unique();
$unique->values()->all(); // [1, 2, 3, 4]
// specify the key used to determine uniqueness
// when dealing with nested arrays or objects
$collection = collect([
['name' => 'iPhone 6', 'brand' => 'Apple', 'type' => 'phone'],
['name' => 'iPhone 5', 'brand' => 'Apple', 'type' => 'phone'],
['name' => 'Apple Watch', 'brand' => 'Apple', 'type' => 'watch'],
['name' => 'Galaxy S6', 'brand' => 'Samsung', 'type' => 'phone'],
['name' => 'Galaxy Gear', 'brand' => 'Samsung', 'type' => 'watch'],
]);
$unique = $collection->unique('brand');
$unique->values()->all();
/*
[
['name' => 'iPhone 6', 'brand' => 'Apple', 'type' => 'phone'],
['name' => 'Galaxy S6', 'brand' => 'Samsung', 'type' => 'phone'],
]
*/
// pass own callback to determine item uniqueness
$unique = $collection->unique(function ($item) {
return $item['brand'].$item['type'];
});
$unique->values()->all();
/*
[
['name' => 'iPhone 6', 'brand' => 'Apple', 'type' => 'phone'],
['name' => 'Apple Watch', 'brand' => 'Apple', 'type' => 'watch'],
['name' => 'Galaxy S6', 'brand' => 'Samsung', 'type' => 'phone'],
['name' => 'Galaxy Gear', 'brand' => 'Samsung', 'type' => 'watch'],
]
*/
// uses "loose" comparisons when checking item values,
// string with an integer value will be considered equal to an integer of the same value
// se the uniqueStrict method to filter using "strict" comparisons
when , unless , whenNotEmpty , whenEmpty , unlessEmpty , unlessNotEmpty
// --- when - execute callback when the first argument evaluates to true
$collection = collect([1, 2, 3]);
$collection->when(true, function ($collection) {
return $collection->push(4);
});
$collection->when(false, function ($collection) {
return $collection->push(5);
});
$collection->all(); // [1, 2, 3, 4]
$collection->when(function ($collection) {
return false; // This closure is executed...
}, function ($collection) {
// Not executed since first closure returned "false"...
$collection->merge([1, 2, 3]);
});
// --- unless - execute callback unless first argument evaluates to true
$collection = collect([1, 2, 3]);
$collection->unless(true, function ($collection) {
return $collection->push(4);
});
$collection->unless(false, function ($collection) {
return $collection->push(5);
});
$collection->all(); // [1, 2, 3, 5]
// --- whenEmpty - execute the given callback when the collection is empty
$collection = collect(['michael', 'tom']);
$collection->whenEmpty(function ($collection) {
return $collection->push('adam');
});
$collection->all(); // ['michael', 'tom']
$collection = collect();
$collection->whenEmpty(function ($collection) {
return $collection->push('adam');
});
$collection->all(); // ['adam']
$collection = collect(['michael', 'tom']);
$collection->whenEmpty(function($collection) {
return $collection->push('adam');
}, function($collection) {
return $collection->push('taylor');
});
$collection->all(); // ['michael', 'tom', 'taylor']
// --- whenNotEmpty - execute callback when the collection is not empty
$collection = collect(['michael', 'tom']);
$collection->whenNotEmpty(function ($collection) {
return $collection->push('adam');
});
$collection->all(); // ['michael', 'tom', 'adam']
$collection = collect();
$collection->whenNotEmpty(function ($collection) {
return $collection->push('adam');
});
$collection->all(); // []
$collection = collect();
$collection->whenNotEmpty(function($collection) {
return $collection->push('adam');
}, function($collection) {
return $collection->push('taylor');
});
$collection->all(); // ['taylor']
values - keys reset to consecutive integers
$collection = collect([
10 => ['product' => 'Desk', 'price' => 200],
11 => ['product' => 'Desk', 'price' => 200]
]);
$values = $collection->values();
$values->all();
/*
[
0 => ['product' => 'Desk', 'price' => 200],
1 => ['product' => 'Desk', 'price' => 200],
]
*/
where , whereStrict - filters by a given key/value pair
$collection = collect([
['product' => 'Desk', 'price' => 200],
['product' => 'Chair', 'price' => 100],
['product' => 'Bookcase', 'price' => 150],
['product' => 'Door', 'price' => 100],
]);
$filtered = $collection->where('price', 100);
$filtered->all();
/*
[
['product' => 'Chair', 'price' => 100],
['product' => 'Door', 'price' => 100],
]
*/
// uses "loose" comparisons when checking item values,
// string with an integer value will be considered equal to an integer of the same value
// use the whereStrict method to filter using "strict" comparisons
where[Not]Between , where[Not]In , whereInstanceOf
// --- whereBetween - filters within a given range
$collection = collect([
['product' => 'Desk', 'price' => 200],
['product' => 'Chair', 'price' => 80],
['product' => 'Bookcase', 'price' => 150],
['product' => 'Pencil', 'price' => 30],
['product' => 'Door', 'price' => 100],
]);
$filtered = $collection->whereBetween('price', [100, 200]);
$filtered->all();
/*
[
['product' => 'Desk', 'price' => 200],
['product' => 'Bookcase', 'price' => 150],
['product' => 'Door', 'price' => 100],
]
*/
// --- whereNotBetween - filters within a given range
$collection = collect([
['product' => 'Desk', 'price' => 200],
['product' => 'Chair', 'price' => 80],
['product' => 'Bookcase', 'price' => 150],
['product' => 'Pencil', 'price' => 30],
['product' => 'Door', 'price' => 100],
]);
$filtered = $collection->whereNotBetween('price', [100, 200]);
$filtered->all();
/*
[
['product' => 'Chair', 'price' => 80],
['product' => 'Pencil', 'price' => 30],
]
*/
// --- whereIn - filters by a given key/value contained within the given array
$collection = collect([
['product' => 'Desk', 'price' => 200],
['product' => 'Chair', 'price' => 100],
['product' => 'Bookcase', 'price' => 150],
['product' => 'Door', 'price' => 100],
]);
$filtered = $collection->whereIn('price', [150, 200]);
$filtered->all();
/*
[
['product' => 'Bookcase', 'price' => 150],
['product' => 'Desk', 'price' => 200],
]
*/
// uses "loose" comparisons when checking item values,
// string with an integer value will be considered equal to an integer of the same value
// se the whereInStrict method to filter using "strict" comparisons
// --- whereNotIn - filters by a given key/value not contained within the given array
$collection = collect([
['product' => 'Desk', 'price' => 200],
['product' => 'Chair', 'price' => 100],
['product' => 'Bookcase', 'price' => 150],
['product' => 'Door', 'price' => 100],
]);
$filtered = $collection->whereNotIn('price', [150, 200]);
$filtered->all();
/*
[
['product' => 'Chair', 'price' => 100],
['product' => 'Door', 'price' => 100],
]
*/
// uses "loose" comparisons when checking item values,
// string with an integer value will be considered equal to an integer of the same value
// use the whereNotInStrict method to filter using "strict" comparisons
// --- whereInstanceOf - filters by a given class type
$collection = collect([
new User,
new User,
new Post,
]);
return $collection->whereInstanceOf(User::class);
wrap , unwrap - wraps value into collection , returns underlying items
// --- wrap - wraps the given value in a collection when applicable
$collection = Collection::wrap('John Doe');
$collection->all(); // ['John Doe']
$collection = Collection::wrap(['John Doe']);
$collection->all(); // ['John Doe']
$collection = Collection::wrap(collect('John Doe'));
$collection->all(); // ['John Doe']
// --- unwrap - returns underlying items from the given value when applicable
Collection::unwrap(collect('John Doe')); // ['John Doe']
Collection::unwrap(['John Doe']); // ['John Doe']
Collection::unwrap('John Doe'); // 'John Doe'
zip - merges at the corresponding index
$collection = collect(['Chair', 'Desk']);
$zipped = $collection->zip([100, 200]);
$zipped->all(); // [['Chair', 100], ['Desk', 200]]
Lazy Collections
// uses PHP generators for large datasets with low memory usage
// keep only small part of the file in memory at a given time
use App\Models\LogEntry;
use Illuminate\Support\LazyCollection;
// --- CREATING, pass a PHP generator function to the collection 'make' method
// - processing large file:
LazyCollection::make(function () {
$handle = fopen('log.txt', 'r');
while (($line = fgets($handle)) !== false) {
yield $line;
}
})->chunk(4)->map(function ($lines) {
return LogEntry::fromLines($lines);
})->each(function (LogEntry $logEntry) {
// Process the log entry...
});
// - iterate through a lot of Eloquent models:
$users = User::all()->filter(function ($user) {
return $user->id > 500;
});
// - query builder 'cursor' method returns a LazyCollection instance
// runs a single query against the database, only keeps one Eloquent model loaded in memory
// filter callback is not executed until we iterate over each user individually
$users = User::cursor()->filter(function ($user) {
return $user->id > 500;
});
foreach ($users as $user) {
echo $user->id;
}
// --- METHODS
// almost all methods available on the Collection class are available.
// both classes implement the Illuminate\Support\Enumerable, additional:
// --- takeUntilTimeout()
// returns a new lazy collection that will enumerate values until the specified time
// after that time, the collection will then stop enumerating
$lazyCollection = LazyCollection::times(INF)
->takeUntilTimeout(now()->addMinute());
$lazyCollection->each(function ($number) {
dump($number);
sleep(1);
}); // 1 2 ... 58 59
// example of application that submits invoices from the database using a cursor
// schedule task that runs every 15 minutes
// only processes invoices for a maximum of 14 minutes:
use App\Models\Invoice;
use Illuminate\Support\Carbon;
// ...
Invoice::pending()->cursor()
->takeUntilTimeout(
Carbon::createFromTimestamp(LARAVEL_START)->add(14, 'minutes')
)
->each(fn ($invoice) => $invoice->submit());
// --- tapEach()
// while each method calls callback for each item in the collection right away
// this, only calls callback as the items are being pulled out of the list one by one
// nothing has been dumped so far...:
$lazyCollection = LazyCollection::times(INF)->tapEach(function ($value) {
dump($value);
});
// Three items are dumped...
$array = $lazyCollection->take(3)->all(); // 1 2 3
// --- remember() - returns a new lazy collection
// that will remember any values that have already been enumerated
// and will not retrieve them again on subsequent collection enumerations
// no query has been executed yet...:
$users = User::cursor()->remember();
// query is executed...
// first 5 users are hydrated from the database...
$users->take(5)->all();
// first 5 users come from the collections cache...
// rest are hydrated from the database...
$users->take(20)->all();
Higher Order Message
// each higher order message can be accessed as a dynamic property
// use to call a method on each object within a collection:
$users = User::where('votes', '>', 500)->get();
$users->each->markAsVip();
// use the sum higher order message
// to gather the total number of "votes" for a collection of users:
$users = User::where('group', 'Development')->get();
return $users->sum->votes;
Encryption
- set the APP_KEY option of .env file to a 32 character, random, or all values encrypted by Lumen will be insecure
- OpenSSL and the AES-256-CBC cipher signed with a message authentication code (MAC) to detect any modifications
php artisan key:generate
, in Lumen, generate with str_random(32)
- encrypted values are passed through serialize during encryption, allows for encryption of objects and arrays, non-PHP clients receiving encrypted values will need to unserialize the data, use encryptString and decryptString
// --- Crypt::encrypt
// encrypt a secret and store it on an Eloquent model
namespace App\Http\Controllers;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
class UserController extends Controller {
// secret message for the user
public function storeSecret(Request $request, $id) {
$user = User::findOrFail($id);
$user->fill([
'secret' => Crypt::encrypt($request->secret)
])->save();
}
}
// --- Crypt::decrypt
use Illuminate\Contracts\Encryption\DecryptException;
// ...
try {
$decrypted = Crypt::decrypt($encryptedValue);
} catch (DecryptException $e) {
// ...
}
// ...
// --- Crypt::encryptString , Crypt::decryptString
$encrypted = Crypt::encryptString('Hello world.');
$decrypted = Crypt::decryptString($encrypted);
Error Handling
- all exceptions are handled by the App\Exceptions\Handler class, contains two methods: report and render
- debug option in config/app.php configuration file determines how much information about an error is actually displayed to the user, is set to respect the value of the APP_DEBUG environment variable, which is stored in .env file, in production should be false, in dev, true
- Laravel automatically adds the current user ID to every exception log message as contextual data
// --- report
// helper, report an exception but continue handling the current request
public function isValid($value) {
try {
// validate the value...
} catch (Exception $e) {
report($e);
return false;
}
}
// --- HTTP exceptions
abort(404);
abort(403, 'Unauthorized action.');
// --- custom HTTP error pages
// customize the error page for 404 HTTP status codes,
// create a resources/views/errors/404.blade.php
// views within this directory should be named to match the HTTP status code
// HttpException instance will be passed to the view as an $exception variable:
<h2>{{ $exception->getMessage() }}</h2>
// publish error page templates, customize them to liking:
php artisan vendor:publish --tag=laravel-errors
// ---
namespace App\Exceptions;
use Exception;
use Illuminate\Validation\ValidationException;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Laravel\Lumen\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\HttpException;
class Handler extends ExceptionHandler {
// list of the exception types that should not be reported
protected $dontReport = [
// \Illuminate\Auth\AuthenticationException::class,
AuthorizationException::class,
HttpException::class,
ModelNotFoundException::class,
ValidationException::class,
];
// report or log an exception
public function report(Throwable $exception) {
// great spot to send exceptions to Sentry, Bugsnag, etc.
if ($exception instanceof CustomException) {
// ...
}
parent::report($exception);
}
// render an exception into an HTTP response
public function render($request, Throwable $exception) {
// check the exception type or return own custom response
if ($exception instanceof CustomException) {
return response()->view('errors.custom', [], 500);
}
return parent::render($request, $exception);
}
// overriding the context method
// information will be included in every exception log message
protected function context() {
return array_merge(parent::context(), [
'foo' => 'bar',
]);
}
}
// --- custom exception
namespace App\Exceptions;
use Exception;
class RenderException extends Exception {
public function report() {
// ...
}
// type-hint any required dependencies of the report method
// they will automatically be injected into the method by service container
public function render($request) {
return response(...);
}
}
Logging
- log messages to files, the system error log, and even to Slack to notify entire team
- utilizes the Monolog library
- config/logging.php - configure application log channels, review each of the available channels and options
- stack channel is used by default when logging messages, used to aggregate multiple log channels into a single channel
- Monolog is instantiated with a "channel name" that matches the current environment, such as production or local, change this value by adding a "name" option to channel configuration
- by default, message will be written to the default log channel as configured by config/logging.php configuration file
- define warning level (emergency,...) for channels to intercept their log errors, also write information to the logs with Log facade
- advanced monolog channel customization
use Illuminate\Support\Facades\Log;
// ...
Log::emergency($message);
Log::alert($message);
Log::critical($message);
Log::error($message);
Log::warning($message);
Log::notice($message);
Log::info('Showing user profile for user: '.$id);
Log::debug($message);
// with contextual data
Log::info('User failed to login.', ['id' => $user->id]);
// log a message to a channel other than default
Log::channel('slack')->info('Something happened!');
// create an on-demand logging stack consisting of multiple channels
Log::stack(['single', 'slack'])->info('Something happened!');
// ---
namespace App\Http\Controllers;
use App\User;
use Illuminate\Support\Facades\Log;
use App\Http\Controllers\Controller;
class UserController extends Controller {
// show profile for user
public function showProfile($id) {
Log::info('Showing user profile for user: '.$id);
return view('user.profile', ['user' => User::findOrFail($id)]);
}
}
// --- config/logging.php
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
return [
// --- default log channel, one of the channels defined in the "channels"
'default' => env('LOG_CHANNEL', 'stack'),
// --- configure the log channels
'channels' => [
// wrapper to facilitate creating "multi-channel" channels
'stack' => [
'driver' => 'stack',
// aggregates other channels, will have the opportunity to log the message
'channels' => ['daily'],
// 'name' => 'channel-name',
],
// single file or path based logger channel (StreamHandler)
'single' => [
'driver' => 'single',
'path' => storage_path('logs/lumen.log'),
'level' => 'debug',
// optional:
// if messages should bubble up to other channels after being handled, default is true
bubble => true,
// log file permissions, default is 0644
permission => 0644,
// attempt to lock the log file before writing to it, default is false
locking => false,
],
// RotatingFileHandler based Monolog driver which rotates daily
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/lumen.log'),
'level' => 'debug',
'days' => 14,
// optional:
// if messages should bubble up to other channels after being handled, default is true
bubble => true,
// log file permissions, default is 0644
permission => 0644,
// attempt to lock the log file before writing to it, default is false
locking => false,
],
// SlackWebhookHandler based Monolog driver
'slack' => [
'driver' => 'slack',
// URL should match incoming webhook that is configured for Slack team
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'Lumen Log',
'emoji' => ':boom:',
'level' => 'critical',
],
// SyslogUdpHandler based Monolog driver
'papertrail' => [
'driver' => 'monolog',
'level' => 'debug',
'handler' => SyslogUdpHandler::class,
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
],
],
'stderr' => [
'driver' => 'monolog',
'handler' => StreamHandler::class,
'with' => [
'stream' => 'php://stderr',
],
],
// SyslogHandler based Monolog driver
'syslog' => [
'driver' => 'syslog',
'level' => 'debug',
],
// ErrorLogHandler based Monolog driver
'errorlog' => [
'driver' => 'errorlog',
'level' => 'debug',
],
// Monolog factory driver that may use any supported Monolog handler
'monolog' => [
// ...
],
// driver that calls a specified factory to create a channel
'custom' => [
// ...
],
],
];
Events
- subscribe and listen for various events that occur in application
- typically stored in the app/Events directory, while their listeners are stored in app/Listeners, if these directories dont exists, they will be created as you generate events and listeners using Artisan console commands
- single event can have multiple listeners that do not depend on each other
php artisan make:event SimpleEvent
with lumen-generators
- OR
php artisan event:generate SimpleEvent
in Laravel
- events should be registered via the EventServiceProvider $listen array, also register Closure based events manually in the boot method of EventServiceProvider
- if your App\Providers\EventServiceProvider class contains a 'register' function, you should ensure that you call parent::register at the beginning of this method
- automatic event discovery
- find and register events and listeners by scanning Listeners directory, instead of registering events and listeners manually in the $listen array of the EventServiceProvider
- any explicitly defined events listed in the EventServiceProvider will still be registered
- Laravel finds event listeners by scanning the listener classes using reflection. Laravel will register any listener class method that begins with handle as event listeners for the event that is type-hinted in the method signature
- enable by overriding shouldDiscoverEvents method of app EventServiceProvider
php artisan event:list
- display a list of all registered events and listeners
php artisan event:cache
during deployment process to cache a manifest of all events and listeners, speed up the event registration process, avoid scanning all listeners on every request, php artisan event:clear
- destroy the cache
// --- app/Providers/EventServiceProvider.php
use Illuminate\Support\Facades\Event;
class EventServiceProvider extends ServiceProvider {
// manual registration of Closure based events
public function boot() {
parent::boot();
Event::listen('event.name', function ($foo, $bar) {}
Event::listen('event.*', function ($eventName, array $data) {
// ...
});
Event::listen(function (PodcastProcessed $event) {
//
});
// marked as queueable using the Illuminate\Events\queueable function
// use function Illuminate\Events\queueable;
Event::listen(queueable(function (PodcastProcessed $event) {
// ...
}));
// use the onConnection, onQueue, and delay methods to customize the execution of the queued listener
Event::listen(queueable(function (PodcastProcessed $event) {
// ...
})->onConnection('redis')->onQueue('podcasts')->delay(now()->addSeconds(10)));
// handle anonymous queued listener failures,
// provide a closure to the catch method while defining the queueable listener:
// use Illuminate\Support\Facades\Event;
// use Throwable;
Event::listen(queueable(function (PodcastProcessed $event) {
// ...
})->catch(function (PodcastProcessed $event, Throwable $e) {
// queued listener failed...
}));
}
// determine if events and listeners should be automatically discovered
public function shouldDiscoverEvents() {
return true;
}
// all listeners within Listeners directory will be scanned
// add listener directories that should be used to discover events
protected function discoverEventsWithin() {
return [
$this->app->path('Listeners'),
];
}
// event listener mappings for the application
protected $listen = [
'App\Events\OrderShipped' => [
'App\Listeners\SendShipmentNotification',
],
];
}
// --- EVENT
// data container which holds the information related to the event
// assume generated OrderShipped event receives an Eloquent ORM object
// container for the Order instance that was purchased:
namespace App\Events;
use App\Order;
use Illuminate\Queue\SerializesModels;
class OrderShipped {
// serialize any Eloquent models
// if the event object is serialized using PHP serialize function
use SerializesModels;
public $order;
// create a new event instance
public function __construct(Order $order) {
$this->order = $order;
}
}
// --- LISTENER
// receives event instance in handle method and perform any actions necessary to respond to the event
// event:generate - automatically import proper event class and type-hint event on the handle method
// type-hint any dependencies you need on constructors,
// they will be resolved via the Laravel service container,
// so dependencies will be injected automatically
namespace App\Listeners;
use App\Events\OrderShipped;
class SendShipmentNotification {
// create the event listener
public function __construct() {
// ...
}
// handle the event
public function handle(OrderShipped $event) {
// access the order using $event->order...
// if required, stop propagation of an event to other listeners
return false;
}
}
// --- QUEUED EVENT LISTENERS
// when listener is going to perform a slow task: sending an e-mail or making an HTTP request
// configure queue and start a queue listener on server or local development environment
// when this listener is called for an event,
// it will be automatically queued by the event dispatcher using Laravel queue system
// if no exceptions are thrown when the listener is executed by the queue,
// queued job will automatically be deleted after it has finished processing
namespace App\Listeners;
use App\Events\OrderShipped;
use Illuminate\Contracts\Queue\ShouldQueue;
// to manually access the listener underlying queue job delete and release methods
use Illuminate\Queue\InteractsWithQueue;
class SendShipmentNotification implements ShouldQueue {
// optional, manually handle the event
use InteractsWithQueue;
public function handle(OrderShipped $event) {
if (true) { $this->release(30); }
}
// optional, handle a job failure
// if queued listener exceeds the maximum number of attempts as defined by queue worker
public function failed(OrderShipped $event, $exception) {
// ...
}
// optional, name of the connection the job should be sent to
public $connection = 'sqs';
// optional, name of the queue the job should be sent to
public $queue = 'listeners';
// optional, time (seconds) before the job should be processed
public $delay = 60;
// ...
}
// --- DISPATCHING EVENTS
// pass an instance of the event to the event helper
// will dispatch the event to all of its registered listeners
// call it from anywhere in application
namespace App\Http\Controllers;
use App\Order;
use App\Events\OrderShipped;
use App\Http\Controllers\Controller;
class OrderController extends Controller {
// ship the given order
public function ship($orderId) {
$order = Order::findOrFail($orderId);
// order shipment logic...
event(new OrderShipped($order));
// Event::dispatch(new ExampleEvent);
}
}
// --- EVENT SUBSCRIBERS
// classes that may subscribe to multiple events from within the class itself,
// allowing to define several event handlers within a single class
// should define a subscribe method, which will be passed an event dispatcher instance
// call the listen method on the given dispatcher to register event listeners
namespace App\Listeners;
class UserEventSubscriber {
// handle user login events
public function handleUserLogin($event) {}
// handle user logout events
public function handleUserLogout($event) {}
// register the listeners for the subscriber
public function subscribe($events) {
$events->listen(
'Illuminate\Auth\Events\Login',
'App\Listeners\UserEventSubscriber@handleUserLogin'
);
$events->listen(
'Illuminate\Auth\Events\Logout',
'App\Listeners\UserEventSubscriber@handleUserLogout'
);
}
}
// register event subscribers with the event dispatcher
// using the $subscribe property on the EventServiceProvider
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider {
// event listener mappings for the application
protected $listen = [
//
];
// subscriber classes to register
protected $subscribe = [
'App\Listeners\UserEventSubscriber',
];
}
// --- AUTOMATIC EVENT DISCOVERY
// available for Laravel 5.8.9 or later !
// any listener class method that begins with handle,
// register those methods as event listeners for the event
// that is type-hinted in the method signature
use App\Events\PodcastProcessed;
class SendPodcastProcessedNotification {
// handle the given event
public function handle(PodcastProcessed $event) {
// ...
}
}
Mail
- simple API over Symfony Mailer library with drivers for SMTP, Mailgun, Amazon SES, PHP mail function, and sendmail, allowing to quickly get started sending mail through a local or cloud based service of choice
- install the illuminate/mail package via Composer
- register the Illuminate\Mail\MailServiceProvider in bootstrap/app.php: $app->register(Illuminate\Mail\MailServiceProvider::class);
- configure mail options by copying the mail.php configuration file from the full Laravel framework to the config directory in the root of project, adjust the configuration options as needed
- register the mailer and its aliases within bootstrap/app.php
composer require guzzlehttp/guzzle
- API based drivers such as Mailgun, and Postmark are often simpler and faster than SMTP servers, use one of these drivers, all of the API drivers require the Guzzle HTTP library
php artisan make:mail OrderShipped
- each type of email sent by application is represented as a "mailable" class, stored in the app/Mail directory
- create a resources/views/emails directory to house all of email templates
- in local development you dont actually send emails to live email addresses
- log mail driver will write all email messages to log files for inspection, configure
- set a universal recipient of all emails sent by the framework, to option in config/mail.php: 'to' => ['address'=>'example@example.com', 'name'=>'Example'],
- use a service like Mailtrap and the smtp driver to send email messages to a "dummy" mailbox where you may view them in a true email client, actually inspect the final emails in Mailtrap message viewer
// register the mailer and its aliases within bootstrap/app.php
$app->configure('mail');
$app->alias('mailer', Illuminate\Mail\Mailer::class);
$app->alias('mailer', Illuminate\Contracts\Mail\Mailer::class);
$app->alias('mailer', Illuminate\Contracts\Mail\MailQueue::class);
// following configuration options should also be available to .env file
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=hello@example.com
MAIL_FROM_NAME="Example app"
API drivers
// --- Mailgun
composer require symfony/mailgun-mailer symfony/http-client
// next, install Guzzle
// then set the driver option in config/mail.php configuration file to "mailgun"
// next, verify that config/services.php configuration file contains
'mailgun' => [
'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'),
// define region endpoint if not "US":
'endpoint' => env('MAILGUN_ENDPOINT', 'api.eu.mailgun.net'),
],
// --- Postmark
composer require symfony/postmark-mailer symfony/http-client
// next, install Guzzle
// then set the driver option in config/mail.php configuration file to "postmark"
// verify that config/services.php configuration file contains
'postmark' => [
'token' => env('POSTMARK_TOKEN'),
// specify the message stream that should be used by a given mailer
'transport' => 'postmark',
'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
],
// --- Amazon SES
composer require aws/aws-sdk-php
// next, set the driver option in config/mail.php configuration file
// to "ses" and verify that config/services.php configuration file
// contains the following options:
'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
// AWS temporary credentials via a session token:
'token' => env('AWS_SESSION_TOKEN'),
],
// include additional options when executing the SES SendRawEmail request,
// define an options array within ses configuration:
'ses' => [
'key' => 'your-ses-key',
'secret' => 'your-ses-secret',
'region' => 'ses-region', // e.g. us-east-1
'options' => [
'ConfigurationSetName' => 'MyConfigurationSet',
'Tags' => [
[ 'Name' => 'foo', 'Value' => 'bar' ],
],
],
],
// --- Failover Configuration
// when external service may be down
'mailers' => [
'failover' => [
'transport' => 'failover',
'mailers' => [
'postmark',
'mailgun',
'sendmail',
],
],
// ...
],
// set it as default:
'default' => env('MAIL_MAILER', 'failover'),
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
class EmailTests extends Mailable {
// mailable class that should always be queued
// even if you call send method when mailing,
// mailable will still be queued since it implements the contract
// class OrderShipped extends Mailable implements ShouldQueue {
use Queueable, SerializesModels;
// any defined public property will automatically be made available to the view:
public $order; // order instance, {{ $order->price }} in template
// create a new message instance
public function __construct(Order $order) {
// ...
}
// build the message
public function build() {
return $this
// sender of the email
->from('example@example.com')
->view('emails.orders.shipped') // template
// define a plain-text version of email
->text('emails.orders.shipped_plain')
// set data to protected or private properties
// so is not automatically made available to the template
// then manually pass data to the view
->with([
'orderName' => $this->order->name,
'orderPrice' => $this->order->price, // {{ $orderPrice }}
])
// add attachments
->attach('/path/to/file')
// specify the display name and / or MIME type
->attach('/path/to/file', [
'as' => 'name.pdf',
'mime' => 'application/pdf',
])
// file on one of filesystem disks
->attachFromStorage('/path/to/file')
// with name and additional options
->attachFromStorage('/path/to/file', 'name.pdf', [
'mime' => 'application/pdf'
])
// specify a storage disk other than default disk
->attachFromStorageDisk('s3', '/path/to/file')
// attach a raw string of bytes as an attachment
// for generated PDF in memory and attachments without writing to disk
->attachData($this->pdf, 'name.pdf', [
'mime' => 'application/pdf',
]);
// register a callback which will be invoked with the raw Symfony mailer message
// customize the message before it is delivered
$this->withSymfonyMessage(function ($message) {
$message->getHeaders()->addTextHeader(
'Custom-Header', 'HeaderValue'
);
});
}
// capture the HTML content of a mailable without sending it
// return evaluated contents of the mailable as a string
public function render () {
$invoice = App\Invoice::find(1);
return (new App\Mail\InvoicePaid($invoice))->render();
}
}
// --- config/mail.php - GLOBAL configurations
'from' => ['address' => 'example@example.com', 'name' => 'App Name'],
'reply_to' => ['address' => 'example@example.com', 'name' => 'App Name'],
// --- embed - inline attachments
// $message variable available to all email templates
// not available in plain-text messages
<body>
Here is an image:
<img src="{{ $message->embed($pathToImage) }}">
// embedData - raw data string
<img src="{{ $message->embedData($data, $name) }}">
</body>
// --- preview design without sending it to an actual email address
Route::get('mailable', function () {
$invoice = App\Invoice::find(1);
return new App\Mail\InvoicePaid($invoice);
});
Markdown Mailables
// pre-built templates and components of mail notifications in mailables
// responsive HTML templates for the messages
// while also automatically generating a plain-text counterpart:
// php artisan make:mail OrderShipped --markdown=emails.orders.shipped
// then use markdown method instead of the view:
// ...
public function build() {
return $this->from('example@example.com')
->markdown('emails.orders.shipped');
}
// ...
// combination of Blade components and Markdown syntax
// do not use indentation when writing Markdown emails
// Markdown parsers will render indented content as code blocks
@component('mail::message')
# Order Shipped
order has been shipped!
@component('mail::button', ['url' => $url])
View Order
@endcomponent
Thanks,<br>
{{ config('app.name') }}
@endcomponent
// --- button component - centered button link
@component('mail::button', ['url' => $url, 'color' => 'success']) // primary, success, error
View Order
@endcomponent
// --- panel component
// block of text in a panel with different background color
// draw attention to a given block of text
@component('mail::panel')
This is the panel content.
@endcomponent
// --- table component
// transform a Markdown table into an HTML table
// column alignment is supported using the default Markdown table alignment syntax
@component('mail::table')
| Laravel | Table | Example |
| ------------- |:-------------:| --------:|
| Col 2 is | Centered | $10 |
| Col 3 is | Right-Aligned | $20 |
@endcomponent
// --- export all Markdown mail components to own application for customization
// php artisan vendor:publish --tag=laravel-mail
// will publish the Markdown mail components to the resources/views/vendor/mail
// will contain an html and a text directory of every available component
// resources/views/vendor/mail/html/themes will contain a default.css file
// styles will automatically be in-lined within the HTML representations
// to build an entirely new theme for the Markdown components,
// write a new CSS file within the html/themes directory
// and change the "theme" option of "mail" configuration file
namespace App\Http\Controllers;
use App\Order;
use App\Mail\OrderShipped;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use App\Http\Controllers\Controller;
class OrderController extends Controller {
// ship the given order
public function ship(Request $request, $orderId) {
$order = Order::findOrFail($orderId);
// ship order...
// send email
// accepts an email address, a user instance, or a collection of users
Mail::to($request->user())->send(new OrderShipped($order));
// if you pass an object or collection of objects,
// mailer will automatically use their email and name properties
// when setting the email recipients,
// make sure these attributes are available on objects
}
}
// --- set "to", "cc", and "bcc" recipients all within chained method call
Mail::to($request->user())
->cc($moreUsers)
->bcc($evenMoreUsers)
->send(new OrderShipped($order));
// --- queue a mail message
// take care of pushing a job onto the queue so the message is sent in the background
// configure queues before using this feature
Mail::to($request->user())
->cc($moreUsers)
->bcc($evenMoreUsers)
->queue(new OrderShipped($order));
// --- delay the delivery of a queued email message
$when = now()->addMinutes(10);
Mail::to($request->user())
->cc($moreUsers)
->bcc($evenMoreUsers)
->later($when, new OrderShipped($order));
// --- onQueue , onConnection - specify the connection and queue name for the message
$message = (new OrderShipped($order))
->onConnection('sqs')
->onQueue('emails');
Mail::to($request->user())
->cc($moreUsers)
->bcc($evenMoreUsers)
->queue($message);
// --- locale, other than the current language
// will remember this locale if the mail is queued
Mail::to($request->user())
->locale('es')
->send(new OrderShipped($order));
// --- user preferred locales
// instruct Laravel to use stored locale when sending mail
use Illuminate\Contracts\Translation\HasLocalePreference;
class User extends Model implements HasLocalePreference {
// get the user preferred locale
public function preferredLocale() {
return $this->locale;
}
}
// use preferred locale when sending mailables and notifications to the model
// no need to call the locale method when using this interface
Mail::to($request->user())->send(new OrderShipped($order));
// --- MessageSending and MessageSent events
// fired when the mail is being sent, not when it is queued
// register an event listener for this event in EventServiceProvider
protected $listen = [
'Illuminate\Mail\Events\MessageSending' => [
'App\Listeners\LogSendingMessage',
],
'Illuminate\Mail\Events\MessageSent' => [
'App\Listeners\LogSentMessage',
],
];
// --- multiple mail drivers
// configure multiple mailers for a single app within the mail configuration file
// own options and even its own unique transport
// use different email services to send certain email messages
// send a message using a specific mailer configuration:
Mail::mailer('postmark')
->to($request->user())
->send(new OrderShipped($order));
Queues
- unified API across a variety of different queue backends, defer the processing of a time consuming task, such as sending an email, until a later time
- config/queue.php - configuration file, connection configurations for each of the queue drivers that are included with the framework, which includes a database, Beanstalkd, Amazon SQS, Redis, and a synchronous driver that will execute jobs immediately (for local use), null queue driver is also included which discards queued jobs
php artisan make:job ProcessPodcast
- generate a new queued job, stored in the app/Jobs directory, or make a copy of ExampleJob class
- class will implement the Illuminate\Contracts\Queue\ShouldQueue interface, indicating to Laravel that the job should be pushed onto the queue to run asynchronously
- if an exception is thrown while the job is being processed, the job will automatically be released back onto the queue so it may be attempted again, until it has been attempted the maximum number of times allowed, define number of attempts
- binary data, such as raw image contents, should be passed through the base64_encode function before being passed to a queued job, job may not properly serialize to JSON when being placed on the queue
- queue worker
php artisan queue:work
- process new jobs as they are pushed onto the queue, once started, it will continue to run until it is manually stopped or terminal is closed, stores booted application state in memory, will not notice changes in code base, php artisan queue:restart
queue workers
- queue uses cache to store restart signals
php artisan queue:listen
- designed for working locally, doesnt boot the application state in memory so you never have to restart the queues when making code changes, never use in production, it is significantly less efficient than the queue:work command
php artisan queue:work redis
- specify which queue connection the worker should utilize, corresponds to one defined in config/queue.php
php artisan queue:work redis --queue=emails
- processing particular queues for a given connection, start a worker that only processes only emails queue
php artisan queue:work --once
- only process a single job from the queue
php artisan queue:work --stop-when-empty
- process all jobs and then exit gracefully, useful when working Laravel queues within a Docker container if you wish to shutdown the container after the queue is empty
php artisan queue:work --queue=high,default
- run a worker that gives higher processing priority for jobs pushed to a "high" queue
php artisan queue:work --timeout=60
- how long queue master process will wait before killing off a child queue worker that is processing a job, removes frozen processes, works together with retry_after configuration option and should always be at least several seconds shorter
php artisan queue:work --sleep=3
- how long (in seconds) the worker will "sleep" if there are no new jobs available, avoid delay
- after a job has exceeded this amount of attempts, it will be inserted into the failed_jobs database table, create a migration for the failed_jobs table:
php artisan queue:failed-table
, php artisan migrate
php artisan queue:work redis --tries=3
- specify the maximum number of times a job should be attemptedwhen running queue worker, one time by default, to force jobs to be tried indefinitely, pass 0
php artisan queue:failed
- view all of failed jobs that have been inserted into yo failed_jobs database table, will list the job ID, connection, queue, and failure time
php artisan queue:retry 5
- retry a failed job that has an ID of 5
php artisan queue:retry all
- retry all failed jobs
php artisan queue:forget 5
- delete failed job
php artisan queue:flush
- delete all of failed jobs
- Daemon queue workers do not "reboot" the framework before processing each job, free any heavy resources after each job completes, if you are doing image manipulation with the GD library,free the memory with imagedestroy when you are done, and so on
- when injecting an Eloquent model into a job, it is automatically serialized before being placed on the queue and restored when the job is processed, if the model has been deleted while the job was waiting to be processed by a worker, job may fail with a ModelNotFoundException, choose to automatically delete jobs with missing models by setting job deleteWhenMissingModels property to true
- Supervisor - process monitor for the Linux operating system, will automatically restart queue:work process if it fails
sudo apt-get install supervisor
- configuration files are typically stored in the /etc/supervisor/conf.d
- create any number of configuration files that instruct supervisor how processes should be monitored, for example, laravel-worker.conf file that run 8 queue:work processes and monitor all of them, automatically restarting them if they fail, change the queue:work sqs portion of the command directive to reflect desired queue connection
-
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /home/forge/app.com/artisan queue:work sqs --sleep=3 --tries=3
autostart=true
autorestart=true
user=forge
numprocs=8
redirect_stderr=true
redirect_stderr=true
stdout_logfile=/home/forge/app.com/worker.log
- update configuration and start the processes:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*
- Horizon - dashboard and configuration system for Redis powered queues
// --- config/queue.php
return [
'default' => env('QUEUE_CONNECTION', 'sync'),
// connection to a backend service
'connections' => [
'sync' => [
'driver' => 'sync',
],
'database' => [
'driver' => 'database',
'table' => env('QUEUE_TABLE', 'jobs'),
'queue' => 'default', // queue that jobs will be dispatched to
// how many seconds the queue connection should wait
// before retrying a job that is being processed
// set to maximum number of seconds jobs should reasonably take
'retry_after' => 90,
],
// ...
'redis' => [
'driver' => 'redis',
'connection' => env('QUEUE_REDIS_CONNECTION', 'default'),
'queue' => 'default',
'retry_after' => 90,
'block_for' => null,
],
],
// behavior of failed queue job logging
// control which database and table are used to store the jobs that have failed
'failed' => [
'database' => env('DB_CONNECTION', 'mysql'),
'table' => env('QUEUE_FAILED_TABLE', 'failed_jobs'),
],
];
Drivers
// --- to use database queue driver,
// create a database table to hold the jobs
php artisan queue:table
php artisan migrate
// --- Redis
// configure a Redis database connection in config/database.php configuration file
// - Redis Cluster
// queue names must contain a key hash tag
// required in order to ensure all of the Redis keys for a given queue
// are placed into the same hash slot
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => '{default}',
'retry_after' => 90,
// driver should block for five seconds while waiting for a job to become available
'block_for' => 5,
// wait until all open db transactions have been committed before dispatching the job
// discard when transaction rolled back due to an exception
// also cause any queued event listeners, mailables, notifications, and broadcast events
// to be dispatched after all open database transactions have been committed
'after_commit' => true,
],
// --- listed queue drivers dependencies
Amazon SQS: aws/aws-sdk-php ~3.0
Beanstalkd: pda/pheanstalk ~4.0
Redis: predis/predis ~1.0
Creating
namespace App\Jobs;
use App\Podcast;
use App\AudioProcessor;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class ProcessPodcast implements ShouldQueue {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $podcast;
// create a new job instance
public function __construct(Podcast $podcast) {
$this->podcast = $podcast;
}
// execute the job
public function handle(AudioProcessor $processor) {
// process uploaded podcast...
}
// --- specify connection as a property, alternative to onConnection
public $connection = 'sqs';
// or, Artisan command: php artisan queue:work --tries=3
// alternative, time at which the job should timeout
// allows a job to be attempted any number of times within a given time frame
public function retryUntil() {
return now()->addSeconds(5);
}
// delete the job if its models no longer exist
public $deleteWhenMissingModels = true;
// job was failed to process
public function failed(Exception $exception) {
// send user notification of failure, etc...
}
// --- RATE LIMITING
// --- maximum number of attempts
public $tries = 5;
// --- maximum number of unhandled exceptions to allow before failing
public $maxExceptions = 3;
// --- number of seconds a job should be allowed to run on the job class itself
// optimized for PHP 7.1+ and the pcntl PHP extension
public $timeout = 120; // OR, Artisan command: php artisan queue:work --timeout=30
public function handle() {
// --- release - manually release job back onto the queue for later attempt
this->release();
this->release(10); // make job available for processing until seconds has elapsed
// fail - mark a job as failed
$this->fail();
$this->fail($exception);
// --- throttle - throttle queued jobs by time or concurrency
// for application which interacts with Redis
// throttle job to only run 10 times every 60 seconds
// if a lock can not be obtained,
// release the job back onto the queue so it can be retried later
// key - any string that uniquely identifies the type of job to rate limit
// releasing a throttled job back onto the queue
// will still increment total number of attempts
Redis::throttle('key')
->allow(10)
->every(60)
->then(function () {
// job logic...
}, function () {
// could not obtain lock...
return $this->release(10);
});
// --- funnel - maximum number of workers that may simultaneously process job
// helpful when a queued job is modifying a resource
// that should only be modified by one job at a time
Redis::funnel('key')
->limit(1)
->then(function () {
// job logic...
}, function () {
// could not obtain lock...
return $this->release(10);
});
// combine rate limiting with time based attempts
// to determine number of attempts correctly
}
}
// --- SERVICE PROVIDER
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Jobs\ProcessPodcast;
use Illuminate\Support\Facades\Queue;
use Illuminate\Queue\Events\JobFailed;
use Illuminate\Queue\Events\JobProcessed;
use Illuminate\Queue\Events\JobProcessing;
class AppServiceProvider extends ServiceProvider {
// ...
public function boot() {
// --- bindMethod
// take total control over how the container injects dependencies into the handle method
// call this method from a service provider
$this->app->bindMethod(
ProcessPodcast::class.'@handle',
function ($job, $app) { // callback which receives the job
return $job->handle($app->make(AudioProcessor::class));
}
);
// --- failing - register an event that will be called when a job fails
// notify team via email or Slack
Queue::failing(function (JobFailed $event) {
// $event->connectionName
// $event->job
// $event->exception
});
// --- before , after
// specify callbacks to be executed before or after a queued job is processed
// perform additional logging or increment statistics for a dashboard
Queue::before(function (JobProcessing $event) {
// $event->connectionName
// $event->job
// $event->job->payload()
});
Queue::after(function (JobProcessed $event) {
// $event->connectionName
// $event->job
// $event->job->payload()
});
// --- looping
// specify callbacks executed before worker attempts to fetch a job from a queue
// rollback any transactions that were left open by a previously failed job:
Queue::looping(function () {
while (DB::transactionLevel() > 0) {
DB::rollBack();
}
});
}
// ...
}
Dispatching
namespace App\Http\Controllers;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class PodcastController extends Controller {
// store a new podcast
public function store(Request $request) {
$podcast = Podcast::create(...);
// --- dispatch - send to the default queue, dispatch jobs from anywhere
ProcessPodcast::dispatch($podcast);
// conditionally dispatch a job
ProcessPodcast::dispatchIf($accountActive, $podcast);
ProcessPodcast::dispatchUnless($accountSuspended, $podcast);
// OR, with facade, uncomment $app->withFacades() in bootstrap/app.php file:
Queue::push(new ExampleJob);
// --- delay - job should not be available for processing
// until 10 minutes after it has been dispatched
// Amazon SQS queue service has a maximum delay time of 15 minutes
ProcessPodcast::dispatch($podcast)
->delay(now()->addMinutes(10));
// --- dispatchAfterResponse method delay dispatching until after the HTTP response is sent to browser
// typically only for jobs that take about a second, such as sending an email
// processed within the current HTTP request
// do not require a queue worker to be running
// use App\Jobs\SendNotification;
SendNotification::dispatchAfterResponse();
// chain the afterResponse method onto the dispatch helper
// use App\Mail\WelcomeMessage;
// use Illuminate\Support\Facades\Mail;
dispatch(function () {
Mail::to('taylor@example.com')->send(new WelcomeMessage);
})->afterResponse();
// --- dispatchSync - run job immediately within the current process
// job will not be queued
ProcessPodcast::dispatchSync($podcast);
// specific job behavior on db transactions
ProcessPodcast::dispatch($podcast)->afterCommit();
ProcessPodcast::dispatch($podcast)->beforeCommit();
// --- dispatch a Closure
// for quick, simple tasks to be executed outside of the current request cycle
// Closure code contents is cryptographically signed, can not be modified in transit
$podcast = App\Podcast::find(1);
dispatch(function () use ($podcast) {
$podcast->publish();
});
// --- chain method, provided by the Bus facade
// specify a list of queued jobs that should be run in sequence
// if one job in the sequence fails, the rest of the jobs will not be run
// use App\Jobs\OptimizePodcast;
// use App\Jobs\ProcessPodcast;
// use App\Jobs\ReleasePodcast;
// use Illuminate\Support\Facades\Bus;
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
new ReleasePodcast,
])->dispatch();
// chain closures
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
function () {
Podcast::update(...);
},
])->dispatch();
// deleting jobs using the $this->delete() method
// will not prevent chained jobs from being processed
// chain will only stop executing if a job in the chain fails
// --- onConnection/onQueue specify the queue connection and queue name that used
// unless the queued job is explicitly assigned a different connection/queue
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
new ReleasePodcast,
])->onConnection('redis')->onQueue('podcasts')->dispatch();
// --- catch - specify closure invoked if chain fails
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
new ReleasePodcast,
])->catch(function (Throwable $e) {
// a job within the chain has failed...
})->dispatch();
// --- onQueue - pushing jobs to different queues
// "categorize" queued jobs and even prioritize
// how many workers you assign to various queues
// this does not push jobs to different queue "connections"
// as defined by queue configuration file,
// but only to specific queues within a single connection
ProcessPodcast::dispatch($podcast)->onQueue('processing');
// --- onConnection - specify which connection to push a job to
ProcessPodcast::dispatch($podcast)->onConnection('sqs');
// specify the connection and the queue for a job
ProcessPodcast::dispatch($podcast)
->onConnection('sqs')
->onQueue('processing');
// --- retryUntil, also can be chained and used like in job class
}
}
Job Batching
Container/Provider
- service container - tool for managing class dependencies and performing dependency injection
- Laravel\Lumen\Application instance (default app) is an extension of Illuminate\Container\Container, it may be treated as the service container for application
- since they are injected, we are able to easily swap it out with another implementation, "mock", or create a dummy implementation when testing application
- almost all service container bindings will be registered within service providers, use the bind, singleton, instance, and other container methods provided by the container
- no need to bind classes into the container if they do not depend on any interfaces, container does not need to be instructed on how to build these objects, since it can automatically resolve these objects using reflection
- bootstraping - registering things, including registering service container bindings, event listeners, middleware, and even routes
- service providers are the central place to configure application, extend the Illuminate\Support\ServiceProvider
- in Lumen, use $app->register() in bootstrap/app.php to register required service providers
- within a service provider, access container via the $this->app property
php artisan make:provider RiakServiceProvider
- most service providers contain a register and a boot method
- register - only bind things into the service container, never attempt to register any event listeners, routes, or any other piece of functionality there, otherwise, you may accidentally use a service that is provided by a service provider which has not loaded yet
- boot - called after all other service providers have been registered, meaning you have access to all other services that have been registered by the framework, you can register here a view composer
- config/app.php contains providers array, all of the service provider classes that will be loaded for application, many of these are "deferred" providers, they will not be loaded on every request, only when are actually needed
- defer provider registration until one of the registered bindings is actually needed, will improve the performance of application, since it is not loaded from the filesystem on every request, do this if he is only registering bindings in the service container
Container
// --- make - resolve a class instance out of the container
$api = $this->app->make('HelpSpot\API');
$api = resolve('HelpSpot\API'); // when no access to the $app
// --- makeWith - when class dependencies are not resolvable via the container
$api = $this->app->makeWith('HelpSpot\API', ['id' => 1]);
$instance = app(Something::class);
// --- resolving - listen to event fired each time it resolves an object
$this->app->resolving(function ($object, $app) {
// called when container resolves object of any type...
});
$this->app->resolving(HelpSpot\API::class, function ($api, $app) {
// called when container resolves objects of type "HelpSpot\API"...
});
namespace App\Http\Controllers;
use App\User;
use App\Repositories\UserRepository;
use App\Http\Controllers\Controller;
class UserController extends Controller {
// user repository implementation
protected $users;
// create a new controller instance
// inject a service that is able to retrieve users
// for queued jobs, type-hint dependencies in the handle method
public function __construct(UserRepository $users) {
$this->users = $users;
}
// show the profile for the given user
public function show($id) {
$user = $this->users->find($id);
return view('user.profile', ['user' => $user]);
}
}
// --- type-hint the PSR-11 container interface to obtain container instance
use Psr\Container\ContainerInterface;
// ...
Route::get('/', function (ContainerInterface $container) {
$service = $container->get('Service');
// ...
});
Provider
namespace App\Providers;
use Riak\Connection;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Routing\ResponseFactory;
class RiakServiceProvider extends ServiceProvider {
// --- register
// only bind things into the service container
public function register() {
// --- bind - register a binding, pass class or interface name
$this->app->bind('HelpSpot\API', function ($app) {
// received container itself as an argument to the resolver
// use to resolve sub-dependencies of the object we are building
return new HelpSpot\API($app->make('HttpClient'));
});
// --- singleton - bind class or interface that should only be resolved one time
// once resolved, same object instance will be returned on subsequent calls
$this->app->singleton(Connection::class, function ($app) {
return new Connection(config('riak'));
});
$this->app->singleton('HelpSpot\API', function ($app) {
return new HelpSpot\API($app->make('HttpClient'));
});
// --- instance - bind an existing object instance into the container
// instance will always be returned on subsequent calls into the container
$api = new HelpSpot\API(new HttpClient); // ...
$this->app->instance('HelpSpot\API', $api);
// --- contextual binding - inject any value class may need
// when class that receives some injected classes, also needs an injected primitive
$this->app->when('App\Http\Controllers\UserController')
->needs('$variableName')
->give($value);
// inject different implementations, for classes that utilize the same interface
$this->app->when(PhotoController::class)
->needs(Filesystem::class)
->give(function () {
return Storage::disk('local');
});
$this->app->when([VideoController::class, UploadController::class])
->needs(Filesystem::class)
->give(function () {
return Storage::disk('s3');
});
// --- tag - resolve all of a certain "category" of binding via the tagged method
$this->app->bind('SpeedReport', function () { /* ... */ });
$this->app->bind('MemoryReport', function () { /* ... */ });
$this->app->tag(['SpeedReport', 'MemoryReport'], 'reports');
$this->app->bind('ReportAggregator', function ($app) {
return new ReportAggregator($app->tagged('reports'));
});
// --- extend - modification of resolved services
// run additional code to decorate or configure the service
$this->app->extend(Service::class, function ($service) {
return new DecoratedService($service);
});
}
// --- bootstrap any application services
public function boot() {
view()->composer('view', function () {
// ...
});
}
// type-hint dependencies for service provider boot method
// service container will automatically inject any dependencies need:
public function boot(ResponseFactory $response) {
$response->macro('caps', function ($value) {
// ...
});
}
// --- bindings , singletons
// for many simple bindings, instead of manual registration
// when the service provider is loaded by the framework,
// automatically check for these properties and register their bindings
public $bindings = [ // bindings that should be registered
ServerProvider::class => DigitalOceanServerProvider::class,
];
public $singletons = [ // singletons that should be registered
DowntimeNotifier::class => PingdomDowntimeNotifier::class,
ServerToolsProvider::class => ServerToolsProvider::class,
];
}
Deferred Provider
namespace App\Providers;
use Riak\Connection;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Support\DeferrableProvider;
class RiakServiceProvider extends ServiceProvider implements DeferrableProvider {
// register any application services
public function register() {
$this->app->singleton(Connection::class, function ($app) {
return new Connection($app['config']['riak']);
});
}
// get the services provided by the provider
public function provides() {
// return the service container bindings registered by the provider
return [Connection::class];
}
}
// --- config/app.php
'providers' => [
// other service providers...
App\Providers\ComposerServiceProvider::class,
],
Testing
- unit tests are focused on a very small, isolated portion of code, most unit tests probably focus on a single method, feature tests may test a larger portion of code, including how several objects interact with each other or even a full HTTP request to a JSON endpoint
- phpunit.xml - PHPUnit setup, define testing environment configuration values as necessary
- create a .env.testing file in project root, file will override the .env file when running PHPUnit tests or executing Artisan commands with the --env=testing
php artisan test
- any arguments that can be passed to the phpunit
command may also be passed
- Tests
- ExampleTest.php file is provided in the tests directory, create a new test file in the tests directory, Laravel will contain Feature and Unit directories inside
- test class should extend TestCase, define test methods as you normally would using PHPUnit, if you define own setUp method within a test class, be sure to call parent::setUp
php artisan make:test UserTest
- create a test in the Laravel tests/Feature (or tests, in Lumen) directory
php artisan make:test UserTest --unit
- create a test in the Laravel tests/Unit directory
- run
phpunit
- command to run tests
- clear configuration cache using the
config:clear
command before running tests
- Factories
php artisan make:factory PostFactory
- create a factory, placed in database/factories directory
php artisan make:factory PostFactory --model=Post
- pre-fill the generated factory file with the given model
- factory Closure receives Faker instance to generate various kinds of random data for testing
- set the locale by adding a faker_locale option to config/app.php
composer require laravel/legacy-factories
- use Laravel <=7 factories with Laravel 8+, https://laravel.com/docs/8.x/upgrade#seeder-factory-namespaces
- CSRF middleware is automatically disabled when running tests
- Browser Tests (Laravel Dusk)
// namespace Tests\Feature;
use Tests\TestCase;
use Laravel\Lumen\Testing\DatabaseMigrations;
use Laravel\Lumen\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Foundation\Testing\WithoutMiddleware;
class ExampleTest extends TestCase {
use DatabaseMigrations; // rollback db after each test and migrate it before the next test
use DatabaseTransactions; // wrap every test case in a db transaction
use RefreshDatabase; // reset db after each tests
$this->seed(); // run DatabaseSeeder
$this->seed(OrderStatusSeeder::class); // run specific seeder
// OR instruct RefreshDatabase trait to automatically seed the db before each test
protected $seed = true;
protected $seeder = OrderStatusSeeder::class;
public function testSomethingIsTrue() {
$this->assertTrue(true);
// modify the time returned by helpers
// such as 'now' or Illuminate\Support\Carbon::now()
// travel into the future...
$this->travel(5)->milliseconds();
// ...
$this->travel(5)->years();
// travel into the past
$this->travel(-5)->hours();
// travel to an explicit time
$this->travelTo(now()->subHours(6));
// return back to the present time
$this->travelBack();
}
public function testHttpExample() {
// --- get, post, put, patch, delete
$response = $this->get('/');
$response->assertStatus(200);
// --- withHeaders - add any custom headers to the request
$response = $this->withHeaders([
'X-Header' => 'Value',
])->json('POST', '/user', ['name' => 'Sally']);
// --- dump , dumpHeaders - examine and debug the response contents
$response->dumpHeaders();
$response->dump();
// json --- assert that a given array was returned in JSON format
// assertJson , seeJson --- convert given array into JSON,
// then verifies that the JSON fragment occurs anywhere
// within the entire JSON response returned by the application
// if there are other properties in the JSON response,
// test will still pass as long as the given fragment is present
$this->json('POST', '/user', ['name' => 'Sally'])
->assertStatus(201)
->assertJson([
// ->seeJson([
'created' => true,
]);
// --- assertExactJson , seeJsonEquals - exact match for the returned JSON
$this->post('/user', ['name' => 'Sally'])
// ->seeJsonEquals([
->assertExactJson([
'created' => true,
]);
// --- withSession - loading the session with data before issuing a request
$response = $this->withSession(['foo' => 'bar'])
->get('/');
// --- actingAs - authenticate a given user as the current user
$user = factory('App\User')->create();
$this->actingAs($user)->get('/user');
// specify which guard should be used to authenticate
$this->actingAs($user, 'api')
$response = $this->actingAs($user)
->withSession(['foo' => 'bar'])
->get('/');
// --- call - custom HTTP request into app and full Illuminate\Http\Response object
$response = $this->call('GET', '/');
$this->assertEquals(200, $response->status());
// pass an array of input data with the request
// will be available in routes and controller via the Request instance
$response = $this->call('POST', '/user', ['name' => 'Taylor']);
}
public function testWithAuthExample() {
// user is authenticated
$this->assertAuthenticated($guard = null);
// user is not authenticated
$this->assertGuest($guard = null);
// user is authenticated
$this->assertAuthenticatedAs($user, $guard = null);
// redentials are valid
$this->assertCredentials(array $credentials, $guard = null);
// given credentials are invalid
$this->assertInvalidCredentials(array $credentials, $guard = null);
}
public function testDbExample() {
// --- make - create model with factory but dont save to database
$user = factory('App\User')->make();
// replace default values
$user = factory('App\User')->make([
'name' => 'Abigail',
]);
$users = factory('App\User', 3)->make(); // three App\User instances
$user = factory('App\User', 'admin')->make(); // an App\User "admin" instance
$users = factory('App\User', 'admin', 3)->make(); // three App\User "admin" instances
// --- states - apply states
$users = factory(App\User::class, 5)->states('delinquent')->make();
$users = factory(App\User::class, 5)->states('premium', 'delinquent')->make();
// --- create - create model and save to the database using Eloquent save
$user = factory('App\User')->create();
$users = factory(App\User::class, 3)->create();
$user = factory(App\User::class)->create([
'name' => 'Abigail',
]);
// --- relationships
// persist multiple models to the database
// create method returns Eloquent collection instance
// attach a relation to the created models
// use any of the convenient functions provided by the collection, such as each
$users = factory('App\User', 3)
->create()
->each(function($u) {
$u->posts()->save(factory('App\Post')->make());
});
// --- assertDatabaseCount
// assert that a table in the db contains the given number of records
$this->assertDatabaseCount('users', 5);
// --- assertDatabaseHas , seeInDatabase
// data exists in the db matching a given set of criteria
$this->seeInDatabase('users', ['email' => 'sally@foo.com']);
// --- table in the db does not contain the given data
$this->assertDatabaseMissing($table, array $data);
// --- assertModelMissing, model has been deleted from the database:
// use App\Models\User;
$user = User::find(1);
$user->delete();
$this->assertModelMissing($user);
// --- record has been soft deleted
$this->assertSoftDeleted($table, array $data);
// use models in tests...
}
}
HTTP Asserts
// --- JSON
// response contains an exact match of the given JSON data
// requires numeric keys of compared arrays to match and be in the same order
$response->assertExactJson(array $data);
// assertSimilarJson - without requiring numerically keyed arrays to have the same order
// response contains the given JSON data
$response->assertJson(array $data);
// response JSON has an array with the expected number of items at the given key
$response->assertJsonCount($count, $key = null);
// response contains the given JSON fragment
$response->assertJsonFragment(array $data);
// response does not contain the given JSON fragment
$response->assertJsonMissing(array $data);
// response does not contain the exact JSON fragment
$response->assertJsonMissingExact(array $data);
// response has no JSON validation errors for the given keys
$response->assertJsonMissingValidationErrors($keys);
// response has a given JSON structure
$response->assertJsonStructure(array $structure);
// response has the given JSON validation errors
$response->assertJsonValidationErrors(array $data);
// --- STATUS
// response has a given code
$response->assertStatus($code);
// response has a forbidden status code
$response->assertForbidden();
// response has a not found status code
$response->assertNotFound();
// response has a 200 status code
$response->assertOk();
// response has a successful status code
$response->assertSuccessful();
// --- HEADER , URI
// header is present on the response
$response->assertHeader($headerName, $value = null);
// header is not present on the response
$response->assertHeaderMissing($headerName);
// response has the given URI value in the Location header
$response->assertLocation($uri);
// response is a redirect to a given URI
$response->assertRedirect($uri);
// --- STRING
// string is contained within the response
$response->assertSee($value, $escape_value = true);
// strings are contained in order within the response
$response->assertSeeInOrder(array $values);
// string is contained within the response text
$response->assertSeeText($value);
// strings are contained in order within the response text
$response->assertSeeTextInOrder(array $values);
// string is not contained within the response
$response->assertDontSee($value);
// string is not contained within the response text
$response->assertDontSeeText($value);
// --- VIEW
// response view was given a piece of data
$response->assertViewHas($key, $value = null);
// response view has a given list of data
$response->assertViewHasAll(array $data);
// given view was returned by the route
$response->assertViewIs($value);
// response view is missing a piece of bound data
$response->assertViewMissing($key);
// --- COOKIE
// response contains the given cookie
$response->assertCookie($cookieName, $value = null);
// response contains the given cookie and it is expired
$response->assertCookieExpired($cookieName);
// response contains the given cookie and it is not expired
$response->assertCookieNotExpired($cookieName);
// response does not contains the given cookie
$response->assertCookieMissing($cookieName);
// response contains the given cookie (unencrypted)
$response->assertPlainCookie($cookieName, $value = null);
// --- SESSION
// session contains the given piece of data
$response->assertSessionHas($key, $value = null);
// session has a given list of values
$response->assertSessionHasAll(array $data);
// session contains an error for the given field
$response->assertSessionHasErrors(array $keys, $format = null, $errorBag = 'default');
// session has the given errors
$response->assertSessionHasErrorsIn($errorBag, $keys = [], $format = null);
// session has no errors
$response->assertSessionHasNoErrors();
// session has no errors for the given keys
$response->assertSessionDoesntHaveErrors($keys = [], $format = null, $errorBag = 'default');
// session does not contain the given key
$response->assertSessionMissing($key);
Mocking, faking
// --- expectsEvents - prevents expected events running any handlers
$this->expectsEvents('App\Events\UserRegistered');
// --- withoutEvents - prevent all event handlers from running
$this->withoutEvents();
// --- expectsJobs - dispatch but dont execute expected jobs
// only detects jobs that are dispatched via the dispatch functions
// does not detect jobs that are sent directly to Queue::push
$this->expectsJobs('App\Jobs\PurchasePodcast');
// --- shouldReceive - mock the call to the facade
// dont mock the Request facade, pass the desired input into the HTTP helper
// such as call and post when running test
Cache::shouldReceive('get')
->once()
->with('key')
->andReturn('value');
$this->get('/users');
// --- mocking objects
// when mocking an object that is going to be injected into application via service container,
// bind mocked instance into the container as an instance binding,
// will instruct the container to use mocked instance of the object
// instead of constructing the object itself:
use Mockery;
use App\Service;
// ...
$this->instance(Service::class, Mockery::mock(Service::class, function ($mock) {
$mock->shouldReceive('process')->once();
}));
// --- mock - for convenience
use App\Service;
// ...
$this->mock(Service::class, function ($mock) {
$mock->shouldReceive('process')->once();
});
// --- Bus::fake() - alternative to mocking, prevent jobs from being dispatched
// use Illuminate\Support\Facades\Bus;
Bus::fake();
// ...perform order shipping...
Bus::assertDispatched(ShipOrder::class, function ($job) use ($order) {
return $job->order->id === $order->id;
});
// job was not dispatched
Bus::assertNotDispatched(AnotherJob::class);
// --- Storage::fake - Illuminate\Http\UploadedFile class method
// used to generate dummy files or images for testing
// combine to test an avatar upload form
// use Illuminate\Support\Facades\Storage;
Storage::fake('avatars');
$file = UploadedFile::fake()->image('avatar.jpg');
// specify the width, height, and size of the image
// to better test validation rules
$file = UploadedFile::fake()->image('avatar.jpg', $width, $height)->size(100);
// create files of any other type
$file = UploadedFile::fake()->create('document.pdf', $sizeInKilobytes);
$response = $this->json('POST', '/avatar', [
'avatar' => $file,
]);
Storage::fake('photos');
$response = $this->json('POST', '/photos', [
UploadedFile::fake()->image('photo1.jpg'),
UploadedFile::fake()->image('photo2.jpg')
]);
// --- assertExists - file was stored
Storage::disk('photos')->assertExists('photo1.jpg');
Storage::disk('photos')->assertExists(['photo1.jpg', 'photo2.jpg']);
// --- assertMissing - file does not exist
Storage::disk('photos')->assertMissing('missing.jpg');
Storage::disk('photos')->assertMissing(['missing.jpg', 'non-existing.jpg']);
// --- persistentFake - dont delete files from temporary directory (like fake())
// --- Mail::fake() - prevent mail from being sent, as testOrderShipping()
// assert that mailables were sent to users and inspect the data they received
// use Illuminate\Support\Facades\Mail;
Mail::fake();
// no mailables were sent
Mail::assertNothingSent();
// ...perform order shipping...
Mail::assertSent(OrderShipped::class, function ($mail) use ($order) {
return $mail->order->id === $order->id;
});
// message was sent to the given users
Mail::assertSent(OrderShipped::class, function ($mail) use ($user) {
return $mail->hasTo($user->email) &&
$mail->hasCc('...') &&
$mail->hasBcc('...');
});
// mailable was sent twice...
Mail::assertSent(OrderShipped::class, 2);
// mailable was not sent...
Mail::assertNotSent(AnotherMailable::class);
// if queueing mailables for delivery in the background,
// use the assertQueued method instead of assertSent:
Mail::assertQueued(...);
Mail::assertNotQueued(...);
// --- Event::fake() - prevent all event listeners from executing
// assert that events were dispatched and inspect the data they received
// use Illuminate\Support\Facades\Event;
Event::fake();
// ...perform order shipping...
Event::assertDispatched(OrderShipped::class, function ($e) use ($order) {
return $e->order->id === $order->id;
});
// event was dispatched twice
Event::assertDispatched(OrderShipped::class, 2);
// event was not dispatched
Event::assertNotDispatched(OrderFailedToShip::class);
// after calling Event::fake(), no event listeners will be executed
// if tests use model factories that rely on events,
// such as creating a UUID during a model creating event,
// call Event::fake() after using factories
// --- fake event listeners for a specific set of event
Event::fake([
OrderCreated::class,
]);
$order = factory(Order::class)->create();
Event::assertDispatched(OrderCreated::class);
// events are dispatched as normal...
$order->update([...]);
// --- Event::fakeFor - scoped Event Fakes
// fake event listeners for a portion of test
$order = Event::fakeFor(function () {
$order = factory(Order::class)->create();
Event::assertDispatched(OrderCreated::class);
return $order;
});
// events are dispatched as normal and observers will run
$order->update([...]);
// --- Notification::fake() - prevent notifications from being sent, as testOrderShipping()
// assert that notifications were sent to users and inspect the data they received
// use Illuminate\Support\Facades\Notification;
Notification::fake();
// Assert that no notifications were sent...
Notification::assertNothingSent();
// ...perform order shipping...
Notification::assertSentTo(
$user,
OrderShipped::class,
function ($notification, $channels) use ($order) {
return $notification->order->id === $order->id;
}
);
// notification was sent to the given users
Notification::assertSentTo(
[$user], OrderShipped::class
);
// notification was not sent
Notification::assertNotSentTo(
[$user], AnotherNotification::class
);
// notification was sent via Notification::route() method
Notification::assertSentTo(
new AnonymousNotifiable, OrderShipped::class
);
// --- Queue::fake() - prevent jobs from being queued, as testOrderShipping()
// assert that jobs were pushed to the queue and inspect the data they received
// use Illuminate\Support\Facades\Queue;
Queue::fake();
// no jobs were pushed
Queue::assertNothingPushed();
// ...perform order shipping...
Queue::assertPushed(ShipOrder::class, function ($job) use ($order) {
return $job->order->id === $order->id;
});
// job was pushed to a given queue
Queue::assertPushedOn('queue-name', ShipOrder::class);
// job was pushed twice
Queue::assertPushed(ShipOrder::class, 2);
// job was not pushed
Queue::assertNotPushed(AnotherJob::class);
// job was pushed with a specific chain...
Queue::assertPushedWithChain(ShipOrder::class, [
AnotherJob::class,
FinalJob::class
]);
Model Factories (Laravel <=7)
//app/database/factories/ModelFactory.php
$factory->define(App\User::class, function (Faker\Generator $faker) {
return [
'name' => $faker->name,
'email' => $faker->email,
];
});
// ---defineAs - define additional factories
// "Administrator" users in addition to normal users
$factory->defineAs('App\User', 'admin', function ($faker) {
return [
'name' => $faker->name,
'email' => $faker->email,
'admin' => true,
];
});
// --- raw - retrieve the base attributes
// instead of duplicating all of the attributes from base user factory
$factory->defineAs('App\User', 'admin', function ($faker) use ($factory) {
$user = $factory->raw('App\User');
return array_merge($user, ['admin' => true]);
});
// --- state
// define modifications that can be applied to model factories in any combination
$factory->state(App\User::class, 'delinquent', [
'account_status' => 'delinquent',
]);
$factory->state(App\User::class, 'address', function ($faker) {
return [
'address' => $faker->address,
];
});
// --- afterMaking , afterCreating
// perform additional tasks after making or creating a model
$factory->afterMaking(App\User::class, function ($user, $faker) {
// ...
});
$factory->afterCreating(App\User::class, function ($user, $faker) {
$user->accounts()->save(factory(App\Account::class)->make());
});
// callbacks for factory states
$factory->afterMakingState(App\User::class, 'delinquent', function ($user, $faker) {
// ...
});
$factory->afterCreatingState(App\User::class, 'delinquent', function ($user, $faker) {
// ...
});
// --- relationships
// create a new User instance when creating a Post
$factory->define(App\Post::class, function ($faker) {
return [
'title' => $faker->title,
'content' => $faker->paragraph,
'user_id' => function () {
return factory(App\User::class)->create()->id;
}
];
});
// receive the evaluated attribute array of the factory that defines them
$factory->define(App\Post::class, function ($faker) {
return [
'title' => $faker->title,
'content' => $faker->paragraph,
'user_id' => function () {
return factory(App\User::class)->create()->id;
},
'user_type' => function (array $post) {
return App\User::find($post['user_id'])->type;
}
];
});
Model Factories (Laravel 8+)
--- RELATIONSHIPS
// --- hasMany - user that has three posts
// Laravel will assume that User model must have a 'posts' method that defines the relationship
use App\Models\Post;
use App\Models\User;
// ...
$user = User::factory()
->has(Post::factory()->count(3))
->create();
// explicitly specify the name of the relationship to manipulate
$user = User::factory()
->has(Post::factory()->count(3), 'posts')
->create();
// state manipulations on the related models
$user = User::factory()
->has(
Post::factory()
->count(3)
->state(function (array $attributes, User $user) {
return ['user_type' => $user->type];
})
)->create();
// - magic methods, use convention to determine related models relationship method
$user = User::factory()
->hasPosts(3) // related models created via 'posts' relationship method on User model
->create();
$user = User::factory()
->hasPosts(3, [
'published' => false,
])->create();
$user = User::factory()
->hasPosts(3, function (array $attributes, User $user) {
return ['user_type' => $user->type];
})->create();
// --- belongsTo, inverse of hasMany
use App\Models\Post;
use App\Models\User;
// ...
$posts = Post::factory()
->count(3)
->for(User::factory()->state([
'name' => 'Jessica Archer',
]))->create();
// pass parent model instance
$user = User::factory()->create();
$posts = Post::factory()
->count(3)
->for($user)
->create();
// - magic methods, use convention to determine related models relationship method
$posts = Post::factory()
->count(3)
->forUser([ // posts should belong to the user relationship on the Post model
'name' => 'Jessica Archer',
])->create();
// --- belongsToMany, many to many
use App\Models\Role;
use App\Models\User;
// ...
$user = User::factory()
->has(Role::factory()->count(3))
->create();
// attributes set on pivot/intermediate table linking the models
$user = User::factory()
->hasAttached(
Role::factory()->count(3),
['active' => true]
)->create();
// state transformation
$user = User::factory()
->hasAttached(
Role::factory()
->count(3)
->state(function (array $attributes, User $user) {
return ['name' => $user->name.' Role'];
}),
['active' => true]
)->create();
// three roles will be attached to all three users
$roles = Role::factory()->count(3)->create();
$user = User::factory()
->count(3)
->hasAttached($roles, ['active' => true])
->create();
// - magic methods, use convention to determine related models relationship method
$user = User::factory()
->hasRoles(1, [
'name' => 'Editor'
])
->create();
--- RELATIONSHIPS WITHIN FACTORIES
// assign a new factory instance to foreign key of relationship
// normally done for the 'inverse' relationships: belongsTo and morphTo
// create new user when creating post:
use App\Models\User;
// ...
public function definition() {
return [
'user_id' => User::factory(),
'title' => $this->faker->title(),
'content' => $this->faker->paragraph(),
];
}
// if the relationship columns depend on factory that defines it, assign closure to attribute
// closure will receive the factory evaluated attribute array
public function definition() {
return [
'user_id' => User::factory(),
'user_type' => function (array $attributes) {
return User::find($attributes['user_id'])->type;
},
'title' => $this->faker->title(),
'content' => $this->faker->paragraph(),
];
}
--- POLYMORPHIC RELATIONSHIPS
// 'morph many' relationships are created same way as typical hasMany relationships
use App\Models\Post;
// ...
$post = Post::factory()->hasComments(3)->create(); // Post has morphMany with Comment
// --- morphTo, NO magic methods
// 'for' method must be used directly and relationship name must be explicitly provided
// Comment has 'commentable' method that defines a morphTo relationship
// create three comments that belong to single post by using the for method directly:
$comments = Comment::factory()->count(3)->for(
Post::factory(), 'commentable'
)->create();
// --- morphToMany, morphedByMany - created like non-polymorphic belongsToMany
use App\Models\Tag;
use App\Models\Video;
// ...
$videos = Video::factory()
->hasAttached(
Tag::factory()->count(3),
['public' => true]
)
->create();
// magic methods, use convention to determine related models relationship method
$videos = Video::factory()
->hasTags(3, ['public' => true])
->create();
Console Tests
// --- expectsQuestion - "mock" user input for console commands
// --- assertExitCode - specify the exit code and expected text from console command
Artisan::command('question', function () {
$name = $this->ask('What is your name?');
$language = $this->choice('Which language do you prefer?', [
'PHP',
'Ruby',
'Python',
]);
$this->line('Your name is '.$name.' and you prefer '.$language.'.');
});
// --- expectsQuestion , expectsOutput , assertExitCode - test command
// test a console command
public function test_console_command() {
$this->artisan('question')
->expectsQuestion('What is your name?', 'Taylor Otwell')
->expectsQuestion('Which language do you prefer?', 'PHP')
->expectsOutput('Your name is Taylor Otwell and you prefer PHP.')
->doesntExpectOutput('Your name is Taylor Otwell and you prefer Ruby.')
->assertExitCode(0);
}
// --- expectsConfirmation - command which expects confirmation in the form of a "yes" or "no" answer
$this->artisan('module:import')
->expectsConfirmation('Do you really wish to run this command?', 'no')
->assertExitCode(1);
// --- expectsTable
$this->artisan('users:all')
->expectsTable([
'ID',
'Email',
], [
[1, 'taylor@example.com'],
[2, 'abigail@example.com'],
]);
Validation
- by default, base controller class uses a ValidatesRequests trait which provides a convenient method to validate incoming HTTP request with a variety of powerful validation rules
- if the incoming request parameters do not pass the given validation rules, Laravel will automatically redirect the user back to their previous location, in addition, all of the validation errors will automatically be flashed to the session
- during an AJAX request validate method will generate JSON response containing all of the validation errors sent with a 422 HTTP status code
- $errors variable is bound to the view by the Illuminate\View\Middleware\ShareErrorsFromSession middleware provided by the web middleware group, when applied, an $errors variable will always be available in views
php artisan make:request StoreBlogPost
- form request (not supported by Lumen), for more complex validation scenarios in Laravel, custom request classes that contain validation logic, will be placed in the app/Http/Requests directory, add validation rules to the rules method, all form requests extend the base Laravel request class, use the user method to access the currently authenticated user
- in Lumen
- $this->validate helper will always return a JSON response with the relevant error messages
- Lumen provides access to the validate method from within Route closures
- for exists or unique validation rules, uncomment the $app->withEloquent() method call in bootstrap/app.php
- $errors view variable that is available (Lumen does not support sessions out of the box), $this->validate helper will throw Illuminate\Validation\ValidationException with embedded JSON response that includes all relevant error messages
- language file example: app/vendor/laravel/lumen-framework/lang/en/validation.php
Route::get('post/create', 'PostController@create');
Route::post('post', 'PostController@store');
// ...
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Validator;
class PostController extends Controller {
// show the form to create a new blog post
public function create() {
return view('post.create');
}
// store a new blog post
public function store(Request $request) {
$validatedData = $request->validate([
'title' => 'required|unique:posts|max:255',
'body' => 'required',
]);
// --- bail
// stop running validation rules after the first validation failure
// if any rule fails, next will not be checked
$request->validate([
'title' => 'bail|required|unique:posts|max:255',
'body' => 'required',
]);
// --- mark "optional" request fields as nullable
// null values are considered invalid
// by default, Laravel includes TrimStrings and ConvertEmptyStringsToNull middleware
// in application global middleware stack
$request->validate([
'title' => 'required|unique:posts|max:255',
'body' => 'required',
'publish_at' => 'nullable|date',
]);
// --- "dot" syntax on nested parameters
$request->validate([
'title' => 'required|unique:posts|max:255',
'author.name' => 'required',
'author.description' => 'required',
]);
// validate attributes within an array
$validator = Validator::make($request->all(), [
'photos.profile' => 'required|image',
]);
// * - validate each element of an array
$validator = Validator::make($request->all(), [
'person.*.email' => 'email|unique:users',
'person.*.first_name' => 'required_with:person.*.last_name',
]);
// --- make - custom validator instance with Validator facade
// instead of validate method on the request
$validator = Validator::make(
$request->all(), // data under validation
[ // validation rules
'title' => 'required|unique:posts|max:255',
'body' => 'required',
]
);
if ($validator->fails()) {
return redirect('post/create')
// withErrors - accepts a validator, a MessageBag, or a PHP array
->withErrors($validator) // errors variable will automatically be shared
->withInput();
}
// --- validate
// automatic redirection for manually created validator instance
Validator::make($request->all(), [
'title' => 'required|unique:posts|max:255',
'body' => 'required',
])->validate();
// --- MessageBag - for multiple forms on a single page
// retrieve the error messages for a specific form
return redirect('register')
->withErrors($validator, 'login');
// access named MessageBag instance from the $errors variable:
// {{ $errors->login->first('email') }}
// --- after - attach callbacks to be run after validation is completed
// further validation and add more error messages to the message collection
$validator = Validator::make(...);
$validator->after(function ($validator) {
if ($this->somethingElseIsInvalid()) {
$validator->errors()->add('field', 'Something is wrong with this field!');
}
});
if ($validator->fails()) { /* ... */ }
// --- CONDITIONAL VALIDATION
// --- sometimes - checks against a field only if that field is present in the input array
$v = Validator::make($data, [
'email' => 'sometimes|required|email',
]);
// adding rule depending on other fields
// reason field is required when games > 100
$v = Validator::make($data, [ // static rules that never change
'email' => 'required|email',
'games' => 'required|numeric',
]);
$v->sometimes('reason', 'required|max:500', function ($input) {
return $input->games >= 100;
});
// for several fields at once
$v->sometimes(['reason', 'cost'], 'required', function ($input) {
return $input->games >= 100;
});
// --- ERROR MESSAGES
// --- first - first error message for a given field
$errors = $validator->errors();
echo $errors->first('email');
// --- get - array of all the messages for a given field
foreach ($errors->get('email') as $message) {
// ...
}
// --- * - all of the messages for each of the array elements
// when validating an array form field
foreach ($errors->get('attachments.*') as $message) {
// ...
}
// --- all - array of all messages for all fields
foreach ($errors->all() as $message) {
// ...
}
// --- has - determine if any error messages exist for a given field
if ($errors->has('email')) {
// ...
}
// --- custom error messages
$messages = [
// :attribute placeholder is replaced by the name of the field under validation
'required' => 'The :attribute field is required.',
// other placeholders in validation messages
'same' => 'The :attribute and :other must match.',
'size' => 'The :attribute must be exactly :size.',
'between' => 'The :attribute value :input is not between :min - :max.',
'in' => 'The :attribute must be one of the following types: :values',
// custom error message only for a specific field: attribute_name.rule
'email.required' => 'We need to know your e-mail address!',
];
$validator = Validator::make($input, $rules, $messages);
// store the blog post...
}
}
// --- in Lumen routes
use Illuminate\Http\Request;
$router->post('/user', function (Request $request) {
$this->validate($request, [
'name' => 'required',
'email' => 'required|email|unique:users'
]);
// Store User...
});
<!--
user will be redirected to controller "create" method when validation fails,
display the error messages in the view
/resources/views/post/create.blade.php
-->
<h1>Create Post</h1>
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<!--
... create post form ...
-->
<!--
@error Blade directive
quickly check if validation error messages exist for a given attribute
-->
<label for="title">Post Title</label>
<input id="title" type="text" class="@error('title') is-invalid @enderror">
@error('title')
<div class="alert alert-danger">{{ $message }}</div>
@enderror
Form Request Validation (Laravel)
// type-hint any dependencies within rules method signature
// they will automatically be resolved via the Laravel service container
public function rules() {
return [
'title' => 'required|unique:posts|max:255',
'body' => 'required',
];
}
// type-hint the request on controller method
// incoming form request is validated before the controller method is called
// no need to clutter controller with any validation logic
public function store(StoreBlogPost $request) {
// incoming request is valid...
// retrieve the validated input data...
$validated = $request->validated();
}
// --- withValidator method - add an "after" hook to a form request
// receives fully constructed validator,
// call any of its methods before the validation rules are actually evaluated
public function withValidator($validator) {
$validator->after(function ($validator) {
if ($this->somethingElseIsInvalid()) {
$validator->errors()->add(
'field',
'Something is wrong with this field!'
);
}
});
}
// --- authorize method
// check if the authenticated user actually has the authority to update resource
// if returns false, 403 status code returned and controller method will not execute
// determine if a user actually owns a blog comment they are attempting to update
public function authorize() {
$comment = Comment::find($this->route('comment'));
return $comment && $this->user()->can('update', $comment);
// if you have authorization logic in another part of your application:
return true;
}
// user() method also grants access to the URI parameters defined on the route
// such as the {comment} parameter: Route::post('comment/{comment}');
// --- messages method
// customize the error messages used by the form request
// should return an array of attribute/rule pairs
// and their corresponding error messages
public function messages() {
return [
'title.required' => 'A title is required',
'body.required' => 'A message is required',
];
}
// --- attributes method
// replace :attribute portion of validation message with a custom attribute name
public function attributes() {
return [
'email' => 'email address',
];
}
lang/xx/validation.php
// --- custom array - specify custom messages in a language file instead of
// passing them directly to the Validator
'custom' => [
'email' => [
'required' => 'We need to know your e-mail address!',
],
],
// --- attributes array - custom attributes in language files
// replace :attribute portion of validation message with a custom attribute name
'attributes' => [
'email' => 'email address',
],
// --- values array - custom value representation
'values' => [
'payment_type' => [
'cc' => 'credit card'
],
],
// then, for:
$request->validate([
'credit_card_number' => 'required_if:payment_type,cc'
]);
// message will be:
The credit card number field is required when payment type is credit card.
// instead of:
The credit card number field is required when payment type is cc.
// --- * - use a single validation message for array based fields
'custom' => [
'person.*.email' => [
'unique' => 'Each person must have a unique e-mail address',
]
],
Validation Rules
boolean // able to be cast as a boolean: true, false, 1, 0, "1", and "0"
alpha // entirely alphabetic characters
alpha_dash // may have alpha-numeric characters, dashes and underscores
alpha_num // entirely alpha-numeric characters
string // string, assign the nullable rule to allow the field to also be null
digits:value // numeric and must have an exact length of value
digits_between:min,max // length between the given min and max
integer // integer
numeric // numeric
email // formatted as an e-mail address
json // valid JSON string
array // PHP array
file // successfully uploaded file.
image // file under validation must be an image (jpeg, png, bmp, gif, or svg)
date // valid, non-relative date according to the strtotime PHP function
date_equals:date // equal to the given date
date_format:format // match the given format
// use either date or date_format when validating a field, not both
// dates will be passed into the PHP strtotime function
timezone // valid timezone identifier according to the timezone_identifiers_list PHP function
url // valid URL
active_url // valid A or AAAA record according to the dns_get_record PHP function
uuid // valid RFC 4122 (version 1, 3, 4, or 5) universally unique identifier (UUID)
ip // IP address
ipv4 // IPv4 address
ipv6 // IPv6 address
in_array:anotherfield.* // exist in anotherfield values
min:value // minimum value
max:value // less than or equal to a maximum value
gt:field // greater than the given field
gte:field // greater than or equal to the given field
lt:field // less than the given field
lte:field // less than or equal to the given field
// two fields must be of the same type
// strings, numerics, arrays, and files are evaluated using the same conventions as the size rule
// the name of another field under validation may be supplied as the value of date
between:min,max // size between the given min and max
// strings, numerics, arrays, and files are evaluated in the same fashion as the size rule
size:value // size matching the given value
// or string data, value corresponds to the number of characters
// for numeric data, value corresponds to a given integer value
// for an array, size corresponds to the count of the array
// for files, size corresponds to the file size in kilobytes
present // present in the input data but can be empty
filled // not be empty when it is present
same:field // given field must match the field under validation
different:field // different value than field
nullable // may be null, validating primitive such as strings and integers that can contain null values
accepted // yes, on, 1, or true, useful for validating "Terms of Service" acceptance
confirmed // matching field of foo_confirmation
// if is 'password', a matching 'password_confirmation' field must be present in the input
current_password // field under validation must match the authenticated users password
// specify an authentication guard using the rule's first parameter:
// 'password' => 'current_password:api'
required // present in the input data and not empty
// field is considered "empty" if one of the following conditions are true:
// value is null
// value is an empty string
// value is an empty array or empty Countable object
// value is an uploaded file with no path
distinct // when working with arrays, wihout duplicate values
'foo.*.id' => 'distinct'
starts_with:foo,bar,... // start with one of the given values
ends_with:foo,bar,... // end with one of the given values
in:foo,bar,... // included in the given list of values
// ...
use Illuminate\Validation\Rule;
// ...
Validator::make($data, [
'zones' => [
'required',
Rule::in(['first-zone', 'second-zone']),
],
]);
// ...
not_in:foo,bar,... // not be included in the given list of values
// Rule::notIn method may be used to fluently construct the rule:
// ...
use Illuminate\Validation\Rule;
// ...
Validator::make($data, [
'toppings' => [
'required',
Rule::notIn(['sprinkles', 'cherries']),
],
]);
// ...
after:date // value after a given date
before:date // value preceding the given date
// name of another field under validation may be supplied as the value of date
'start_date' => 'required|date|after:tomorrow'
// instead of passing a date string to be evaluated by strtotime,
// specify another field to compare against the date:
'finish_date' => 'required|date|after:start_date'
after_or_equal:date // value after or equal to the given date
before_or_equal:date // value preceding or equal to the given date
not_regex:pattern // not match the given regular expression
regex:pattern // match the given regular expression
// uses the PHP preg_match function
// pattern specified should obey the same formatting required by preg_match
// and thus also include valid delimiters: 'email' => 'not_regex:/^.+$/i'
// when using the regex / not_regex patterns, specify rules in an array
// instead of using pipe delimiters, especially if the regular expression contains a pipe character
// present and not empty ...
required_if:anotherfield,value,...
// if the anotherfield field is equal to any value
// Rule::requiredIf for more complex condition for the required_if rule
// accepts a boolean or a Closure (should return true or false to indicate if is required)
// ...
use Illuminate\Validation\Rule;
// ...
Validator::make($request->all(), [
'role_id' => Rule::requiredIf($request->user()->is_admin),
]);
Validator::make($request->all(), [
'role_id' => Rule::requiredIf(function () use ($request) {
return $request->user()->is_admin;
}),
]);
// ...
required_unless:anotherfield,value,... // unless the anotherfield field is equal to any value
required_with:foo,bar,... // only if any of the other specified fields are present
required_with_all:foo,bar,... // only if all of the other specified fields are present
required_without:foo,bar,... // only when any of the other specified fields are not present
required_without_all:foo,bar,... // only when all of the other specified fields are not present
bail // stop running validation rules after the first validation failure
////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
dimensions // file under validation must be an image
// meeting the dimension constraints as specified by the rule's parameters:
'avatar' => 'dimensions:min_width=100,min_height=200'
// available constraints: min_width, max_width, min_height, max_height, width, height, ratio
// ratio constraint should be represented as width divided by height
// can be specified either by a statement like 3/2 or a float like 1.5:
'avatar' => 'dimensions:ratio=3/2'
// since this rule requires several arguments,
// use the Rule::dimensions method to fluently construct the rule:
// ...
use Illuminate\Validation\Rule;
// ...
Validator::make($data, [
'avatar' => [
'required',
Rule::dimensions()->maxWidth(1000)->maxHeight(500)->ratio(3 / 2),
],
]);
// ...
mimetypes:text/plain,... // file under validation must match one of the given MIME types:
'video' => 'mimetypes:video/avi,video/mpeg,video/quicktime'
// to determine the MIME type of the uploaded file, file contents will be read
// and the framework will attempt to guess the MIME type,
// which may be different from the client provided MIME type.
mimes:foo,bar,... // file under validation must have a one of the listed MIME type
'photo' => 'mimes:jpeg,bmp,png'
// https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types
////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
exists:table,column // exist on a given database table.
'state' => 'exists:states' // if the column option is not specified, the field name will be used
'state' => 'exists:states,abbreviation'
// specify a specific database connection to be used for the exists query
// prependin the connection name to the table name using "dot" syntax:
'email' => 'exists:connection.staff,email'
// customize the query executed by the validation rule,
// specify the validation rules as an array instead of using the | character to delimit them:
// ...
use Illuminate\Validation\Rule;
// ...
Validator::make($data, [
'email' => [
'required',
Rule::exists('staff')->where(function ($query) {
$query->where('account_id', 1);
}),
],
]);
// ...
unique:table,column,except,idColumn // not exist within the given database table.
// --- custom column name, if not specified, the field name will be used
'email' => 'unique:users,email_address'
// --- custom database connection, connection and the table name using "dot" syntax:
'email' => 'unique:connection.users,email_address'
// --- forcing a unique rule to ignore a given id
// use the Rule class to fluently define the rule
// specify the validation rules as an array instead of using the | character to delimit the rules:
// ...
use Illuminate\Validation\Rule;
// ...
Validator::make($data, [
'email' => [
'required',
Rule::unique('users')->ignore($user->id),
],
]);
// ...
// never pass any user controlled request input into the ignore method
// only pass a system generated unique ID such as an auto-incrementing ID or UUID
// instead of passing the model key value to the ignore method,
// pass the entire model instance, Laravel will automatically extract the key from the model
Rule::unique('users')->ignore($user)
// --- specify the name of the column when calling the ignore method
// if table uses a primary key column name other than id
Rule::unique('users')->ignore($user->id, 'user_id')
// --- pass a different column name as the second argument to the unique method
Rule::unique('users', 'email_address')->ignore($user->id),
// --- adding additional where clauses
// specify additional query constraints by customizing the query using the where method
// verifies the account_id is 1
'email' => Rule::unique('users')->where(function ($query) {
return $query->where('account_id', 1);
})
Custom Validation Rules
// php artisan make:rule Uppercase
// generate a new rule object, contains two methods:
// --- passes - receives the attribute value and name,
// and should return true or false depending on whether the attribute value is valid or not
// --- message - should return the validation error message
// that should be used when validation fails
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class Uppercase implements Rule {
// determine if the validation rule passes
public function passes($attribute, $value) {
return strtoupper($value) === $value;
// --- trans - helper, return an error message from translation files
return trans('validation.uppercase');
}
// get the validation error message
public function message() {
return 'The :attribute must be uppercase.';
}
}
// ...
// ATTACH RULE TO VALIDATOR
use App\Rules\Uppercase;
// ...
$request->validate([
'name' => ['required', 'string', new Uppercase],
]);
// --- Closure, instead of a rule object
// receives the attribute name, value, and a $fail callback
$validator = Validator::make($request->all(), [
'title' => [
'required',
'max:255',
function ($attribute, $value, $fail) {
if ($value === 'foo') {
$fail($attribute.' is invalid.');
}
},
],
]);
// --- extend - use within a service provider to register a custom validation rule
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Validator;
class AppServiceProvider extends ServiceProvider {
// register any application services
public function register() {
// ...
}
// bootstrap any application services
public function boot() {
// ...
Validator::extend(
'foo',
function (
$attribute, // name of the $attribute being validated
$value, // value
$parameters, // array of parameters passed to the rule
$validator // Validator instance
) {
return $value == 'foo';
});
// pass a class and method to the extend method instead of a Closure:
Validator::extend('foo', 'FooValidator@validate');
// custom placeholder replacements for error messages
Validator::replacer(
'foo',
function (
$message,
$attribute,
$rule,
$parameters
) {
return str_replace(...);
});
// for a rule to run even when an attribute is empty,
// imply (assume) that the attribute is required
// by default, when an attribute being validated is not present or contains an empty string,
// normal validation rules, including custom extensions, are not run
Validator::extendImplicit(
'foo',
function (
$attribute, // name of the $attribute being validated
$value, // value
$parameters, // array of parameters passed to the rule
$validator // Validator instance
) {
return $value == 'foo';
});
// ...
}
}
// error message for custom rule is set using an inline custom message array
// or by adding an entry in the validation language file
// message should be placed in the first level of the array, not within the custom array,
// which is only for attribute-specific error messages:
"foo" => "Your input was invalid!",
"accepted" => "The :attribute must be accepted.",...
Views
- contain the HTML served by application and separate controller/application logic from presentation logic
- stored in the resources/views directory
- view() helper - returns views from resources/views
- Blade Templates
php artisan view:cache
- recompile views, run as part of your deployment process, php artisan view:clear
- clear the view cache
// --- view - returns views
// resources/views/greeting.blade.php in routes definition
Route::get('/', function () {
return view('greeting', ['name' => 'James']);
});
// --- "dot" notation - reference nested views
// resources/views/admin/profile.blade.php
return view('admin.profile', $data);
// --- exists - determine if a view exists
use Illuminate\Support\Facades\View;
// ...
if (View::exists('emails.customer')) {
// ...
}
// --- first - create the first view that exists in a given array of views
return view()->first(['custom.admin', 'admin'], $data);
// via the View facade
use Illuminate\Support\Facades\View;
// ...
return View::first(['custom.admin', 'admin'], $data);
// --- PASSING DATA TO VIEWS
return view('greetings', ['name' => 'Victoria']);
// --- with - add individual pieces of data to the view
return view('greeting')->with('name', 'Victoria');
// --- share - share a piece of data with all views that are rendered by application
// place within a service provider boot method.
// add them to the AppServiceProvider or generate a separate service provider to house them
namespace App\Providers;
use Illuminate\Support\Facades\View;
class AppServiceProvider extends ServiceProvider {
// register any application services
public function register() {
// ...
}
// bootstrap any application services
public function boot() {
View::share('key', 'value');
}
}
// --- VIEW COMPOSERS
// callbacks or class methods that are called when a view is rendered
// for data that you want to be bound to a view each time that view is rendered,
// organize that logic into a single location
// register the view composers within a service provider
// use the View facade to access the underlying Illuminate\Contracts\View\Factory contract
// Laravel does not include a default directory for view composers
// organize them however you wish, create an app/Http/View/Composers directory
namespace App\Providers;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
class ViewServiceProvider extends ServiceProvider {
// register any application services
public function register() {
// ...
}
// bootstrap any application services
public function boot() {
// using class based composers
View::composer(
'profile',
// ['profile', 'dashboard'], // attaching to multiple views
// '*', // attach to all views
'App\Http\View\Composers\ProfileComposer'
);
// using Closure based composers
View::composer('dashboard', function ($view) {
// ...
});
// creators - very similar to view composers
// executed immediately after the view is instantiated
// instead of waiting until the view is about to render
View::creator(
'profile',
'App\Http\View\Creators\ProfileCreator'
);
}
}
// now that the composer is registered
// ProfileComposer@compose method will be executed each time the profile view is being rendered
// define the composer class
namespace App\Http\View\Composers;
use Illuminate\View\View;
use App\Repositories\UserRepository;
class ProfileComposer {
// user repository implementation
protected $users;
// create a new profile composer
public function __construct(UserRepository $users) {
// dependencies automatically resolved by service container...
$this->users = $users;
}
// bind data to the view
// called before the view is rendered with the Illuminate\View\View instance
// use the with method to bind data to the view
public function compose(View $view) {
$view->with('count', $this->users->count());
}
}
URL Generation
// --- url
// generate arbitrary URLs, uses scheme (HTTP or HTTPS) and host from the current request
$post = App\Post::find(1);
echo url("/posts/{$post->id}"); // http://example.com/posts/1
echo url()->current(); // current URL without the query string
echo url()->full(); // current URL including the query string
echo url()->previous(); // full URL for the previous request
// each method may also be accessed via the URL facade
use Illuminate\Support\Facades\URL;
// ...
echo URL::current();
// --- route - generate URLs to named routes
// named routes allow generation of URLs without being coupled to the actual URL defined on the route
// if the route URL changes, no changes need to be made to your route function calls
Route::get('/post/{post}', function () {
// ...
})->name('post.show');
// ...
echo route('post.show', ['post' => 1]); // http://example.com/post/1
// automatically extracts models primary key
echo route('post.show', ['post' => $post]);
// generate URLs for routes with multiple parameters
Route::get('/post/{post}/comment/{comment}', function () {
// ...
})->name('comment.show');
// ...
echo route(
'comment.show',
['post' => 1, 'comment' => 3]
); // http://example.com/post/1/comment/3
// --- action - generate URL for the given controller action
// pass the controller class name relative to the App\Http\Controllers namespace
// no need to pass the full namespace of the controller
$url = action('HomeController@index');
// reference actions with a "callable" array syntax
use App\Http\Controllers\HomeController;
// ...
$url = action([HomeController::class, 'index']);
// if the controller method accepts route parameters,
// pass them as the second argument to the function
$url = action('UserController@profile', ['id' => 1]);
// --- {locale} parameter - request-wide default values for certain URL parameters
Route::get('/{locale}/posts', function () {
// ...
})->name('post.index');
// URL::defaults
// default value for parameter that will always be applied during the current request
// may be called from a route middleware so that you have access to the current request
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\URL;
class SetDefaultLocaleForUrls {
public function handle($request, Closure $next) {
URL::defaults(['locale' => $request->user()->locale]);
return $next($request);
}
}
// once the default value for the locale parameter has been set,
// you are no longer required to pass its value when generating URLs via the route helper
// --- signedRoute - create "signed" URLs to named routes
// URLs will have a "signature" hash appended to the query string
// which allows Laravel to verify that the URL has not been modified since it was created
// useful for routes that are publicly accessible, need a layer of protection
use Illuminate\Support\Facades\URL;
// ...
return URL::signedRoute('unsubscribe', ['user' => 1]);
// --- temporarySignedRoute - temporary signed route URL that expires
use Illuminate\Support\Facades\URL;
// ...
return URL::temporarySignedRoute(
'unsubscribe', now()->addMinutes(30), ['user' => 1]
);
// --- hasValidSignature - verify that an incoming request has a valid signature
use Illuminate\Http\Request;
// ...
Route::get('/unsubscribe/{user}', function (Request $request) {
if (! $request->hasValidSignature()) {
abort(401);
}
// ...
})->name('unsubscribe');
// alternatively, assign Illuminate\Routing\Middleware\ValidateSignature middleware to route
// if it is not already present, assign this middleware a key in HTTP kernel routeMiddleware array
// application route middleware:
protected $routeMiddleware = [
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
];
// after middleware registration in kernel, attach it to a route
// if the incoming request does not have a valid signature,
// middleware will automatically return a 403 error response
Route::post('/unsubscribe/{user}', function (Request $request) {
// ...
})->name('unsubscribe')->middleware('signed');
Performance
composer install --prefer-dist --no-dev -o
- in deployment script, - o
- optimize autoloader by generating a "classmap", OR --classmap-authoritative
instead of the '-o' for further optimization if app does not generate classes at runtime
- as part of production deployment process:
php artisan config:cache
- cache all of you configuration files into a single file, be sure that you only call the env function from within configuration files, once cached, the .env file will not be loaded; therefore, env function will only return external, system level environment variables
php artisan route:cache
- generate route cache, php artisan route:clear
- clear cache
php artisan event:cache
- cache a manifest of all events and listeners, speed up the event registration process, avoid scanning all listeners on every request, php artisan event:clear
- destroy the cache
php artisan view:cache
- recompile views, php artisan view:clear
- clear the view cache
- drivers
- caching in production: in-memory cache drivers such as Redis, Memcached, or DynamoDB, consider local filesystem caching for a single-server setup, although it would be slower than the in-memory options
- queueing: Redis, SQS, or Beanstalkd drivers, database driver is not suitable for production environments and is known to have deadlock issues
- sessions: Database, Redis, Memcached, or DynamoDB drivers, the cookie driver has the file size and security limitations and is not recommended for production
- queue time-consuming tasks: parsing and storing a CSV file, interacting with third-party APIs, sending notifications to users, expensive database queries, and updating search index
- compression headers - enable on web server or CDN for text format files, like CSS, JS, XML, or JSON, set up 'gzip' or 'brotli'
- enable cache-control headers at the webserver level or at CDN level (if applicable), set these headers at app instead of the webserver, use cache control middleware: \Illuminate\Http\Middleware\SetCacheHeaders, dont forget about cache-busting for changing CSS or JS
- CDNs - boosting asset serving performance: Cloudflare, AWS Cloudfront, Azure CDN, Laravel offers CDN support out of the box for Mix and the asset() helper function
- large amounts of data(ex: columns with a datatype of TEXT) columns better to place into separate table: posts content, etc,.
- get latest by sorting primary key in descending order: Post::orderBy('id', 'desc')->get();
- generators commands for Lumen
- Laravel Telescope - debug assistant, insight into the requests coming into application, exceptions, log entries, database queries, queued jobs, mail, notifications, cache operations, scheduled tasks, variable dumps and more
- Laravel Debugbar - integrates php debug bar
- Laravel Socialite - authenticate with OAuth
- Laravel Scout - driver based solution for adding full-text search
- Laravel Passport - API authentication, full OAuth2 server implementation
- lumen-api-oauth
- lumen-api-demo
- lumen-cors
- No CAPTCHA reCAPTCHA
- Facebook GraphQL for Laravel 5, supports Relay, eloquent models, validation and GraphiQL
- elasticsearch ORM for Laravel, Lumen and Native php
- laravel-elasticsearch
- lumen-elasticsearch
- PHP Image Manipulation
- Excel exports and imports in Laravel
- PHP desktop/mobile user agent parser with support for Laravel, based on Mobiledetect
- JSON Web Token Authentication for Laravel & Lumen
- lumen-jwt
- jwt-auth-guard
- Role-based Permissions for Laravel 5 (instead of Gates)
- Fractal wrapper built for Laravel and Lumen applications, transform data before using it in an API
- Laravel package to enable sending push notifications to devices
- package to backup your Laravel app
- Tinker, powerful REPL for the Laravel framework
- Laravel Migrations Generator - automatically generate your migrations from an existing database schema
- Migrate DB - data transfer from one database to another
- Laravel Admin
- Admin Panel/CMS
- Octane - supercharges apps performance by serving using high-powered application servers, including Swoole and RoadRunner
- Enlightn Security Checker - command line tool that checks dependencies with known security vulnerabilities
Back to Main Page