โ˜ฐ

Laravel Complete Reference

Every core concept used in real production Laravel apps โ€” from request lifecycle to container internals. Targets Laravel 12/13 syntax, with version notes wherever behavior changed since Laravel 8. One example per concept โ€” no fluff, no repetition.

Part 1

Foundations

Request lifecycle, routing, middleware, controllers, Blade, and validation โ€” the layer every Laravel app is built on.

๐Ÿ”„ Request Lifecycle

Every request to a Laravel app follows the same path: public/index.php boots the app, creates the Service Container, then hands the request to either the HTTP Kernel or Console Kernel. The HTTP Kernel runs the request through global middleware, then the Router matches it to a route, runs route-specific middleware, and finally executes the controller (or closure), which returns a Response that flows back out through the middleware stack.

The flow in order

  1. public/index.php โ€” entry point, loads autoloader + bootstraps the app
  2. bootstrap/app.php โ€” builds the Application (the container) and registers core bindings
  3. HTTP Kernel โ€” sends the request through global middleware (CORS, trim strings, etc.)
  4. Service Providers โ€” all registered providers' register() then boot() methods run
  5. Router โ€” matches the URI + method to a route, resolves route middleware
  6. Controller / Closure โ€” runs your business logic, returns data or a Response
  7. Response flows back out through middleware, then is sent to the browser
Laravel 11+ note: Prior to L11, this flow was wired through app/Http/Kernel.php and a heavier app/Console/Kernel.php. Laravel 11 introduced the slim skeleton โ€” middleware, routing, and exception handling are now configured directly in bootstrap/app.php, and Kernel.php files are gone by default for new apps (still supported for upgraded apps).

๐Ÿ“ Installation & Directory Structure

Install a new project
composer create-project laravel/laravel example-app
# or via the Laravel installer
laravel new example-app

Key directories

