Artisan CLI, Eloquent ORM, Blade templates, middleware, routing, authentication, queues, and Livewire.
<?php
// ── Basic Routes ──
Route::get('/', [HomeController::class, 'index']);
Route::get('/about', [PageController::class, 'about']);
Route::post('/contact', [ContactController::class, 'store']);
// ── Resource Routes (RESTful) ──
Route::resource('posts', PostController::class);
Route::resource('posts', PostController::class)->only(['index', 'show']);
Route::resource('posts', PostController::class)->except(['destroy']);
// ── API Routes (routes/api.php) ──
Route::apiResource('posts', PostApiController::class);
// ── Nested Resources ──
Route::resource('users.posts', UserPostController::class);
// ── Resource with additional routes ──
Route::resource('posts', PostController::class)->names([
'create' => 'posts.build',
'index' => 'posts.all',
]);
Route::resource('posts', PostController::class)->parameters([
'posts' => 'article',
]);
// ── Route Groups ──
Route::middleware(['auth', 'verified'])->group(function () {
Route::resource('posts', PostController::class);
Route::resource('comments', CommentController::class);
});
Route::prefix('admin')->name('admin.')->middleware('admin')->group(function () {
Route::resource('users', AdminUserController::class);
Route::get('/dashboard', [AdminController::class, 'dashboard']);
});
// ── Route Model Binding ──
Route::get('/posts/{post:slug}', [PostController::class, 'show']);
Route::get('/users/{user}', [UserController::class, 'show']);
// ── Fallback Route ──
Route::fallback(fn () => response()->view('errors.404', [], 404));<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class PostController extends Controller
{
public function __construct()
{
$this->middleware('auth')->except(['index', 'show']);
$this->middleware('verified')->only(['create', 'store']);
}
public function index()
{
$posts = Post::with(['author', 'tags'])
->published()
->orderByDesc('published_at')
->paginate(15);
return view('posts.index', compact('posts'));
}
public function show(Post $post)
{
$post->increment('view_count');
$relatedPosts = Post::where('category_id', $post->category_id)
->where('id', '!=', $post->id)
->published()
->take(5)
->get();
return view('posts.show', compact('post', 'relatedPosts'));
}
public function create()
{
return view('posts.create');
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|min:5|max:200|unique:posts',
'body' => 'required|min:20',
'excerpt' => 'nullable|max:500',
'category_id' => 'required|exists:categories,id',
'tags' => 'array',
'tags.*' => 'exists:tags,id',
'cover_image' => 'nullable|image|max:5120',
]);
$validated['author_id'] = auth()->id();
if ($request->hasFile('cover_image')) {
$validated['cover_image'] = $request->file('cover_image')
->store('covers', 'public');
}
$post = Post::create($validated);
$post->tags()->attach($request->input('tags', []));
return redirect()->route('posts.show', $post)
->with('success', 'Post created successfully.');
}
}| HTTP Verb | URI | Action | Route Name |
|---|---|---|---|
| GET | /posts | index | posts.index |
| GET | /posts/create | create | posts.create |
| POST | /posts | store | posts.store |
| GET | /posts/{post} | show | posts.show |
| GET | /posts/{post}/edit | edit | posts.edit |
| PUT/PATCH | /posts/{post} | update | posts.update |
| DELETE | /posts/{post} | destroy | posts.destroy |
| Helper | Returns |
|---|---|
| route("posts.index") | URL for named route |
| route("posts.show", $post) | URL with model key |
| url("/posts") | Absolute URL |
| action("[Controller@method]") | URL from controller action |
| redirect()->route(...) | Redirect to named route |
| redirect()->back() | Redirect to previous page |
| current_route_name() | Current route name |
| is_route("posts.*") | Check current route pattern |
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Cache;
class Post extends Model
{
use SoftDeletes;
protected $fillable = [
'title', 'slug', 'body', 'excerpt', 'author_id',
'category_id', 'status', 'published_at',
];
protected $casts = [
'published_at' => 'datetime',
'is_featured' => 'boolean',
'metadata' => 'array',
];
protected $hidden = ['internal_notes'];
protected $appends = ['reading_time'];
// ── Scopes ──
public function scopePublished($query)
{
return $query->where('status', 'published')
->whereNotNull('published_at')
->where('published_at', '<=', now());
}
public function scopePopular($query, $minViews = 100)
{
return $query->where('view_count', '>=', $minViews);
}
public function scopeSearch($query, string $term)
{
return $query->whereFullText(['title', 'body'], $term);
}
// ── Relationships ──
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
public function comments(): HasMany
{
return $this->hasMany(Comment::class)->orderByDesc('created_at');
}
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class)->withTimestamps();
}
// ── Accessors & Mutators ──
public function getReadingTimeAttribute(): int
{
return (int) ceil(str_word_count($this->body) / 200);
}
public function setSlugAttribute(string $value): void
{
$this->attributes['slug'] = str_slug($value);
}
// ── Events ──
protected static function booted(): void
{
static::creating(function (self $post) {
if (empty($post->slug)) {
$post->slug = str_slug($post->title);
}
});
static::saved(function (self $post) {
Cache::forget("post-{$post->id}");
});
}
}<?php
use App\Models\Post;
// ── Basic Queries ──
Post::all();
Post::find(1);
Post::findOrFail(1);
Post::firstWhere('slug', 'hello-world');
Post::firstOrFail();
// ── Where Clauses ──
Post::where('status', 'published')->get();
Post::where('view_count', '>=', 100)->get();
Post::whereBetween('view_count', [50, 200])->get();
Post::whereIn('status', ['published', 'archived'])->get();
Post::whereNull('deleted_at')->get();
Post::whereNotNull('published_at')->get();
Post::whereDate('created_at', '2024-01-15')->get();
Post::whereMonth('created_at', 1)->get();
// ── Ordering, Limiting ──
Post::orderByDesc('created_at')->limit(10)->get();
Post::latest()->first(); // shorthand for orderByDesc('created_at')
Post::oldest('published_at')->first();
Post::inRandomOrder()->first();
// ── Eager Loading (Avoid N+1) ──
Post::with(['author', 'category', 'tags'])->get();
Post::withCount(['comments', 'likes'])->get();
Post::withSum('comments', 'rating')->get();
Post::withExists(['comments as has_comments'])->get();
// ── Lazy Eager Loading ──
$posts = Post::all();
$posts->load('author', 'comments');
// ── Has / WhereHas ──
Post::has('comments', '>=', 3)->get(); // has at least 3 comments
Post::whereHas('author', fn ($q) => // where author is verified
$q->where('is_verified', true)
)->get();
Post::doesntHave('comments')->get(); // no comments
// ── Create / Update ──
Post::create(['title' => 'New', 'body' => 'Content', 'author_id' => 1]);
$post->update(['title' => 'Updated']);
Post::upsert([
['id' => 1, 'title' => 'Updated 1'],
['id' => 2, 'title' => 'Updated 2'],
], ['id'], ['title']);
// ── Chunking ──
Post::chunk(200, function ($posts) {
foreach ($posts as $post) {
// Process batch
}
});
// ── Aggregates ──
Post::count();
Post::avg('view_count');
Post::sum('view_count');
Post::max('created_at');
Post::where('category_id', 1)->count();| Relation | Method | Inverse |
|---|---|---|
| One-to-Many | hasMany() | belongsTo() |
| One-to-One | hasOne() | belongsTo() |
| Many-to-Many | belongsToMany() | belongsToMany() |
| Has Many Through | hasManyThrough() | hasMany() |
| Morph Many | morphMany() | morphTo() |
| Morph One | morphOne() | morphTo() |
| Morph To Many | morphToMany() | morphedByMany() |
| Method | Purpose |
|---|---|
| filter($callback) | Filter items by callback |
| map($callback) | Transform each item |
| pluck($key) | Get array of single column values |
| unique() | Remove duplicates |
| groupBy($key) | Group by key |
| sortBy($key) | Sort ascending |
| sortByDesc($key) | Sort descending |
| take($n) | Get first N items |
| values() | Reset numeric keys |
| toArray() | Convert to plain PHP array |
Post::with(['author', 'comments']) when accessing relationships in loops. Use Laravel Telescope or Clockwork in development to detect N+1 queries.{{-- ── Layout (Master Template) ── --}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', config('app.name'))</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
<header>
<nav>
<a href="{{ route('home') }}">Home</a>
@guest
<a href="{{ route('login') }}">Login</a>
<a href="{{ route('register') }}">Register</a>
@endguest
@auth
<a href="{{ route('dashboard') }}">Dashboard</a>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit">Logout</button>
</form>
@endauth
</nav>
</header>
<main>
@if(session('success'))
<div class="alert alert-success">
{{ session('success') }}
</div>
@endif
@yield('content')
</main>
@stack('scripts')
</body>
</html>{{-- ── Child Template ── --}}
@extends('layouts.app')
@section('title', 'All Posts')
@section('content')
<div class="container">
<div class="flex justify-between items-center mb-6">
<h1>Posts</h1>
<a href="{{ route('posts.create') }}" class="btn btn-primary">
New Post
</a>
</div>
<div class="grid gap-4">
@foreach($posts as $post)
<article class="card p-4">
@if($post->cover_image)
<img src="{{ Storage::url($post->cover_image) }}"
alt="{{ $post->title }}" class="rounded-lg mb-4">
@endif
<h2>
<a href="{{ route('posts.show', $post) }}">
{{ Str::limit($post->title, 80) }}
</a>
</h2>
<div class="meta text-sm text-gray-500">
<span>By {{ $post->author->name }}</span>
<span>·</span>
<span>{{ $post->created_at->diffForHumans() }}</span>
<span>·</span>
<span>{{ number_format($post->view_count) }} views</span>
</div>
<p class="mt-2">{{ Str::words($post->body, 40) }}</p>
<div class="flex gap-2 mt-3">
@foreach($post->tags as $tag)
<span class="badge">{{ $tag->name }}</span>
@endforeach
</div>
</article>
@endforeach
</div>
<div class="mt-8">
{{ $posts->withQueryString()->links() }}
</div>
</div>
@endsection
@push('scripts')
<script src="/js/posts.js"></script>
@endpush| Directive | Purpose |
|---|---|
| @if / @elseif / @else | Conditional rendering |
| @unless / @endunless | Inverse conditional |
| @isset($var) | Check if variable is defined and not null |
| @empty($var) | Check if variable is empty |
| @auth / @guest | Check authentication |
| @for / @foreach / @forelse | Loops |
| @while | While loop |
| @section / @yield | Template inheritance |
| @include | Include subview |
| @each("view", $items, "item") | Loop with partial |
| @csrf | CSRF token field |
| @method("PUT") | HTTP method spoofing |
| Component | Usage | Purpose |
|---|---|---|
| x-alert | <x-alert type="success" /> | Alert component |
| x-slot | <x-slot:name> | Named slot |
| @props | @props(['type' => 'info']) | Component props |
| $attributes | Inherited HTML attributes | Attribute merging |
| <x-form> | <x-form method="POST"> | Form wrapper |
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserIsAdmin
{
public function handle(Request $request, Closure $next): Response
{
if (! $request->user() || ! $request->user()->is_admin) {
abort(403, 'Unauthorized access.');
}
return $next($request);
}
}
// ── Register middleware (bootstrap/app.php - Laravel 11+) ──
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'admin' => EnsureUserIsAdmin::class,
'throttle.api' => ThrottleRequests::class.':60,1',
]);
});<?php
// ── Built-in Auth Routes ──
// routes/auth.php (Laravel Breeze / Jetstream)
Route::middleware('guest')->group(function () {
Route::get('register', [RegisteredUserController::class, 'create']);
Route::post('register', [RegisteredUserController::class, 'store']);
Route::get('login', [AuthenticatedSessionController::class, 'create']);
Route::post('login', [AuthenticatedSessionController::class, 'store']);
});
Route::middleware('auth')->group(function () {
Route::post('logout', [AuthenticatedSessionController::class, 'destroy']);
});
// ── Manual Authentication ──
use Illuminate\Support\Facades\Auth;
// Login
if (Auth::attempt(['email' => $request->email, 'password' => $request->password])) {
$request->session()->regenerate();
return redirect()->intended('/dashboard');
}
// Check auth
Auth::check(); // Is logged in?
Auth::user(); // Current user
Auth::id(); // Current user ID
auth()->user()->name; // Shorthand
$request->user()->can('edit-post', $post); // Authorization
// Logout
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
// ── Gates (Authorization) ──
// App\Providers\AuthServiceProvider
Gate::define('update-post', function (User $user, Post $post) {
return $user->id === $post->author_id || $user->is_admin;
});
Gate::define('admin-access', function (User $user) {
return $user->is_admin;
});
// Usage
if (Gate::allows('update-post', $post)) { }
if (Gate::denies('admin-access')) { abort(403); }
// Policy
// php artisan make:policy PostPolicy --model=Post
class PostPolicy {
public function update(User $user, Post $post): bool {
return $user->id === $post->author_id;
}
public function delete(User $user, Post $post): bool {
return $user->id === $post->author_id || $user->is_admin;
}
}
// Usage: $user->can('update', $post) or $this->authorize('update', $post){ csrf_field() } or @csrfgenerates a hidden token field. For API routes using Sanctum, use @csrfor include the token in headers. For SPAs, use Sanctum's built-in SPA authentication with CSRF cookies.# ── Artisan Commands ──
php artisan serve # Start dev server (port 8000)
php artisan serve --port=3000 # Custom port
php artisan make:model Post -mcr # Model + migration + controller + resource
php artisan make:migration create_posts_table
php artisan make:controller PostController --resource
php artisan make:request StorePostRequest
php artisan make:resource PostResource
php artisan make:policy PostPolicy --model=Post
php artisan make:seeder PostSeeder
php artisan make:factory PostFactory
php artisan make:command SyncPosts
php artisan make:middleware EnsureTokenIsValid
php artisan migrate # Run migrations
php artisan migrate:fresh # Drop all tables and re-migrate
php artisan migrate:fresh --seed # Fresh + seed
php artisan migrate:rollback # Rollback last batch
php artisan migrate:rollback --step=3 # Rollback N steps
php artisan migrate:status # Show migration status
php artisan db:seed # Run seeders
php artisan db:seed --class=PostSeeder
php artisan route:list # List all routes
php artisan route:list --path=post # Filter routes
php artisan cache:clear # Clear application cache
php artisan config:clear # Clear config cache
php artisan view:clear # Clear compiled views
php artisan route:clear # Clear route cache
php artisan optimize:clear # Clear all cached files
php artisan test # Run PHPUnit tests
php artisan test --filter PostModel # Run specific test<?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;
public function test_guest_can_view_posts(): void
{
Post::factory()->count(5)->create(['status' => 'published']);
$response = $this->get('/posts');
$response->assertStatus(200);
$response->assertViewHas('posts');
$response->assertViewCount('posts', 5);
}
public function test_guest_cannot_create_post(): void
{
$response = $this->post('/posts', [
'title' => 'Test Post',
'body' => 'Content of the test post.',
]);
$response->assertRedirect('/login');
}
public function test_auth_user_can_create_post(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/posts', [
'title' => 'New Post Title',
'body' => 'This is the post body with enough content.',
'category_id' => 1,
]);
$response->assertRedirect();
$this->assertDatabaseHas('posts', [
'title' => 'New Post Title',
'author_id' => $user->id,
]);
}
public function test_post_requires_valid_title(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/posts', [
'title' => '',
'body' => 'Valid body content here.',
]);
$response->assertSessionHasErrors(['title']);
}
}| Method | Purpose |
|---|---|
| assertStatus($code) | Assert HTTP status code |
| assertRedirect($uri) | Assert redirect response |
| assertViewHas($key) | Assert view has variable |
| assertSee($text) | Assert text in response |
| assertDontSee($text) | Assert text not in response |
| assertDatabaseHas($table, $data) | Record exists in DB |
| assertDatabaseMissing($table, $data) | Record does not exist |
| assertSessionHas($key) | Flash data exists |
| assertSessionHasErrors($keys) | Validation errors exist |
| assertAuthenticated() | User is authenticated |
| assertGuest() | User is a guest |
| Method | Purpose |
|---|---|
| Post::factory()->create() | Create and persist |
| Post::factory()->make() | Create without persisting |
| Post::factory()->count(5)->create() | Create N records |
| Post::factory()->create([...]) | Override attributes |
| Post::factory()->published()->create() | Use custom state |
| User::factory()->create()->first() | Create single user |
Facades provide a static-like interface to classes bound in the service container (e.g., DB::table(),Cache::get()). Under the hood, they resolve the underlying class from the container. Dependency Injection (DI) passes dependencies through constructor parameters, which is more explicit, easier to test, and preferred for complex classes. Facades are convenient but can make testing harder since the dependencies are hidden.
N+1 occurs when you query N parent records and then query their relations in a loop (N+1 queries). Laravel solves this with eager loading using Post::with(['author', 'comments']), which joins the relationships in 2-3 queries instead of N+1. Use ::with() proactively,load() for lazy loading after the initial query, and Laravel Telescope to detect N+1 issues.
Middleware provides a filtering mechanism around HTTP requests entering your application. Each middleware performs a check (auth, CSRF, rate limiting) before passing the request to the next middleware or controller. Global middleware runs on every request; route middleware is applied to specific routes or groups. Middleware runs in the order defined in bootstrap/app.php (Laravel 11). Terminable middleware runs after the response has been sent to the browser.