PHP Framework

Laravel

Laravel is the PHP framework built for developer happiness: expressive routing, the Eloquent ORM, Blade templates and an ecosystem that covers everything from auth to deployment.

intermediate16 min readUpdated Sep 16, 2026
routes/web.php
php
<?php
// routes/web.php

use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;

Route::get('/posts', [PostController::class, 'index']);
Route::get('/posts/{post}', [PostController::class, 'show']);

Route::middleware('auth:sanctum')->group(function () {
    Route::post('/posts', [PostController::class, 'store']);
    Route::put('/posts/{post}', [PostController::class, 'update']);
});
Released
2011
Creator
Taylor Otwell
Language
PHP
Architecture
MVC
ORM
Eloquent
CLI
Artisan
Templating
Blade

Why it matters

Why Laravel developers are so productive

Batteries included

Authentication, authorisation, queues, mail, notifications, caching, file storage and scheduling ship with the framework. You spend your time on the product instead of choosing and wiring packages.

Eloquent makes data pleasant

Models map to tables, relationships read like sentences, and migrations, factories and seeders live beside the code they describe.

Artisan and a deep ecosystem

The artisan command line generates the repetitive parts, while Forge, Vapor, Nova and Livewire cover deployment, serverless, admin panels and reactive UI.

The big picture

Eloquent, Artisan, ecosystem

Models own the database, the command line generates the boring parts, and a deep ecosystem covers the rest.

Eloquent

Model

An active-record ORM where each model owns a table, its relationships and its query shortcuts.

Routing & controllers

Handle

web.php and api.php declare endpoints, route model binding injects records, and controllers keep handlers thin.

Migrations

Evolve

Schema changes are versioned PHP files, so every environment builds the same database from the same history.

At a glance

The Laravel toolbox

Routing

Declarative endpoints in web.php and api.php, with resource routes and route model binding.

Middleware

HTTP filters for authentication, throttling and CORS, configured in bootstrap/app.php.

Eloquent

Models, relationships, scopes and an expressive query builder.

Migrations

Versioned schema changes, plus factories and seeders for realistic test data.

Blade

A templating engine with layouts, components and escaped output by default.

Artisan

Generate code, run migrations, dispatch queues and schedule background tasks.

A short history

From a solo project to a full platform

  1. 2011

    Laravel 1

    Taylor Otwell releases a framework designed to make PHP development enjoyable again.

    11
  2. 2013

    Laravel 4

    The framework is rewritten around Composer and the reusable Illuminate components.

    13
  3. 2015

    Laravel 5

    A clearer directory structure, better Artisan commands and a growing ecosystem take shape.

    15
  4. 2020

    Laravel 8

    Model factories, job batching and Jetstream modernise the application stack.

    20
  5. 2024

    Laravel 11

    A slimmer skeleton moves middleware and exception configuration into bootstrap/app.php.

    24
  6. Today

    A full platform

    Forge, Vapor, Nova, Livewire and Sanctum cover deployment, admin panels and authentication.

    Today

The complete guide

Laravel: Everything you need to know

What is Laravel?

Laravel is a full-stack PHP framework built around a simple promise: make web development enjoyable without sacrificing power. It follows the MVC pattern, but the architecture matters less than the feeling of using it. Routing is expressive, the database layer reads like English, and the pieces you would otherwise assemble from a dozen libraries are already there.

It was created by Taylor Otwell in 2011 and has since become the most popular PHP framework, with an ecosystem that reaches well beyond the framework itself: Forge and Vapor for deployment, Nova for admin panels, Livewire for reactive interfaces, and Sanctum for authentication.

If you have written PHP by hand, Laravel feels like someone finally organised the toolbox.

Routing and controllers

Every request enters through a route. Web routes live in routes/web.php and API routes in routes/api.php, and both use the same expressive syntax.

<?php

use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;

Route::get('/posts', [PostController::class, 'index']);
Route::get('/posts/{post}', [PostController::class, 'show']);
Route::post('/posts', [PostController::class, 'store']);

Route::apiResource('posts', PostController::class);

Route::apiResource generates the conventional set of index, store, show, update and destroy routes in one line. A controller then holds one method per action, keeping each handler small.

class PostController extends Controller
{
    public function index()
    {
        return Post::latest()->paginate(20);
    }