PathPurpose
app/ModelsEloquent models
app/Http/ControllersControllers
app/Http/MiddlewareCustom middleware
app/ProvidersService providers
routes/web.phpBrowser/session routes
routes/api.phpStateless API routes (token auth, no CSRF)
resources/viewsBlade templates
database/migrationsSchema migrations
database/factoriesModel factories for seeding/testing
database/seedersDatabase seeders
config/*.phpAll framework + package configuration
.envEnvironment-specific secrets/settings (never commit)
bootstrap/app.phpApp bootstrapping โ€” middleware, routing, exceptions (L11+)
storage/Logs, compiled views, file uploads (local disk)
L11+: routes/api.php and routes/channels.php no longer exist by default โ€” run php artisan install:api or install:broadcasting to add them back when needed.

๐Ÿ›ฃ๏ธ Routing

Routes map a URI + HTTP verb to a closure or controller action. Defined in routes/web.php (or api.php).

Basic routes
Route::get('/users', [UserController::class, 'index']);
Route::post('/users', [UserController::class, 'store']);
Route::put('/users/{user}', [UserController::class, 'update']);
Route::delete('/users/{user}', [UserController::class, 'destroy']);
Route parameters (required, optional, constrained)
Route::get('/posts/{id}', function (string $id) { ... });
Route::get('/posts/{id?}', function (?string $id = null) { ... }); // optional
Route::get('/posts/{id}', ...)->where('id', '[0-9]+'); // regex constraint
Named routes
Route::get('/profile', [ProfileController::class, 'show'])->name('profile.show');
// usage anywhere:
route('profile.show', ['user' => 1]); // -> /profile/1
Route groups (prefix, middleware, name)
Route::prefix('admin')->middleware(['auth', 'verified'])->name('admin.')->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard'); // admin.dashboard
});
Resource routes โ€” generates all 7 RESTful routes at once
Route::resource('posts', PostController::class);
// creates: index, create, store, show, edit, update, destroy
Route::apiResource('posts', PostController::class); // excludes create & edit (no views needed for APIs)

Route Model Binding

Laravel automatically resolves Eloquent models from route parameters when the type-hint matches.

Implicit binding (most common)
Route::get('/posts/{post}', function (Post $post) {
    return $post->title; // {post} auto-resolved to Post::findOrFail($id)
});
// bind by a different column:
Route::get('/posts/{post:slug}', ...); // resolves via WHERE slug = ?

Fallback routes & rate limiting

Route::fallback(function () { return response()->view('errors.404', [], 404); });

Route::middleware('throttle:60,1')->group(function () { // 60 requests / 1 min
    Route::get('/api/data', ...);
});
L11+: Throttle now supports per-second limits, e.g. throttle:10,1,1 style configs via RateLimiter::for() with ->by() and per-second granularity in the limiter definitions.

Route caching (production)

php artisan route:cache   # cache all routes for faster boot โ€” closures NOT allowed in routes when cached
php artisan route:clear
Important: Routes using closures cannot be cached with route:cache โ€” always use controller class array syntax for production routes.

๐Ÿงฑ Middleware

Middleware filters HTTP requests entering your app โ€” authentication checks, logging, CORS, etc. Each middleware can inspect/modify the request, then pass it to the next layer, or short-circuit it.

Creating middleware
php artisan make:middleware EnsureUserIsSubscribed
class EnsureUserIsSubscribed
{
    public function handle(Request $request, Closure $next): Response
    {
        if (! $request->user()?->subscribed) {
            return redirect('/billing');
        }
        return $next($request); // pass along the pipeline
    }
}
Registering middleware โ€” Laravel 11+ (bootstrap/app.php)
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias(['subscribed' => EnsureUserIsSubscribed::class]);
    $middleware->append(SomeGlobalMiddleware::class); // global, runs on every request
})
Laravel 8โ€“10: middleware was registered in app/Http/Kernel.php in the $middleware, $middlewareGroups, and $routeMiddleware arrays. Laravel 11 moved this into bootstrap/app.php via the fluent withMiddleware() closure, but Kernel.php-based registration still works if you have that file.
Using it on a route + passing parameters
Route::get('/premium', ...)->middleware('subscribed');
Route::get('/admin', ...)->middleware('role:admin'); // param after the colon, read via $role in handle($request, $next, $role)

Terminable middleware

Runs after the response has been sent to the browser โ€” useful for logging/cleanup that shouldn't delay the response.

public function terminate(Request $request, Response $response): void
{
    // e.g. write to an analytics log after response is flushed
}

๐ŸŽฎ Controllers

Basic controller
php artisan make:controller PostController --resource
class PostController extends Controller
{
    public function index(): View
    {
        return view('posts.index', ['posts' => Post::latest()->paginate(10)]);
    }

    public function store(StorePostRequest $request): RedirectResponse
    {
        Post::create($request->validated());
        return redirect()->route('posts.index');
    }
}

Single Action Controllers

When a controller does one job, skip the method name entirely with __invoke.

php artisan make:controller ProvisionServer --invokable
class ProvisionServer extends Controller
{
    public function __invoke(Request $request) { ... }
}
// route: Route::post('/server', ProvisionServer::class);

API Resource Controllers

php artisan make:controller Api/PostController --api # no create/edit methods

๐Ÿ“จ Requests & Responses

Reading input

$request->input('name');           // single field, any method (GET/POST)
$request->input('name', 'default'); // with a default
$request->name;                  // magic property access
$request->all();                  // all input as array
$request->only(['name', 'email']);
$request->except(['password']);
$request->has('name');              // bool, present (even if empty)
$request->filled('name');           // bool, present AND not empty
$request->boolean('subscribe');     // casts "1"/"true"/"on" -> true
$request->query('page');            // query string only

File uploads

if ($request->hasFile('avatar')) {
    $path = $request->file('avatar')->store('avatars', 'public');
    // or with a custom name:
    $path = $request->file('avatar')->storeAs('avatars', $request->user()->id.'.jpg', 'public');
}

Responses

return response('Hello World', 200);
return response()->json(['status' => 'ok'], 200);
return response()->download($pathToFile);
return redirect()->route('home')->with('success', 'Saved!'); // flash message
return back()->withErrors(['email' => 'Already taken.']);

๐Ÿ”ต Blade Templating

Echoing & control structures

{{ $user->name }}              {{-- auto-escaped --}}
{!! $rawHtml !!}              {{-- raw, unescaped --}}

@if ($user->isAdmin())
    Admin
@elseif ($user->isEditor())
    Editor
@else
    Viewer
@endif

@foreach ($posts as $post)
    {{ $post->title }}
@endforeach

@forelse ($posts as $post)
    {{ $post->title }}
@empty
    No posts found.
@endforelse

Layouts: @extends / @section vs Components

{{-- resources/views/layouts/app.blade.php --}}
<html>
<body>
    @yield('content')
</body>
</html>

{{-- resources/views/posts/index.blade.php --}}
@extends('layouts.app')
@section('content')
    <h1>Posts</h1>
@endsection

Blade Components (modern approach, preferred since L7+)

php artisan make:component Alert
{{-- resources/views/components/alert.blade.php --}}
<div class="alert alert-{{ $type }}">
    {{ $slot }}
</div>

{{-- usage anywhere --}}
<x-alert type="danger">Something went wrong.</x-alert>

Named slots

{{-- card.blade.php --}}
<div class="card">
    <div class="card-header">{{ $header }}</div>
    <div class="card-body">{{ $slot }}</div>
</div>

<x-card>
    <x-slot:header>Title here</x-slot:header>
    Body content here.
</x-card>

@include, @stack/@push, custom directives

@include('partials.nav', ['active' => 'home'])

{{-- layout file --}}
@stack('scripts')

{{-- any child view --}}
@push('scripts')
    <script src="chart.js"></script>
@endpush

โœ… Validation

Validation can be done inline in the controller, but Form Request classes are the standard for anything beyond trivial โ€” they keep controllers clean and centralize authorization + rules.

Form Request
php artisan make:request StorePostRequest
class StorePostRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true; // or a Gate/Policy check
    }

    public function rules(): array
    {
        return [
            'title' => 'required|string|max:255',
            'email' => 'required|email|unique:users,email',
            'category_id' => 'required|exists:categories,id',
            'tags' => 'array',
            'tags.*' => 'string|distinct',
        ];
    }

    public function messages(): array
    {
        return ['title.required' => 'A title is required.'];
    }
}

// controller โ€” just type-hint it, validation runs automatically:
public function store(StorePostRequest $request) {
    $validated = $request->validated();
}

Common validation rules

RuleWhat it checks
requiredField must be present and not empty
nullableAllows null without failing other rules
string / integer / numeric / boolean / arrayType checks
emailValid email format
unique:table,columnNo existing row with this value
exists:table,columnValue must exist in another table (FK validity)
min:x / max:xLength (string) or value (number) bounds
confirmedRequires matching {field}_confirmation input
date / date_format:Y-m-dValid date / specific format
in:a,b,cValue must be one of the list
file / image / mimes:jpg,pngUpload type checks
regex:patternCustom regex match

Custom rule classes

php artisan make:rule Uppercase
class Uppercase implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if ($value !== strtoupper($value)) {
            $fail('The :attribute must be uppercase.');
        }
    }
}
// rules(): ['code' => ['required', new Uppercase]]

Conditional rules

Validator::make($data, $rules)->sometimes('reason', 'required', function ($input) {
    return $input->status === 'rejected';
});
Part 2

Database & Eloquent

Migrations, the query builder, every Eloquent relationship type, eager loading, collections, casts, scopes, model events, pagination, and large-dataset handling.

๐Ÿงฉ Migrations

Migrations are version control for your database schema โ€” every change is a PHP file that can be run forward (up) or reversed (down).

Create a migration
php artisan make:migration create_posts_table
return new class extends Migration
{
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->string('title');
            $table->string('slug')->unique();
            $table->text('body');
            $table->boolean('published')->default(false);
            $table->unsignedInteger('views')->default(0);
            $table->timestamp('published_at')->nullable();
            $table->softDeletes(); // adds nullable deleted_at
            $table->timestamps(); // created_at + updated_at

            $table->index('slug');
            $table->index(['user_id', 'published']); // composite index
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

Common column types

MethodProduces
id() / bigIncrements('id')Auto-incrementing primary key (BIGINT)
string('name', 100)VARCHAR, default length 255
text() / longText()TEXT / LONGTEXT
integer() / bigInteger() / unsignedInteger()Numeric types
decimal('price', 8, 2)Fixed-precision decimal
boolean()TINYINT(1)
date() / dateTime() / timestamp()Date/time columns
json()JSON column (cast to array/object in model)
enum('status', ['draft','live'])ENUM column
foreignId('user_id')->constrained()FK column + constraint in one call
uuid() / ulid()UUID/ULID columns (for non-integer PKs)
morphs('taggable')Adds taggable_id + taggable_type for polymorphic relations

Modifying existing tables

Schema::table('posts', function (Blueprint $table) {
    $table->string('subtitle')->nullable()->after('title');
    $table->dropColumn('old_column');
    $table->renameColumn('body', 'content');
});
// requires: composer require doctrine/dbal  (for column modification on older Laravel; not needed L11+ in most cases)
Running migrations
php artisan migrate
php artisan migrate:rollback        # undo last batch
php artisan migrate:fresh --seed     # drop everything, re-migrate, re-seed
php artisan migrate:status          # see what's been run

๐ŸŒฑ Seeders & Factories

Factory โ€” generates fake model data
php artisan make:factory PostFactory
class PostFactory extends Factory
{
    public function definition(): array
    {
        return [
            'user_id' => User::factory(), // creates a related user automatically
            'title' => fake()->sentence(),
            'slug' => fake()->unique()->slug(),
            'body' => fake()->paragraphs(3, true),
            'published' => fake()->boolean(80),
        ];
    }

    // state โ€” a named variation of the factory
    public function unpublished(): static
    {
        return $this->state(fn (array $attrs) => ['published' => false]);
    }
}

// usage:
Post::factory()->count(10)->create();
Post::factory()->unpublished()->create();
Post::factory()->for($existingUser)->create(); // attach to a specific user
Seeder
php artisan make:seeder PostSeeder
class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        $this->call([UserSeeder::class, PostSeeder::class]);
    }
}
// php artisan db:seed

๐Ÿ” Query Builder

The query builder (DB::table()) gives a fluent, database-agnostic interface for raw queries โ€” useful when you don't need a full Eloquent model.

DB::table('users')->where('active', true)->get();
DB::table('users')->where('votes', '>', 100)->orWhere('name', 'John')->get();
DB::table('users')->whereIn('id', [1,2,3])->get();
DB::table('users')->whereBetween('age', [18, 30])->get();
DB::table('users')->whereNull('deleted_at')->get();

// joins
DB::table('posts')
    ->join('users', 'users.id', '=', 'posts.user_id')
    ->select('posts.*', 'users.name as author')
    ->get();

// unions
$first = DB::table('archived_posts');
DB::table('posts')->union($first)->get();

// raw expressions when the query builder can't express something
DB::table('orders')->select(DB::raw('COUNT(*) as order_count, status'))->groupBy('status')->get();

// aggregates
DB::table('orders')->count();
DB::table('orders')->sum('total');
DB::table('orders')->avg('total');
Security: Always pass values as bindings (as shown above) rather than concatenating into DB::raw() strings โ€” string concatenation in raw queries is the #1 source of SQL injection in Laravel apps.

๐Ÿ—‚๏ธ Eloquent Basics

php artisan make:model Post -m  # -m also creates the migration
class Post extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = ['title', 'slug', 'body', 'published']; // mass-assignable
    // or, the inverse โ€” block specific fields:
    protected $guarded = ['id'];
}

Mass assignment

Eloquent blocks mass assignment by default to prevent attackers injecting unexpected fields (e.g. is_admin) via form input. Use $fillable (allow-list) or $guarded (block-list) to control it.

Post::create(['title' => 'Hello', 'body' => 'World']); // only $fillable fields apply

Timestamps & Soft Deletes

// disable auto timestamps if the table doesn't have them:
public $timestamps = false;

// soft deletes โ€” row stays in DB, deleted_at is set instead of removed
$post->delete();              // sets deleted_at
Post::withTrashed()->get();  // include soft-deleted rows
Post::onlyTrashed()->get();   // only soft-deleted rows
$post->restore();             // undelete
$post->forceDelete();         // permanently remove

๐Ÿ”— Eloquent Relationships

Every relationship type you'll need in a real app, with the migration shape implied by each.

One to One โ€” hasOne / belongsTo

// User has one Phone. phones table has user_id.
class User extends Model {
    public function phone(): HasOne { return $this->hasOne(Phone::class); }
}
class Phone extends Model {
    public function user(): BelongsTo { return $this->belongsTo(User::class); }
}
// usage: $user->phone->number;  $phone->user->name;

One to Many โ€” hasMany / belongsTo

// User has many Posts. posts table has user_id.
class User extends Model {
    public function posts(): HasMany { return $this->hasMany(Post::class); }
}
// usage: $user->posts; // Collection of Post
$user->posts()->where('published', true)->get(); // query further before executing

Many to Many โ€” belongsToMany

// Post belongs to many Tags, and vice versa. Needs a pivot table: post_tag (post_id, tag_id).
class Post extends Model {
    public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class); }
}
// attaching / detaching / syncing the pivot:
$post->tags()->attach($tagId);
$post->tags()->detach($tagId);
$post->tags()->sync([1, 2, 3]); // replaces all pivot rows with exactly these

// pivot table with extra columns:
public function tags(): BelongsToMany
{
    return $this->belongsToMany(Tag::class)->withPivot('added_by')->withTimestamps();
}

Has Many Through

// Mechanic -> Car -> Owner. Mechanic can reach Owner through Car.
class Mechanic extends Model {
    public function carOwners(): HasManyThrough
    {
        return $this->hasManyThrough(Owner::class, Car::class);
    }
}

Polymorphic Relations

Lets a model belong to more than one other model type using a single relation โ€” e.g. Comment can belong to either a Post or a Video.

// comments table: commentable_id, commentable_type (added via $table->morphs('commentable'))
class Comment extends Model {
    public function commentable(): MorphTo { return $this->morphTo(); }
}
class Post extends Model {
    public function comments(): MorphMany { return $this->morphMany(Comment::class, 'commentable'); }
}
// usage is identical to a normal relation:
$post->comments;  $comment->commentable; // returns the Post or Video instance

Many-to-Many Polymorphic

// Post and Video can both have many Tags, Tags can belong to many of both.
class Tag extends Model {
    public function posts(): MorphToMany { return $this->morphedByMany(Post::class, 'taggable'); }
}
class Post extends Model {
    public function tags(): MorphToMany { return $this->morphToMany(Tag::class, 'taggable'); }
}

โšก Eager Loading & the N+1 Problem

Accessing a relationship inside a loop ($user->posts for every user) fires one query per row โ€” the classic N+1 problem. Eager loading fetches all related rows in a second query up front.

The problem
$users = User::all(); // 1 query
foreach ($users as $user) {
    echo $user->posts->count(); // +1 query PER user โ€” N+1!
}
The fix โ€” with()
$users = User::with('posts')->get(); // 2 queries total, regardless of row count
foreach ($users as $user) {
    echo $user->posts->count(); // no extra query โ€” already loaded
}

// nested eager loading:
User::with('posts.comments')->get();
// multiple relations:
User::with(['posts', 'profile'])->get();
// constrained eager loading โ€” only published posts:
User::with(['posts' => fn ($q) => $q->where('published', true)])->get();
withCount / withSum / withAvg โ€” aggregate without loading the rows
$users = User::withCount('posts')->get(); // adds $user->posts_count, no rows loaded
User::withSum('orders', 'total')->get();   // $user->orders_sum_total
User::withAvg('reviews', 'rating')->get();
Lazy eager loading โ€” load() on an already-fetched collection
$users = User::all();
if ($needsPosts) {
    $users->load('posts'); // load relation after the fact, still avoids N+1
}
Tip: In local/dev, install Laravel Debugbar or use DB::listen() to spot N+1 queries before they reach production.
All versions: You can force Laravel to throw an exception when lazy loading happens accidentally by calling Model::preventLazyLoading() in a service provider's boot() method (typically wrapped in !app()->isProduction()) โ€” great for catching N+1s in development.

๐Ÿ“ฆ Collections

Every Eloquent query that returns multiple rows gives you a Collection โ€” a powerful wrapper around arrays with dozens of chainable methods. map() is generally preferred over foreach for transforming data: it's chainable, returns a new collection, and doesn't require manual array building.

MethodWhat it doesExample
map()Transform each item, return new collection$users->map(fn($u) => $u->name)
filter()Keep items matching a condition$users->filter(fn($u) => $u->active)
reject()Opposite of filter โ€” remove matches$users->reject(fn($u) => $u->banned)
reduce()Collapse to a single value$cart->reduce(fn($c,$i)=>$c+$i->price, 0)
flatMap()Map then flatten one level$orders->flatMap(fn($o)=>$o->items)
groupBy()Group items by a key/callback$users->groupBy('country')
sortBy() / sortByDesc()Sort by key/callback$users->sortBy('name')
unique()Remove duplicates (by key, optionally)$users->unique('email')
pluck()Extract one column as a flat list$users->pluck('email')
each()Side-effect iteration (no return value)$users->each(fn($u)=>$u->ping())
partition()Split into two collections by condition[$paid,$unpaid]=$orders->partition(fn($o)=>$o->paid)
contains() / firstWhere()Search$users->firstWhere('id', 5)
chunk()Split into smaller collections$users->chunk(100)
map() vs foreach โ€” the classic controller upgrade
// foreach: more lines, manual array building, doesn't chain
$usersData = [];
foreach ($users as $user) {
    $usersData[] = ['id' => $user->id, 'name' => $user->name];
}

// map(): one expression, returns a new Collection, chainable
$usersData = $users->map(fn ($user) => ['id' => $user->id, 'name' => $user->name]);
// chain freely:
User::query()->active()->with('profile')->get()->map(...)->sortBy('name')->values();

LazyCollection โ€” for huge datasets

Behaves like a Collection but only loads one item into memory at a time using PHP generators. See Large Dataset Handling below.

Custom collection macros

// in a service provider's boot():
Collection::macro('toUpper', function () {
    return $this->map(fn ($v) => strtoupper($v));
});
// usage anywhere: collect(['a','b'])->toUpper();

๐ŸŽญ Accessors, Mutators & Casts

Accessors / Mutators โ€” modern Attribute syntax (L9+)

use Illuminate\Database\Eloquent\Casts\Attribute;

class User extends Model
{
    // accessor: $user->full_name
    protected function fullName(): Attribute
    {
        return Attribute::make(
            get: fn ($value, $attributes) => "{$attributes['first_name']} {$attributes['last_name']}",
        );
    }

    // mutator: setting $user->password = '...' auto-hashes it
    protected function password(): Attribute
    {
        return Attribute::make(
            set: fn ($value) => bcrypt($value),
        );
    }
}
Laravel 8 and earlier: the old syntax used separate magic methods โ€” getFullNameAttribute() and setPasswordAttribute($value). Still works in every version, but the Attribute::make() syntax (introduced L9) is now the recommended approach โ€” it's more testable and supports caching.

Casts

class Post extends Model
{
    protected function casts(): array
    {
        return [
            'published' => 'boolean',
            'metadata' => 'array',      // JSON column <-> PHP array, automatic
            'published_at' => 'datetime',
            'price' => 'decimal:2',
        ];
    }
}
Laravel 11+: casts moved from a protected $casts = [...] property to a casts(): array method, allowing casts to use constructor-injected dependencies. The old property syntax still works.

Custom cast classes

php artisan make:cast Money
class Money implements CastsAttributes
{
    public function get($model, $key, $value, $attributes): string
    {
        return number_format($value / 100, 2); // stored as cents, displayed as dollars
    }
    public function set($model, $key, $value, $attributes): int
    {
        return (int) ($value * 100);
    }
}

๐ŸŽฏ Query Scopes

Local scopes โ€” reusable query constraints

class Post extends Model
{
    public function scopePublished(Builder $query): void
    {
        $query->where('published', true);
    }

    public function scopeByAuthor(Builder $query, int $userId): void
    {
        $query->where('user_id', $userId);
    }
}
// usage โ€” drop the "scope" prefix and call it like a normal method, chainable:
Post::published()->byAuthor(5)->get();
L11+ note: scopes can also be defined as Attribute-style classes via the new Scope attribute on methods, but the scopeXxx() convention above remains the standard, most-used approach across all versions.

Global scopes โ€” applied to EVERY query automatically

class ActiveScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('active', true);
    }
}
// register in the model's booted() method:
protected static function booted(): void
{
    static::addGlobalScope(new ActiveScope);
}
// bypass when needed:
User::withoutGlobalScope(ActiveScope::class)->get();
Note: SoftDeletes works internally via a global scope โ€” that's how ->withTrashed() and ->onlyTrashed() bypass/adjust it.

๐Ÿ“ก Model Events & Observers

Eloquent fires events at each stage of a model's lifecycle: retrieved, creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored.

Quick version โ€” closures in the model
protected static function booted(): void
{
    static::creating(function (Post $post) {
        $post->slug = Str::slug($post->title);
    });
}
Scalable version โ€” Observers (preferred for anything non-trivial)
php artisan make:observer PostObserver --model=Post
class PostObserver
{
    public function creating(Post $post): void
    {
        $post->slug = Str::slug($post->title);
    }

    public function deleted(Post $post): void
    {
        Cache::forget("post.{$post->id}");
    }
}
// register โ€” in AppServiceProvider::boot(), or via #[ObservedBy] attribute on the model (L10.1+):
Post::observe(PostObserver::class);

// or directly on the model (Laravel 10.1+):
#[ObservedBy([PostObserver::class])]
class Post extends Model { ... }

๐Ÿ“„ Pagination

MethodHow it worksBest for
paginate(15)Runs a COUNT(*) + the page query, gives page links + totalStandard pagination with page numbers
simplePaginate(15)Skips COUNT(*), only knows if there's a next pageFaster โ€” when you only need Next/Prev, not total pages
cursorPaginate(15)Uses a WHERE on the last seen ID/column instead of OFFSETHuge tables โ€” OFFSET gets slower the deeper you page
// controller
$posts = Post::latest()->paginate(15);
return view('posts.index', ['posts' => $posts]);

// blade โ€” renders page links automatically
{{ $posts->links() }}

// cursor pagination for huge tables (no OFFSET, no COUNT)
$posts = Post::orderBy('id')->cursorPaginate(15);
Why it matters: paginate()'s COUNT(*) and high OFFSET values both get expensive on tables with millions of rows. For deep pagination on large tables, prefer cursorPaginate().

๐Ÿ—๏ธ Large Dataset Handling

Loading millions of rows with ::all() or ::get() will exhaust memory. These methods stream data instead of loading it all at once.

MethodHow it works
chunk(1000, fn)Fetches 1000 rows at a time, runs your callback on each chunk
chunkById(1000, fn)Same, but paginates by primary key โ€” safe if rows are being deleted/inserted during the loop
lazy()Same chunking under the hood, but returns a flat LazyCollection you iterate like a normal collection
lazyById()lazy() + the same primary-key-safe pagination as chunkById
cursor()One DB row at a time via a PHP generator โ€” lowest memory footprint, but holds the connection open the whole time
chunk()
User::chunk(1000, function (Collection $users) {
    foreach ($users as $user) {
        // process each user
    }
});
lazy() โ€” feels like a normal collection, but memory-safe
User::lazy()->each(function (User $user) {
    // runs over ALL users, 1000 at a time under the hood, low memory
});
cursor() โ€” one row at a time
foreach (User::cursor() as $user) {
    // only ONE user in memory at any moment
}
chunk() pitfall: if your callback updates/deletes rows on the same column you're chunking by, rows can be skipped because the OFFSET shifts. Use chunkById() in that situation instead.

๐Ÿ”’ Database Transactions

Wrap multiple writes so they all succeed together, or all roll back if any step fails โ€” critical for operations like "deduct stock AND create order".

DB::transaction(function () {
    $order = Order::create([...]);
    Product::find($id)->decrement('stock');
    // if any line above throws, EVERYTHING rolls back automatically
});

// manual control:
DB::beginTransaction();
try {
    // ... writes ...
    DB::commit();
} catch (\Throwable $e) {
    DB::rollBack();
    throw $e;
}
Part 3

Core Architecture & Internals

The Service Container is the heart of Laravel โ€” understanding it unlocks why everything else (facades, dependency injection, providers) works the way it does.

๐Ÿงฐ Service Container

The container is a big dependency-resolution registry. When you type-hint a class in a controller/job/anywhere Laravel resolves objects, the container figures out how to build it โ€” including its own nested dependencies โ€” automatically.

Automatic resolution (no binding needed for most classes)

class PodcastController extends Controller
{
    public function show(AudioProcessor $processor) {
        // Laravel sees the type-hint, builds AudioProcessor (and ITS dependencies) automatically
    }
}

bind() vs singleton() vs instance()

MethodBehavior
bind()New instance created every time it's resolved
singleton()Built once, same instance reused for the rest of the request/app lifecycle
instance()You already have an object โ€” register that exact instance
// in a Service Provider's register():
$this->app->bind(PaymentGateway::class, fn (Application $app) => new StripeGateway(config('services.stripe.key')));

$this->app->singleton(MetricsCollector::class, fn () => new MetricsCollector());

// resolving manually anywhere:
$gateway = app(PaymentGateway::class);
$gateway = resolve(PaymentGateway::class);

Binding an interface to an implementation (the real power move)

// register():
$this->app->bind(PaymentGatewayInterface::class, StripeGateway::class);

// now ANY class can type-hint the interface, and the container hands it the concrete implementation:
class CheckoutController {
    public function __construct(private PaymentGatewayInterface $gateway) {}
    // swap Stripe for PayPal app-wide by changing ONE line in the provider
}

Contextual binding

Inject a different implementation depending on which class is asking.

$this->app->when(PhotoController::class)
    ->needs(Filesystem::class)
    ->give(fn () => Storage::disk('local'));

$this->app->when(VideoController::class)
    ->needs(Filesystem::class)
    ->give(fn () => Storage::disk('s3'));

Tagging โ€” resolve a group of related bindings at once

$this->app->tag([CpuReport::class, MemoryReport::class], 'reports');
$reports = $this->app->tagged('reports'); // iterable of all tagged instances

๐Ÿ—๏ธ Service Providers

Service Providers are where all of Laravel's (and your app's) bootstrapping happens โ€” binding things into the container, registering routes/views/event listeners, etc.

php artisan make:provider PaymentServiceProvider
class PaymentServiceProvider extends ServiceProvider
{
    // register(): ONLY bind things here. Other providers may not be loaded yet.
    public function register(): void
    {
        $this->app->singleton(PaymentGatewayInterface::class, StripeGateway::class);
    }

    // boot(): runs AFTER all providers are registered โ€” safe to use other services here
    public function boot(): void
    {
        View::share('siteName', config('app.name'));
    }
}
Laravel 11+: new providers are registered in bootstrap/providers.php (a plain array), not config/app.php's providers array as in L8โ€“10. Both work if you've upgraded an older app rather than starting fresh.

Deferred providers โ€” only loaded when actually needed

public function isDeferred(): bool { return true; } // (older mechanism)
// modern: implement the Illuminate\Contracts\Support\DeferrableProvider interface
class PaymentServiceProvider extends ServiceProvider implements DeferrableProvider
{
    public function provides(): array { return [PaymentGatewayInterface::class]; }
}

๐ŸŽญ Facades

A Facade (Cache::get(), Route::get(), etc.) is a static-looking proxy to an object resolved from the container. It's not magic โ€” it just calls __callStatic, which resolves the real instance and forwards the call.

// what Cache::get('key') actually does under the hood, roughly:
class Cache extends Facade
{
    protected static function getFacadeAccessor() { return 'cache'; } // the container binding key
}
// Cache::get('key') resolves app('cache') from the container, then calls ->get('key') on it

Real instance vs facade โ€” interchangeable

Cache::put('key', 'value', 60);          // facade
app('cache')->put('key', 'value', 60);  // same thing, resolved directly

Facades vs Contracts (interfaces)

Facades are convenient and very testable via Cache::shouldReceive() mocking, but they hide the dependency. Constructor-injecting a Contract (e.g. Illuminate\Contracts\Cache\Repository) makes the dependency explicit โ€” preferred in packages or when testing without facade mocks. Both call the exact same underlying object.

Real-time facades

use Facades\App\Services\PaymentGateway;
PaymentGateway::charge(100); // turns any class into a facade just by aliasing the import with "Facades\"
Part 4

Authentication & Authorization

Who is the user (authentication), and what are they allowed to do (authorization) โ€” guards, providers, Sanctum, Gates, and Policies.

๐Ÿ Auth Scaffolding Options

PackageWhat it gives you
BreezeMinimal login/register/reset views + routes, using Blade or a JS stack (Vue/React via Inertia). Best starting point for most apps.
JetstreamBreeze + extras: teams, API tokens (Sanctum), two-factor auth, profile management UI. Heavier, opinionated.
FortifyHeadless backend logic only (no views) โ€” used when you want full control over the frontend.
SanctumLightweight API token + SPA cookie authentication โ€” see below.
PassportFull OAuth2 server โ€” only needed if you're issuing tokens to third-party apps.
composer require laravel/breeze --dev
php artisan breeze:install

๐Ÿ›ก๏ธ Guards & Providers

In config/auth.php: a guard defines how users are authenticated per request (session vs token); a provider defines how user records are retrieved (usually Eloquent).

// config/auth.php
'guards' => [
    'web' => ['driver' => 'session', 'provider' => 'users'],
    'api' => ['driver' => 'sanctum', 'provider' => 'users'],
],
'providers' => [
    'users' => ['driver' => 'eloquent', 'model' => App\Models\User::class],
],
Auth::guard('api')->user();    // check a specific guard
Auth::attempt(['email' => $email, 'password' => $password]); // login
Auth::user();                  // current authenticated user (default guard)
Auth::logout();

๐Ÿ”‘ Sanctum (API Tokens & SPA Auth)

Sanctum handles two separate use cases with one package: API tokens for mobile apps/third-party clients, and cookie-based auth for SPAs that live on the same domain (or subdomain) as the API.

API tokens

use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable {
    use HasApiTokens;
}

// issuing a token (e.g. after login):
$token = $user->createToken('mobile-app')->plainTextToken;

// client sends it as: Authorization: Bearer {token}
// protect routes:
Route::middleware('auth:sanctum')->get('/user', fn (Request $r) => $r->user());

// scoping token abilities:
$user->createToken('mobile-app', ['posts:read']);
if ($request->user()->tokenCan('posts:read')) { ... }
Laravel 10+: Sanctum is the default API auth choice in new starter kits (replacing Passport for most use cases) since it's much simpler when you don't need full OAuth2.

๐Ÿšช Gates

Gates are simple closure-based authorization checks โ€” best for actions that aren't tied to a specific model (e.g. "can view the admin panel").

// AppServiceProvider::boot()
Gate::define('view-admin-panel', function (User $user) {
    return $user->role === 'admin';
});

// usage:
if (Gate::allows('view-admin-panel')) { ... }
Gate::authorize('view-admin-panel'); // throws 403 if denied

// in Blade:
@can('view-admin-panel')
    <a href="/admin">Admin</a>
@endcan

๐Ÿ“‹ Policies

Policies are classes for organizing authorization logic around a specific model โ€” e.g. "can THIS user update THIS post".

php artisan make:policy PostPolicy --model=Post
class PostPolicy
{
    public function update(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }

    public function delete(User $user, Post $post): bool
    {
        return $user->id === $post->user_id || $user->isAdmin();
    }
}
Laravel 11+: Policies are auto-discovered by naming convention (PostPolicy for Post) โ€” no manual registration needed in most cases. L8โ€“10 required registering them in AuthServiceProvider's $policies array.

Using policies

// in a controller:
public function update(Request $request, Post $post) {
    $this->authorize('update', $post); // throws 403 automatically if denied
    // ...
}

// directly:
if ($user->can('update', $post)) { ... }

// in Blade:
@can('update', $post)
    <a href="/posts/{{ $post->id }}/edit">Edit</a>
@endcan
Part 5

Queues, Jobs & Events

Moving heavy work out of the request-response cycle โ€” this is what separates hobby apps from production apps.

โš™๏ธ Jobs & Dispatching

A Job is a class representing one unit of work that can run asynchronously on a queue โ€” sending an email, generating a PDF, calling a slow third-party API, etc.

php artisan make:job ProcessPodcast
class ProcessPodcast implements ShouldQueue
{
    use Queueable;

    public function __construct(public Podcast $podcast) {} // $podcast is serialized & stored with the job

    public function handle(): void
    {
        // the actual work โ€” runs on the queue worker, not the web request
    }
}

// dispatching:
ProcessPodcast::dispatch($podcast);
ProcessPodcast::dispatch($podcast)->delay(now()->addMinutes(10));
ProcessPodcast::dispatch($podcast)->onQueue('high-priority');

Job chaining & batching

// chain โ€” run in sequence, stop if any fails
Bus::chain([
    new ProcessPodcast($podcast),
    new OptimizeAudio($podcast),
    new NotifySubscribers($podcast),
])->dispatch();

// batch โ€” run in parallel, track collective progress/completion
Bus::batch([
    new ImportCsvRow(1), new ImportCsvRow(2),
])->then(fn (Batch $batch) => /* all jobs succeeded */)
  ->catch(fn (Batch $batch, Throwable $e) => /* a job failed */)
  ->dispatch();

