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
$fillableand 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.