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.
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
public/index.phpโ entry point, loads autoloader + bootstraps the appbootstrap/app.phpโ builds the Application (the container) and registers core bindings- HTTP Kernel โ sends the request through global middleware (CORS, trim strings, etc.)
- Service Providers โ all registered providers'
register()thenboot()methods run - Router โ matches the URI + method to a route, resolves route middleware
- Controller / Closure โ runs your business logic, returns data or a Response
- Response flows back out through middleware, then is sent to the browser
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
composer create-project laravel/laravel example-app
# or via the Laravel installer
laravel new example-app
Key directories
| Path | Purpose |
|---|---|
app/Models | Eloquent models |
app/Http/Controllers | Controllers |
app/Http/Middleware | Custom middleware |
app/Providers | Service providers |
routes/web.php | Browser/session routes |
routes/api.php | Stateless API routes (token auth, no CSRF) |
resources/views | Blade templates |
database/migrations | Schema migrations |
database/factories | Model factories for seeding/testing |
database/seeders | Database seeders |
config/*.php | All framework + package configuration |
.env | Environment-specific secrets/settings (never commit) |
bootstrap/app.php | App bootstrapping โ middleware, routing, exceptions (L11+) |
storage/ | Logs, compiled views, file uploads (local disk) |
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).
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::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
Route::get('/profile', [ProfileController::class, 'show'])->name('profile.show');
// usage anywhere:
route('profile.show', ['user' => 1]); // -> /profile/1
Route::prefix('admin')->middleware(['auth', 'verified'])->name('admin.')->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard'); // admin.dashboard
});
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.
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', ...);
});
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
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.
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
}
}
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->alias(['subscribed' => EnsureUserIsSubscribed::class]);
$middleware->append(SomeGlobalMiddleware::class); // global, runs on every request
})
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.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
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.
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
| Rule | What it checks |
|---|---|
required | Field must be present and not empty |
nullable | Allows null without failing other rules |
string / integer / numeric / boolean / array | Type checks |
email | Valid email format |
unique:table,column | No existing row with this value |
exists:table,column | Value must exist in another table (FK validity) |
min:x / max:x | Length (string) or value (number) bounds |
confirmed | Requires matching {field}_confirmation input |
date / date_format:Y-m-d | Valid date / specific format |
in:a,b,c | Value must be one of the list |
file / image / mimes:jpg,png | Upload type checks |
regex:pattern | Custom 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';
});
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).
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
| Method | Produces |
|---|---|
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)
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
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
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');
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.
$users = User::all(); // 1 query
foreach ($users as $user) {
echo $user->posts->count(); // +1 query PER user โ N+1!
}
$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();
$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();
$users = User::all();
if ($needsPosts) {
$users->load('posts'); // load relation after the fact, still avoids N+1
}
DB::listen() to spot N+1 queries before they reach production.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.
| Method | What it does | Example |
|---|---|---|
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) |
// 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),
);
}
}
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',
];
}
}
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();
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();
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.
protected static function booted(): void
{
static::creating(function (Post $post) {
$post->slug = Str::slug($post->title);
});
}
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
| Method | How it works | Best for |
|---|---|---|
paginate(15) | Runs a COUNT(*) + the page query, gives page links + total | Standard pagination with page numbers |
simplePaginate(15) | Skips COUNT(*), only knows if there's a next page | Faster โ when you only need Next/Prev, not total pages |
cursorPaginate(15) | Uses a WHERE on the last seen ID/column instead of OFFSET | Huge 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);
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.
| Method | How 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 |
User::chunk(1000, function (Collection $users) {
foreach ($users as $user) {
// process each user
}
});
User::lazy()->each(function (User $user) {
// runs over ALL users, 1000 at a time under the hood, low memory
});
foreach (User::cursor() as $user) {
// only ONE user in memory at any moment
}
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;
}
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()
| Method | Behavior |
|---|---|
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'));
}
}
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\"
Authentication & Authorization
Who is the user (authentication), and what are they allowed to do (authorization) โ guards, providers, Sanctum, Gates, and Policies.
๐ Auth Scaffolding Options
| Package | What it gives you |
|---|---|
| Breeze | Minimal login/register/reset views + routes, using Blade or a JS stack (Vue/React via Inertia). Best starting point for most apps. |
| Jetstream | Breeze + extras: teams, API tokens (Sanctum), two-factor auth, profile management UI. Heavier, opinionated. |
| Fortify | Headless backend logic only (no views) โ used when you want full control over the frontend. |
| Sanctum | Lightweight API token + SPA cookie authentication โ see below. |
| Passport | Full 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')) { ... }
๐ช 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();
}
}
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
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
| Driver | Use case |
|---|---|
sync | Runs immediately, no real queueing (local dev default) |
database | Simple, stores jobs in a DB table โ fine for low/medium volume |
redis | Fast, most common production choice |
sqs | AWS-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
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);
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
}
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:
| Approach | Example |
|---|---|
| URI prefix | /api/v1/posts, /api/v2/posts โ route group prefix, simplest and most common |
| Header-based | Accept: application/vnd.myapp.v2+json โ cleaner URLs, more complex routing |
| Separate Resource classes per version | V1\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);
}
});
})
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());
});
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.
class PostTest extends TestCase
{
public function test_post_can_be_created(): void
{
$post = Post::factory()->create();
$this->assertDatabaseHas('posts', ['id' => $post->id]);
}
}
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);
}
}
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);
});
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
| Driver | Use case |
|---|---|
redis | Fast, supports tags/atomic locks โ most common production choice |
memcached | Similar to Redis, simpler feature set |
database | No extra infra needed, slower than Redis/Memcached |
file | Local 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'
๐ฆ 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
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
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.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)
๐จ 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);
}
}
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
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
| Command | Purpose |
|---|---|
php artisan make:model/controller/migration/job/... | Generate boilerplate files |
php artisan migrate / migrate:rollback / migrate:fresh | Schema management |
php artisan tinker | Interactive REPL โ test Eloquent queries live |
php artisan route:list | See every registered route |
php artisan queue:work / queue:restart | Run / restart queue workers |
php artisan config:cache / route:cache / view:cache | Production performance caching (see Part 8) |
php artisan optimize / optimize:clear | Run all caching commands at once / clear them all |
php artisan make:command SendInvoices | Custom Artisan command |
php artisan storage:link | Symlinks 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.
->withSchedule(function (Schedule $schedule) {
$schedule->command('reports:daily')->dailyAt('01:00');
$schedule->job(new CleanupOldFiles)->weekly();
})
app/Console/Kernel.php's schedule() method instead of bootstrap/app.php.# crontab -e
* * * * * cd /path-to-app && php artisan schedule:run >> /dev/null 2>&1
โ Production Deployment Checklist
composer install --optimize-autoloader --no-devโ skip dev dependencies, optimize the class autoloaderphp artisan migrate --forceโ run pending migrations (--forceneeded sinceAPP_ENV=productionnormally prompts for confirmation)php artisan config:cache,route:cache,view:cache(or simplyphp artisan optimize)php artisan queue:restartโ so workers load the newly deployed code- Confirm
APP_DEBUG=falseandAPP_ENV=productionin.env - Reload PHP-FPM / clear OPcache so cached bytecode reflects the new deploy
- Run
npm run buildfor 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.