๐Ÿšš Queue Drivers & Workers

DriverUse case
syncRuns immediately, no real queueing (local dev default)
databaseSimple, stores jobs in a DB table โ€” fine for low/medium volume
redisFast, most common production choice
sqsAWS-managed queue โ€” good for cloud-native, auto-scaling setups
# .env
QUEUE_CONNECTION=redis

# running a worker (must stay running โ€” use Supervisor in production)
php artisan queue:work
php artisan queue:work --queue=high-priority,default # priority order
php artisan queue:work --tries=3  # retry attempts before marking failed
Production: use Supervisor (Linux process monitor) to keep queue:work running and auto-restart it if it crashes. After deploying new code, run php artisan queue:restart so workers pick up the new code (workers cache code in memory and won't see changes otherwise).

โš ๏ธ Failed Jobs & Retries

php artisan queue:failed-table  # migration for the failed_jobs table
php artisan migrate
php artisan queue:failed         # list failed jobs
php artisan queue:retry 5           # retry a specific failed job by ID
php artisan queue:retry all       # retry everything
class ProcessPodcast implements ShouldQueue
{
    public int $tries = 3;            // retry up to 3 times
    public int $backoff = 60;          // wait 60s between retries

    public function failed(Throwable $exception): void
    {
        // notify admin, log, etc. โ€” runs after ALL retries are exhausted
    }
}

๐Ÿ“ข Events & Listeners

Events decouple your app: instead of a controller calling five things directly, it fires one event, and any number of independent Listeners react to it.

php artisan make:event OrderShipped
php artisan make:listener SendShipmentNotification --event=OrderShipped
class OrderShipped
{
    use Dispatchable;
    public function __construct(public Order $order) {}
}

class SendShipmentNotification
{
    public function handle(OrderShipped $event): void
    {
        Mail::to($event->order->user)->send(new OrderShippedMail($event->order));
    }
}

// firing it:
OrderShipped::dispatch($order);
Laravel 11+: events/listeners are auto-discovered โ€” no need to register them in EventServiceProvider unless you want explicit control. L8โ€“10 required manual registration in $listen.

Queueing a listener

class SendShipmentNotification implements ShouldQueue { // just add this interface
    // handle() now runs on the queue instead of synchronously
}
Part 6

API Development

Shaping data for consumption by mobile apps, SPAs, and third parties โ€” resources, versioning, rate limiting, and error handling.

๐Ÿ“ฆ API Resources

Resources transform Eloquent models/collections into clean, controlled JSON โ€” instead of exposing raw model attributes (which can leak internal fields).

php artisan make:resource PostResource
php artisan make:resource PostCollection
class PostResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'author' => $this->user->name,        // reshape relations however you like
            'published_at' => $this->published_at?->toIso8601String(),
            'comments_count' => $this->whenCounted('comments'), // only included if withCount() was used
        ];
    }
}

