The Laravel patterns you need every day — Artisan commands, routing, Eloquent ORM, migrations, Blade templating, queues, and testing — with copy-ready code in every section.
Quick reference
| Task | Command / Code |
|---|---|
| Create project | composer create-project laravel/laravel app |
| Start dev server | php artisan serve |
| List all routes | php artisan route:list |
| Create controller | php artisan make:controller PostController |
| Resource controller | php artisan make:controller PostController --resource |
| Create model | php artisan make:model Post |
| Model + migration | php artisan make:model Post -m |
| Create migration | php artisan make:migration create_posts_table |
| Run migrations | php artisan migrate |
| Rollback | php artisan migrate:rollback |
| Fresh migrate + seed | php artisan migrate:fresh --seed |
| Create seeder | php artisan make:seeder PostSeeder |
| Create factory | php artisan make:factory PostFactory |
| Create middleware | php artisan make:middleware AuthMiddleware |
| Create request | php artisan make:request StorePostRequest |
| Create job | php artisan make:job ProcessPodcast |
| Create event | php artisan make:event OrderShipped |
| Clear cache | php artisan cache:clear |
| Clear config cache | php artisan config:clear |
| Clear route cache | php artisan route:clear |
| Tinker REPL | php artisan tinker |
| Run tests | php artisan test |
| Run queue worker | php artisan queue:work |
Routing
// routes/web.php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PostController;
// Basic routes
Route::get('/posts', [PostController::class, 'index']);
Route::post('/posts', [PostController::class, 'store']);
Route::get('/posts/{post}', [PostController::class, 'show']);
Route::put('/posts/{post}', [PostController::class, 'update']);
Route::delete('/posts/{post}', [PostController::class, 'destroy']);
// Resource routes (all 7 CRUD routes in one line)
Route::resource('posts', PostController::class);
// API resource routes (excludes create/edit — no HTML forms)
Route::apiResource('posts', PostController::class);
// Route parameters
Route::get('/users/{id}/posts/{post}', function ($id, $post) {
return "User {$id}, Post {$post}";
});
// Optional parameter
Route::get('/search/{query?}', function ($query = null) {
return $query ?? 'all';
});
// Named routes
Route::get('/profile', [ProfileController::class, 'show'])->name('profile.show');
// Generate URL: route('profile.show')
// Route groups
Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
Route::resource('users', UserController::class);
});
// Redirect route
Route::redirect('/here', '/there');
// routes/api.php — API routes (stateless, no CSRF, /api/ prefix)
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('posts', PostController::class);
});
Controllers
// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;
use App\Models\Post;
use App\Http\Requests\StorePostRequest;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index()
{
$posts = Post::with('user')->latest()->paginate(15);
return view('posts.index', compact('posts'));
}
public function show(Post $post) // Route model binding — auto-fetches by ID
{
return view('posts.show', compact('post'));
}
public function store(StorePostRequest $request)
{
$post = Post::create($request->validated());
return redirect()->route('posts.show', $post)
->with('success', 'Post created!');
}
public function update(StorePostRequest $request, Post $post)
{
$post->update($request->validated());
return redirect()->route('posts.show', $post);
}
public function destroy(Post $post)
{
$post->delete();
return redirect()->route('posts.index');
}
}
Form requests (validation)
// app/Http/Requests/StorePostRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create', Post::class);
}
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'body' => 'required|string|min:10',
'status' => 'required|in:draft,published',
'tags' => 'nullable|array',
'tags.*' => 'string|max:50',
'image' => 'nullable|image|max:2048', // max KB
'user_id' => 'required|exists:users,id',
'email' => 'required|email|unique:users,email,' . $this->user()?->id,
];
}
public function messages(): array
{
return [
'title.required' => 'A title is required.',
];
}
}
Common validation rules:
| Rule | Description |
|---|---|
required |
Must be present and non-empty |
nullable |
May be null |
string |
Must be a string |
integer / numeric |
Must be a number |
email |
Valid email format |
min:n / max:n |
Length or value constraints |
unique:table,col |
Must not exist in DB column |
exists:table,col |
Must exist in DB column |
in:a,b,c |
Must be one of the listed values |
confirmed |
Field must have _confirmation match |
image |
JPEG, PNG, GIF, BMP, SVG, or WEBP |
array |
Must be an array |
date / date_format:Y-m-d |
Valid date |
boolean |
true, false, 1, 0, "true", "false" |
url |
Valid URL format |
regex:/pattern/ |
Must match regex |
sometimes |
Only validate if field is present |
Eloquent ORM
Model definition
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Post extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = ['title', 'body', 'status', 'user_id'];
// OR use $guarded = [] to allow all (less safe)
protected $casts = [
'published_at' => 'datetime',
'settings' => 'array', // JSON column → PHP array
'is_featured' => 'boolean',
'price' => 'decimal:2',
];
protected $hidden = ['deleted_at']; // Excluded from JSON
// Computed attribute (accessor)
public function getExcerptAttribute(): string
{
return str($this->body)->limit(150);
}
// Modern attribute (Laravel 9+)
use Illuminate\Database\Eloquent\Casts\Attribute;
protected function excerpt(): Attribute
{
return Attribute::make(
get: fn ($value, $attributes) => str($attributes['body'])->limit(150),
);
}
// Relationships
public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(User::class);
}
public function comments(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Comment::class);
}
public function tags(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
{
return $this->belongsToMany(Tag::class)->withTimestamps();
}
// Query scope
public function scopePublished($query)
{
return $query->where('status', 'published');
}
}
Eloquent CRUD
// CREATE
$post = Post::create(['title' => 'Hello', 'body' => 'World', 'user_id' => 1]);
$post = new Post(['title' => 'Hello']);
$post->body = 'World';
$post->save();
// READ
$post = Post::find(1); // null if not found
$post = Post::findOrFail(1); // 404 if not found
$posts = Post::all();
$posts = Post::where('status', 'published')->get();
$post = Post::where('slug', $slug)->first(); // null if not found
$post = Post::firstOrCreate(['slug' => $slug], ['title' => $title]); // find or create
$post = Post::firstOrNew(['slug' => $slug]); // find or build (not saved)
$count = Post::where('status', 'published')->count();
$exists = Post::where('slug', $slug)->exists();
// UPDATE
$post = Post::findOrFail($id);
$post->update(['title' => 'New title']); // mass update (needs $fillable)
$post->title = 'New title';
$post->save();
Post::where('status', 'draft')->update(['status' => 'published']); // bulk update
// DELETE
$post->delete(); // soft delete if trait enabled
Post::destroy([1, 2, 3]); // delete by IDs
Post::where('created_at', '<', now()->subYear())->delete(); // bulk delete
// RESTORE (soft delete)
Post::withTrashed()->find($id)->restore();
Querying
// Ordering, limiting, pagination
Post::orderBy('created_at', 'desc')->take(10)->get();
Post::latest()->paginate(15); // paginate() returns LengthAwarePaginator
Post::latest()->simplePaginate(15); // simpler prev/next pagination
Post::latest()->cursorPaginate(15); // efficient cursor-based pagination
// Filtering
Post::where('status', 'published')
->where('user_id', auth()->id())
->whereNotNull('published_at')
->whereBetween('created_at', [now()->subMonth(), now()])
->whereIn('status', ['published', 'draft'])
->get();
// OR conditions
Post::where('title', 'like', "%{$search}%")
->orWhere('body', 'like', "%{$search}%")
->get();
// whereHas — filter by relationship
Post::whereHas('comments', function ($q) {
$q->where('approved', true);
})->get();
// Eager loading (prevents N+1)
Post::with('user', 'tags')->get();
Post::with(['comments' => function ($q) {
$q->latest()->take(5);
}])->get();
// Lazy eager loading
$posts = Post::all();
$posts->load('user');
// Select specific columns
Post::select('id', 'title', 'user_id')->get();
// Raw expressions
Post::selectRaw('count(*) as total, status')->groupBy('status')->get();
Post::whereRaw('DATE(created_at) = ?', [today()->toDateString()])->get();
// Chunking large datasets
Post::chunk(100, function ($posts) {
foreach ($posts as $post) {
// process
}
});
// Using query scope
Post::published()->latest()->paginate(15);
Relationships
// One-to-Many
class User extends Model {
public function posts(): HasMany { return $this->hasMany(Post::class); }
}
class Post extends Model {
public function user(): BelongsTo { return $this->belongsTo(User::class); }
}
// Usage
$user->posts()->create(['title' => 'New post']);
$post->user; // access related model
// Many-to-Many (with pivot table: post_tag)
class Post extends Model {
public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class); }
}
$post->tags()->attach($tagId);
$post->tags()->detach($tagId);
$post->tags()->sync([1, 2, 3]); // sync (removes unlisted)
$post->tags()->toggle([1, 2]); // add if missing, remove if present
$post->tags()->attach($tagId, ['order' => 1]); // pivot data
// Has One Through / Has Many Through
class Country extends Model {
// Country → has many Users → has many Posts
public function posts(): HasManyThrough {
return $this->hasManyThrough(Post::class, User::class);
}
}
// Polymorphic
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'); }
}
Migrations
// database/migrations/2024_01_01_000000_create_posts_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id(); // BIGINT UNSIGNED AUTO_INCREMENT PK
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title'); // VARCHAR(255)
$table->string('slug')->unique();
$table->text('body');
$table->longText('content')->nullable();
$table->enum('status', ['draft', 'published'])->default('draft');
$table->json('meta')->nullable();
$table->unsignedInteger('views')->default(0);
$table->decimal('price', 10, 2)->nullable();
$table->boolean('is_featured')->default(false);
$table->timestamp('published_at')->nullable();
$table->timestamps(); // created_at + updated_at
$table->softDeletes(); // deleted_at (for SoftDeletes)
$table->index('status');
$table->index(['user_id', 'created_at']);
$table->fullText('body'); // MySQL FULLTEXT index
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};
// Modify existing table
Schema::table('posts', function (Blueprint $table) {
$table->string('subtitle')->nullable()->after('title');
$table->dropColumn('old_column');
$table->renameColumn('old', 'new');
$table->index('subtitle');
});
Column types quick reference:
| Method | SQL type |
|---|---|
id() |
BIGINT UNSIGNED PK AUTO_INCREMENT |
string('col', 100) |
VARCHAR(100) |
text() / longText() |
TEXT / LONGTEXT |
integer() / bigInteger() |
INT / BIGINT |
unsignedBigInteger() |
BIGINT UNSIGNED |
decimal(8, 2) |
DECIMAL(8,2) |
boolean() |
TINYINT(1) |
date() / dateTime() |
DATE / DATETIME |
timestamp() |
TIMESTAMP |
timestamps() |
created_at + updated_at TIMESTAMP |
json() |
JSON |
uuid() |
CHAR(36) |
foreignId('user_id') |
BIGINT UNSIGNED + FK helper |
Blade templates
{{-- resources/views/posts/index.blade.php --}}
@extends('layouts.app')
@section('title', 'Posts')
@section('content')
<h1>All Posts</h1>
{{-- Variables (auto-escaped XSS) --}}
<p>{{ $post->title }}</p>
{{-- Unescaped HTML (only for trusted content) --}}
{!! $post->body !!}
{{-- Control structures --}}
@if ($posts->isEmpty())
<p>No posts found.</p>
@elseif ($posts->count() === 1)
<p>One post.</p>
@else
<p>{{ $posts->count() }} posts.</p>
@endif
@unless ($user->isAdmin())
<p>Access restricted.</p>
@endunless
{{-- Loops --}}
@foreach ($posts as $post)
<div>
<a href="{{ route('posts.show', $post) }}">{{ $post->title }}</a>
@if ($loop->first) <span>New!</span> @endif
@if ($loop->last) <hr> @endif
<small>{{ $loop->index }} of {{ $loop->count }}</small>
</div>
@endforeach
@forelse ($posts as $post)
<p>{{ $post->title }}</p>
@empty
<p>No posts.</p>
@endforelse
{{-- Pagination --}}
{{ $posts->links() }}
{{-- Auth checks --}}
@auth
<a href="{{ route('posts.create') }}">New post</a>
@endauth
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}">Edit</a>
@endcan
@endsection
@push('scripts')
<script>console.log('page loaded');</script>
@endpush
{{-- resources/views/layouts/app.blade.php --}}
<!DOCTYPE html>
<html>
<head>
<title>@yield('title', 'My App')</title>
@stack('styles')
</head>
<body>
@include('partials.nav')
<main>@yield('content')</main>
@stack('scripts')
</body>
</html>
{{-- Blade components (Laravel 7+) --}}
{{-- resources/views/components/alert.blade.php --}}
<div class="alert alert-{{ $type ?? 'info' }}">
{{ $slot }}
@if (isset($title)) <h4>{{ $title }}</h4> @endif
</div>
{{-- Usage --}}
<x-alert type="danger">
<x-slot name="title">Error</x-slot>
Something went wrong.
</x-alert>
Authentication (Laravel Breeze / Sanctum)
# Install Breeze (simple auth scaffolding)
composer require laravel/breeze --dev
php artisan breeze:install blade
php artisan migrate
# Install Sanctum (API tokens / SPA auth)
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
// API token authentication with Sanctum
// routes/api.php
Route::post('/login', function (Request $request) {
$request->validate(['email' => 'required|email', 'password' => 'required']);
if (!Auth::attempt($request->only('email', 'password'))) {
return response()->json(['message' => 'Invalid credentials'], 401);
}
$token = $request->user()->createToken('api-token')->plainTextToken;
return response()->json(['token' => $token]);
});
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', fn (Request $request) => $request->user());
Route::post('/logout', function (Request $request) {
$request->user()->currentAccessToken()->delete();
return response()->json(['message' => 'Logged out']);
});
});
// Auth helpers
auth()->user() // current user (null if guest)
auth()->id() // current user ID
auth()->check() // is logged in?
auth()->guest() // is guest?
Auth::login($user) // log in a user
Auth::logout() // log out
Service providers and dependency injection
// app/Providers/AppServiceProvider.php
use App\Repositories\PostRepositoryInterface;
use App\Repositories\EloquentPostRepository;
public function register(): void
{
// Bind interface to concrete implementation
$this->app->bind(PostRepositoryInterface::class, EloquentPostRepository::class);
// Singleton — same instance every time
$this->app->singleton(MyService::class, function ($app) {
return new MyService($app->make(Config::class));
});
}
public function boot(): void
{
// Boot logic — runs after all providers registered
Post::observe(PostObserver::class);
Gate::define('update-post', fn ($user, $post) => $user->id === $post->user_id);
}
// Controller constructor injection
class PostController extends Controller
{
public function __construct(private PostRepositoryInterface $posts) {}
public function index()
{
return view('posts.index', ['posts' => $this->posts->all()]);
}
}
Queues and jobs
// app/Jobs/SendWelcomeEmail.php
namespace App\Jobs;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SendWelcomeEmail implements ShouldQueue
{
use InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $timeout = 60;
public function __construct(public User $user) {}
public function handle(): void
{
$this->user->notify(new WelcomeNotification());
}
public function failed(\Throwable $e): void
{
// Log or alert on permanent failure
logger()->error('SendWelcomeEmail failed', ['user' => $this->user->id]);
}
}
// Dispatch
SendWelcomeEmail::dispatch($user);
SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(10));
SendWelcomeEmail::dispatchIf($user->wants_email, $user);
// Run worker
// php artisan queue:work --queue=high,default --tries=3
Testing
// tests/Feature/PostTest.php
namespace Tests\Feature;
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PostTest extends TestCase
{
use RefreshDatabase; // Runs migrations + rolls back after each test
public function test_guest_cannot_create_post(): void
{
$response = $this->postJson('/api/posts', ['title' => 'Test']);
$response->assertStatus(401);
}
public function test_user_can_create_post(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user, 'sanctum')
->postJson('/api/posts', [
'title' => 'My Post',
'body' => 'Some content here...',
]);
$response->assertStatus(201)
->assertJsonPath('data.title', 'My Post');
$this->assertDatabaseHas('posts', ['title' => 'My Post', 'user_id' => $user->id]);
}
public function test_post_index_returns_paginated_list(): void
{
Post::factory(20)->create();
$response = $this->getJson('/api/posts');
$response->assertOk()
->assertJsonStructure(['data', 'meta' => ['current_page', 'total']]);
}
}
// database/factories/PostFactory.php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class PostFactory extends Factory
{
public function definition(): array
{
return [
'title' => fake()->sentence(),
'body' => fake()->paragraphs(3, true),
'status' => fake()->randomElement(['draft', 'published']),
'user_id' => \App\Models\User::factory(),
];
}
// State modifier
public function published(): static
{
return $this->state(['status' => 'published', 'published_at' => now()]);
}
}
// Usage in tests
Post::factory(10)->published()->create();
Post::factory()->for($user)->create();
Common mistakes
| Mistake | Fix |
|---|---|
Not setting $fillable on model |
Add $fillable or $guarded = [] to allow create()/update() |
| N+1 query problem | Use ->with('relation') eager loading |
Missing ->validated() in controller |
Use $request->validated() not $request->all() |
Route::resource includes web-only routes in API |
Use Route::apiResource for APIs |
| Storing business logic in controllers | Move to service classes or action classes |
Not using findOrFail() for user-facing routes |
Use findOrFail() to get automatic 404 |
| Querying in Blade templates | Pass data from controller, never query in views |
env() in config files only (not elsewhere) |
Use config('key') at runtime; env() only in config files |
Laravel vs other frameworks
| Feature | Laravel (PHP) | Django (Python) | Express (Node.js) |
|---|---|---|---|
| ORM | Eloquent (Active Record) | Django ORM | Sequelize/Prisma |
| Auth | Sanctum/Passport/Breeze | built-in | Passport.js |
| Template engine | Blade | Django templates | EJS/Handlebars |
| Migrations | Artisan + Schema | manage.py migrate | Knex/Prisma |
| Queues | built-in (Horizon) | Celery | Bull/BullMQ |
| CLI | php artisan | manage.py | No built-in |
| Testing | PHPUnit/Pest | pytest/unittest | Jest/Mocha |
| Ecosystem | Composer / Packagist | pip / PyPI | npm |
FAQ
What version of PHP does Laravel require?
Laravel 11 requires PHP 8.2+. Laravel 10 requires PHP 8.1+. Always check the official docs for your version. Use php -v to check your current version.
What is the difference between $fillable and $guarded?
$fillable is an allowlist — only listed fields can be mass-assigned with create() or update(). $guarded is a blocklist — all fields are mass-assignable except the listed ones. Using $guarded = [] allows everything (use only in controlled contexts like seeders). Never skip both — that opens a mass assignment vulnerability.
How do I fix the N+1 problem in Laravel?
An N+1 happens when you loop over models and access a relationship that wasn't eager-loaded, firing one extra query per item. Fix it with ->with('relation'): Post::with('user')->get() loads all users in one query instead of one per post. Enable Model::preventLazyLoading(true) in AppServiceProvider::boot() during development to detect N+1 automatically.
How do route model binding work?
When a controller parameter name matches a route segment and is type-hinted to a model, Laravel automatically fetches the record from the database. Route::get('/posts/{post}', [PostController::class, 'show']) + public function show(Post $post) auto-fetches Post::findOrFail($id). You can customize the lookup column with Route::get('/posts/{post:slug}', ...).
What is the difference between php artisan migrate and migrate:fresh?
migrate runs only the pending (new) migrations. migrate:fresh drops all tables and reruns all migrations from scratch — never use it in production as it destroys data. migrate:rollback reverses the last batch of migrations. migrate:refresh rollbacks all and re-runs all (slower than fresh, less destructive than fresh for seeding).
How do I handle file uploads in Laravel?
Use $request->file('photo')->store('photos', 'public') to save an upload to storage/app/public/photos/. Run php artisan storage:link to create the public symlink. Validate uploads with 'photo' => 'required|image|max:2048'. Access stored files via asset(Storage::url($path)).