    public function store(StorePostRequest $request)
    {
        $post = $request->user()->posts()->create($request->validated());

        return response()->json($post, 201);
    }
}

Route model binding

Laravel can turn a route parameter into a model instance automatically. Name the parameter after the model and type-hint it in the controller, and Laravel fetches the record or returns a 404.

Route::get('/posts/{post}', [PostController::class, 'show']);

public function show(Post $post)
{
    return $post->load('author');
}

By default the model’s primary key is used. Add getRouteKeyName() to bind on a slug instead, which is how you get clean URLs like /posts/hello-world without extra lookup code.

Eloquent: the ORM people stay for

Eloquent is Laravel’s active-record ORM. A model maps to a table, and relationships are declared as methods.

class Post extends Model
{
    protected $fillable = ['title', 'slug', 'body'];

    public function author(): BelongsTo
    {
        return $this->belongsTo(User::class, 'user_id');
    }

    public function scopePublished($query)
    {
        return $query->whereNotNull('published_at');
    }
}

Relationships include hasMany, belongsTo, belongsToMany, hasManyThrough and polymorphic variants. The biggest practical rule is to eager load relationships you know you will use:

$posts = Post::with('author')->published()->latest()->get();

Without with('author'), accessing $post->author inside a loop issues a query per row — the classic N+1 problem. Eager loading turns that into two queries no matter how many posts you fetch.

Migrations, factories and seeders

Schema changes are versioned PHP files, so every environment is built from the same history.

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->timestamps();
});

Run them with php artisan migrate. Factories describe how to fake a model, and seeders populate a database with known data.

Post::factory()->count(50)->for(User::factory())->create();

Together, factories and seeders make tests realistic without hand-written fixtures.

Blade: templating without surprises

Blade compiles to plain PHP and stays out of your way. Output is escaped by default, and layouts and components keep markup DRY.

{{-- resources/views/posts/show.blade.php --}}
@extends('layouts.app')

@section('content')
    <article>
        <h1>{{ $post->title }}</h1>
        <p>By {{ $post->author->name }}</p>
        {!! $post->body !!}
    </article>
@endsection

{{ }} escapes, {!! !!} does not. Use the escaped form everywhere unless you have sanitised the content yourself, and prefer components like <x-post-card :post="$post" /> for reusable UI.

Validation and form requests

Validation is a first-class feature. For anything beyond a one-off, move the rules into a form request.

php artisan make:request StorePostRequest
class StorePostRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user() !== null;
    }

    public function rules(): array
    {
        return [
            'title' => ['required', 'string', 'max:255'],
            'body' => ['required', 'string'],
            'published_at' => ['nullable', 'date'],
        ];
    }
}

Type-hint the request in the controller and Laravel validates before your code runs, returning a 422 with errors automatically. Then use $request->validated() so only known fields reach the model.

Middleware and the request lifecycle

Middleware filter requests before they reach a route. Recent Laravel versions configure them in bootstrap/app.php.

->withMiddleware(function (Middleware $middleware) {
    $middleware->api(prepend: [
        \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
    ]);

    $middleware->alias([
        'admin' => \App\Http\Middleware\EnsureUserIsAdmin::class,
    ]);
})

A request passes through global middleware, then route middleware such as auth, then the controller. Understanding that order explains most “why is this value missing” questions.

The service container and facades

The service container resolves dependencies for you. Type-hint a class in a constructor or controller and Laravel builds it, injecting its own dependencies recursively.

class PostController extends Controller
{
    public function __construct(private PostRepository $posts) {}
}

Facades like Cache::get() and DB::table() offer a static-looking shorthand to services in the container. They are convenient in controllers, but constructor injection keeps classes testable and dependencies explicit. Prefer injection in services; use facades where they genuinely read better.

Artisan, queues and events

Artisan is the command line that generates and runs the app.

php artisan make:model Post -mfc   # model, migration, factory, controller
php artisan migrate
php artisan queue:work
php artisan schedule:run

Anything slow — sending mail, calling a third-party API, generating a report — belongs in a queued job.

class SendPostNotification implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable;

    public function __construct(public Post $post) {}

    public function handle(): void
    {
        // runs on a worker, not in the request
    }
}

SendPostNotification::dispatch($post);