// controller:
return new PostResource($post);
return PostResource::collection(Post::paginate()); // pagination meta is preserved automatically

๐Ÿ”ข API Versioning

Common approaches, roughly in order of how often they're used in production:

ApproachExample
URI prefix/api/v1/posts, /api/v2/posts โ€” route group prefix, simplest and most common
Header-basedAccept: application/vnd.myapp.v2+json โ€” cleaner URLs, more complex routing
Separate Resource classes per versionV1\PostResource vs V2\PostResource sharing the same model
Route::prefix('v1')->group(function () {
    Route::apiResource('posts', Api\V1\PostController::class);
});

๐Ÿšจ API Error Handling

By default, Laravel returns JSON automatically for exceptions when the request expects JSON (e.g. Accept: application/json). Form Request validation failures auto-respond with a 422 + an errors object.

// bootstrap/app.php (L11+) โ€” custom exception rendering
->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (ModelNotFoundException $e, Request $request) {
        if ($request->is('api/*')) {
            return response()->json(['message' => 'Resource not found.'], 404);
        }
    });
})
Laravel 8โ€“10: custom exception rendering was done by overriding render() in app/Exceptions/Handler.php. L11+ moved this to the fluent withExceptions() closure in bootstrap/app.php.

Rate limiting an API

// in a service provider, or RouteServiceProvider on older versions:
RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
Part 7

Testing

PHPUnit/Pest, feature and unit tests, HTTP testing helpers, database testing, and mocking.

๐Ÿงช PHPUnit vs Pest

Laravel ships with PHPUnit by default. Pest is a more expressive testing framework built on top of PHPUnit โ€” same engine, nicer syntax. Both run with php artisan test.

PHPUnit style
class PostTest extends TestCase
{
    public function test_post_can_be_created(): void
    {
        $post = Post::factory()->create();
        $this->assertDatabaseHas('posts', ['id' => $post->id]);
    }
}
Pest style
it('creates a post', function () {
    $post = Post::factory()->create();
    expect($post)->toBeInstanceOf(Post::class);
    $this->assertDatabaseHas('posts', ['id' => $post->id]);
});
php artisan make:test PostTest          # Feature test (boots the full app)
php artisan make:test PostTest --unit   # Unit test (no app boot, isolated logic)

Feature vs Unit tests

Unit tests test a single class/method in isolation with no framework boot โ€” fast, but most real app logic touches the DB or framework, so they're used less in typical Laravel apps. Feature tests boot the full app and test the whole flow (HTTP request โ†’ DB โ†’ response) โ€” this is where most Laravel test coverage lives.

๐ŸŒ HTTP Testing Helpers

public function test_guest_is_redirected_to_login(): void
{
    $response = $this->get('/dashboard');
    $response->assertRedirect('/login');
}