Events and listeners follow the same pattern, and listeners can implement ShouldQueue themselves, so the request returns immediately.

Authentication with Sanctum

For SPAs and token-based APIs, Sanctum is the default choice. It issues personal access tokens and also supports cookie-based sessions for first-party frontends.

$token = $user->createToken('mobile')->plainTextToken;

Protect routes with the middleware alias:

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
    return $request->user();
});

Passport remains the option when you need a full OAuth2 server with clients, scopes and grants.

Testing with Pest and PHPUnit

Laravel ships with a testing setup that boots the framework and can reset the database per test using the RefreshDatabase trait.

use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

test('it creates a post', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
        ->postJson('/api/posts', ['title' => 'Hello', 'body' => 'World'])
        ->assertCreated();
});

Pest adds a readable function-based syntax on top of PHPUnit. Either way, HTTP tests exercise routing, middleware, validation and the database together, which is where most Laravel bugs actually live.

Best practices

  • Keep controllers thin; move business logic into services and actions.
  • Use form requests for validation and only persist $request->validated().
  • Eager load relationships with with() to avoid N+1 queries.
  • Version the schema with migrations and generate data with factories.
  • Prefer constructor injection over facades in services you want to test.
  • Queue anything slow and keep requests fast.
  • Write feature tests that exercise the HTTP layer and a real database.

Common mistakes

  • Calling Post::create($request->all()) and exposing columns to mass assignment.
  • Lazy loading relationships in a loop and hammering the database.
  • Putting queries, validation and business rules in one giant controller method.
  • Forgetting $fillable and wondering why attributes are silently dropped.
  • Running heavy work synchronously instead of dispatching a job.
  • Editing an old migration after it has run in production.
  • Assuming {{ }} and {!! !!} are interchangeable.

Where to go next

Laravel teaches a productive, opinionated way to build web applications, and much of its foundation comes from Symfony components, so that guide is a natural companion. If you are coming from the JavaScript world, compare the mindset with Express. To design the API your routes expose, read REST and then OpenAPI for documentation.

In practice

From route to database

Four files that describe one feature end to end: endpoint, controller, model and schema.

routes/web.php
<?php

use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;

Route::get('/posts', [PostController::class, 'index']);
Route::get('/posts/{post}', [PostController::class, 'show']);

Route::middleware('auth:sanctum')->group(function () {
    Route::post('/posts', [PostController::class, 'store']);
    Route::put('/posts/{post}', [PostController::class, 'update']);
});

Route model binding

Let the router resolve the record and 404 automatically. Manual lookups scatter findOrFail calls and error handling through every controller.

Prefer
Route::get('/posts/{post}', [PostController::class, 'show']);

public function show(Post $post)
{
    return $post;
}
Avoid
Route::get('/posts/{id}', function ($id) {
    $post = Post::findOrFail($id);
    return $post;
});

Filling a model

Only validated fields should reach the database. Passing the whole request into create lets clients set columns you never meant to expose.

Prefer
$post = $request->user()
    ->posts()
    ->create($request->validated());
Avoid
$post = Post::create($request->all());
// is_admin, user_id and more can be set by the client

Trade-offs

Is Laravel the right PHP framework?

Laravel optimises for developer happiness and speed of delivery. Know where that convenience costs you.

Strengths

  • You ship features, not plumbing

    Auth, queues, mail, storage, notifications and scheduling are first-party. A feature that takes a week in a bare stack often takes a day here.

  • Eloquent and migrations are excellent

    Relationships, eager loading, factories and versioned schema changes make data work readable and repeatable across every environment.

  • The ecosystem is unmatched in PHP

    First-party tools cover hosting, serverless, admin panels, payments and reactive UI, and they share the framework's conventions.

Trade-offs

  • Magic can hide expensive queries

    Lazy-loaded relationships cause N+1 queries if you do not eager load. Facades and the container make dependencies implicit unless you are deliberate.

  • It is heavy for tiny services

    A full Laravel install is a lot of framework for a small internal API. For a single endpoint, a micro-framework may be the better fit.

  • Yearly majors need attention

    Laravel ships a major release most years. Upgrades are usually smooth, but they are recurring work you must schedule rather than ignore.

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Laravel?

Our interactive tutorial walks you through Laravel step by step — with quizzes and real code you can run in the browser.