public function test_authenticated_user_can_create_post(): void
{
    $user = User::factory()->create();

    $response = $this->actingAs($user)
        ->post('/posts', ['title' => 'Hello', 'body' => 'World']);

    $response->assertStatus(302);
    $response->assertSessionHasNoErrors();
    $this->assertDatabaseHas('posts', ['title' => 'Hello']);
}

// JSON API testing:
$response = $this->getJson('/api/posts');
$response->assertOk()->assertJsonCount(3)->assertJsonStructure(['data' => ['*' => ['id', 'title']]]);

๐Ÿ—„๏ธ Database Testing

RefreshDatabase resets your test database between tests so each test starts clean โ€” essential since tests can run in any order and shouldn't depend on each other's leftover data.

class PostTest extends TestCase
{
    use RefreshDatabase; // wraps each test in a transaction, rolls back after

    public function test_example(): void
    {
        Post::factory()->count(3)->create();
        $this->assertDatabaseCount('posts', 3);
    }
}
Tip: Set a dedicated DB_CONNECTION/database for testing in phpunit.xml (often SQLite in-memory: DB_CONNECTION=sqlite, DB_DATABASE=:memory:) โ€” much faster than hitting MySQL for every test run.

๐ŸŽญ Mocking

Mocking replaces real dependencies (APIs, mail, queues) with fakes during a test, so you're not actually sending emails or hitting third-party services.

Mail::fake();
// ... perform action that should send mail ...
Mail::assertSent(OrderShippedMail::class);

Queue::fake();
// ... perform action that should dispatch a job ...
Queue::assertPushed(ProcessPodcast::class);

Http::fake([
    'api.stripe.com/*' => Http::response(['status' => 'ok'], 200),
]);

// mocking a bound interface/class directly:
$this->mock(PaymentGatewayInterface::class, function (MockInterface $mock) {
    $mock->shouldReceive('charge')->once()->andReturn(true);
});
Part 8

Performance

Caching strategies, production-only caching commands, and opcode caching โ€” the layer on top of query optimization (Part 2) that gets apps production-fast.

๐Ÿš€ Caching

DriverUse case
redisFast, supports tags/atomic locks โ€” most common production choice
memcachedSimilar to Redis, simpler feature set
databaseNo extra infra needed, slower than Redis/Memcached
fileLocal dev default, not suitable for multi-server production
Cache::put('key', 'value', 600);     // store for 600 seconds
$value = Cache::get('key', 'default');

// remember() โ€” the most-used pattern: get from cache, or compute + cache if missing
$posts = Cache::remember('posts.popular', 3600, function () {
    return Post::orderBy('views', 'desc')->take(10)->get();
});

Cache::rememberForever('site.settings', fn () => Setting::all());
Cache::forget('posts.popular');  // invalidate manually, e.g. in an Observer when a post changes

// cache tags (Redis/Memcached only) โ€” invalidate a whole group at once:
Cache::tags(['posts'])->put('posts.popular', $data, 3600);
Cache::tags(['posts'])->flush(); // clears every key tagged 'posts'
See Part 2: Large Dataset Handling and Eager Loading for query-level performance โ€” caching and query optimization work together, not as substitutes for each other.

๐Ÿ“ฆ Production Caching Commands

These cache Laravel's own internals (not your application data) to skip re-parsing files on every request โ€” always run on deploy, never in local dev (changes won't show until you clear them).

php artisan config:cache   # combines all config/*.php into one file
php artisan route:cache    # compiles routes โ€” closures NOT allowed when cached
php artisan view:cache     # precompiles all Blade views
php artisan event:cache    # caches auto-discovered event/listener map

# clear all of the above:
php artisan optimize:clear
Common gotcha: after running config:cache, calls to env() outside of config files stop working (they return null) โ€” always read environment values through config('app.name') rather than env('APP_NAME') directly in your application code.

โšก OPcache

OPcache is a PHP extension (not Laravel-specific) that caches compiled PHP bytecode in memory, skipping the parse/compile step on every request. It's a server-level setting, not something you toggle from artisan.

; php.ini
opcache.enable=1
opcache.validate_timestamps=0  ; production: don't check file mtimes on every request
opcache.memory_consumption=256
Important: with validate_timestamps=0, OPcache won't notice file changes after deploy โ€” you must clear it on every deploy via opcache_reset() or a web server reload.
Part 9

Production Architecture & Code Structure

Real projects outgrow "everything in the controller." Services, Actions, Repositories, DTOs, and Pipelines organize business logic so it scales with the codebase.

๐Ÿ›Ž๏ธ Service Classes

A Service extracts business logic out of fat controllers into a dedicated, reusable, testable class โ€” especially useful when the same logic is needed from a controller, a job, AND a console command.

class OrderService
{
    public function __construct(private PaymentGatewayInterface $gateway) {}

    public function placeOrder(User $user, Cart $cart): Order
    {
        return DB::transaction(function () use ($user, $cart) {
            $order = Order::create(['user_id' => $user->id, 'total' => $cart->total()]);
            $this->gateway->charge($user, $cart->total());
            return $order;
        });
    }
}

// controller becomes thin:
public function store(Request $request, OrderService $orders) {
    $order = $orders->placeOrder($request->user(), $request->user()->cart);
    return redirect()->route('orders.show', $order);
}

โšก Action Classes

An Action is a stricter version of a Service: one class, one operation, usually one public method (often named execute() or handle()). Where Services group related operations together, Actions are deliberately single-purpose โ€” easy to find, easy to test, easy to reuse from anywhere.

class CreatePost
{
    public function execute(User $author, array $data): Post
    {
        $post = $author->posts()->create([
            ...$data,
            'slug' => Str::slug($data['title']),
        ]);
        PostCreated::dispatch($post);
        return $post;
    }
}

// usage anywhere โ€” controller, command, another action:
$post = (new CreatePost)->execute($user, $request->validated());

๐Ÿ—ƒ๏ธ Repository Pattern

A Repository abstracts data access behind an interface, so the rest of your app doesn't know (or care) whether data comes from Eloquent, a third-party API, or a cache. Most useful when you genuinely expect to swap data sources, or want to fully decouple business logic from Eloquent for testing.

interface PostRepositoryInterface
{
    public function find(int $id): ?Post;
    public function published(): Collection;
}

class EloquentPostRepository implements PostRepositoryInterface
{
    public function find(int $id): ?Post { return Post::find($id); }
    public function published(): Collection { return Post::where('published', true)->get(); }
}
// bind the interface to this implementation in a Service Provider (see Part 3: Service Container)
Pragmatic note: in most Laravel apps, Eloquent models already act as a reasonable abstraction, and a full Repository layer on top adds indirection without much benefit. It earns its place in larger codebases with multiple data sources or heavy test-isolation needs โ€” not as a default for every project.

๐Ÿ“จ DTOs (Data Transfer Objects)

A DTO is a simple, typed object that carries data between layers โ€” instead of passing loose arrays (where typos in keys fail silently) between a controller, service, and job.

final class CreatePostData
{
    public function __construct(
        public readonly string $title,
        public readonly string $body,
        public readonly ?int $categoryId = null,
    ) {}

    public static function fromRequest(StorePostRequest $request): self
    {
        return new self(...$request->validated());
    }
}
// now CreatePost::execute() can type-hint CreatePostData instead of a loose array โ€”
// typos become IDE/static-analysis errors instead of silent runtime bugs

๐Ÿ”ง Pipelines (Pipeline Facade)

The Pipeline facade runs an object through a series of "pipes" (classes), each able to inspect/modify it before passing it along โ€” the exact same pattern middleware uses internally, available for your own use cases.

$user = Pipeline::send($user)
    ->through([
        VerifyEmailDomain::class,
        ApplyDefaultRole::class,
        SendWelcomeEmail::class,
    ])
    ->then(fn (User $user) => $user);

// each pipe class:
class ApplyDefaultRole
{
    public function handle(User $user, Closure $next)
    {
        $user->assignRole('member');
        return $next($user);
    }
}
When to reach for this: a fixed sequence of independent transformation steps on one object โ€” e.g. a multi-stage signup process, or building a complex search query from several optional filters.
Part 10

DevOps & Deployment

Environment config, essential Artisan commands, task scheduling, and a practical production deployment checklist.

๐ŸŒ Environment Configuration

# .env โ€” NEVER commit this file (add to .gitignore)
APP_ENV=production
APP_DEBUG=false        # CRITICAL: true in production leaks stack traces to visitors
APP_KEY=base64:...     # generated via: php artisan key:generate
DB_CONNECTION=mysql
QUEUE_CONNECTION=redis
CACHE_STORE=redis
Security basics: APP_DEBUG=true on a live site will display full stack traces (including env values) to any visitor who triggers an error. Always false in production. Commit a .env.example with dummy values instead of the real .env.

Always read config via config('app.name') in application code, not env('APP_NAME') directly โ€” once config:cache runs, raw env() calls outside config files return null.

โŒจ๏ธ Essential Artisan Commands

CommandPurpose
php artisan make:model/controller/migration/job/...Generate boilerplate files
php artisan migrate / migrate:rollback / migrate:freshSchema management
php artisan tinkerInteractive REPL โ€” test Eloquent queries live
php artisan route:listSee every registered route
php artisan queue:work / queue:restartRun / restart queue workers
php artisan config:cache / route:cache / view:cacheProduction performance caching (see Part 8)
php artisan optimize / optimize:clearRun all caching commands at once / clear them all
php artisan make:command SendInvoicesCustom Artisan command
php artisan storage:linkSymlinks public/storage โ†’ storage/app/public for public file access

โฐ Task Scheduling

Instead of juggling dozens of cron entries, Laravel lets you define all scheduled tasks in PHP, then point cron at a single command.

Laravel 11+ โ€” bootstrap/app.php
->withSchedule(function (Schedule $schedule) {
    $schedule->command('reports:daily')->dailyAt('01:00');
    $schedule->job(new CleanupOldFiles)->weekly();
})
Laravel 8โ€“10: the same code lived in app/Console/Kernel.php's schedule() method instead of bootstrap/app.php.
The one cron entry needed on the server (all versions)
# crontab -e
* * * * * cd /path-to-app && php artisan schedule:run >> /dev/null 2>&1

โœ… Production Deployment Checklist

  1. composer install --optimize-autoloader --no-dev โ€” skip dev dependencies, optimize the class autoloader
  2. php artisan migrate --force โ€” run pending migrations (--force needed since APP_ENV=production normally prompts for confirmation)
  3. php artisan config:cache, route:cache, view:cache (or simply php artisan optimize)
  4. php artisan queue:restart โ€” so workers load the newly deployed code
  5. Confirm APP_DEBUG=false and APP_ENV=production in .env
  6. Reload PHP-FPM / clear OPcache so cached bytecode reflects the new deploy
  7. Run npm run build for frontend assets if the app uses Vite/Mix

Zero-downtime deploys (concept)

The core idea: build the new release in a fresh directory, run migrations and caching against it, then atomically switch a symlink (e.g. current) to point at the new release directory โ€” so there's no window where old code and a half-finished new deploy are both live. Tools like Laravel Forge, Deployer, or Envoyer automate this pattern; it's also exactly how most container-orchestrated (Docker/Kubernetes) deploys work at a higher level.

โ†‘