Laravel Roadmap
Laravel is a PHP framework for building web applications and APIs. It gives you routing, a database layer, templates, authentication, queues, mail, scheduling and testing in one coherent package. Agencies, start-ups and large product teams use it, and it opens many back-end and full-stack jobs in the PHP world.
This roadmap takes you from installing the tools to deploying a tested application. Each stage builds on the one before: first the project and request flow, then routing and views, then data, security, background work, testing and operations. The topics are written for current Laravel releases, and where older projects differ, such as the Kernel classes removed from new skeletons, the text says so.
By the end you should be able to build a CRUD application with authentication and authorisation, design relationships without N+1 queries, move slow work into queues, write feature tests and deploy with sensible production settings. You should also be able to answer the questions interviewers ask about the framework.
- Level
- Beginner to advanced
- Time
- 10–14 weeks at about 8 hours a week, less if you already know modern PHP well
- Scope
- 65 topics in 9 stages
- Before you start
- PHP Roadmap fundamentals: classes, namespaces, interfaces, traits, type declarations and Composer basics; Basic SQL Roadmap: tables, joins and indexes; Basic HTML Roadmap and CSS Roadmap; Comfort with the terminal and Git and GitHub Roadmap
What you will be able to do
- Create a Laravel project, understand its folders and trace a request through routing, middleware and a controller
- Build pages with Blade layouts and components and choose a front-end approach
- Validate input and handle errors, logging and debugging methodically
- Model data with migrations and Eloquent, and avoid N+1 queries
- Implement authentication, tokens and authorisation with gates and policies
- Use queues, events, scheduling, notifications and file storage
- Write feature tests and keep style consistent with automated tools
- Deploy with correct production configuration and caching
The roadmap at a glance
- Stage 1Foundations: PHP, the framework and your first projectBeginner · 1–2 weeks · 7 topics
- Stage 2Routing, controllers and middlewareBeginner · 2 weeks · 10 topics
- Stage 3Requests, responses and viewsBeginner · 2 weeks · 10 topics
- Stage 4Validation, errors and debuggingIntermediate · 1–2 weeks · 6 topics
- Stage 5Databases and EloquentIntermediate · 3 weeks · 8 topics
- Stage 6Authentication and authorisationIntermediate · 2–3 weeks · 8 topics
- Stage 7Application architecture and background workIntermediate · 3 weeks · 8 topics
- Stage 8Testing and code qualityIntermediate · 2 weeks · 4 topics
- Stage 9Deployment and operationsAdvanced · 2 weeks · 4 topics
Stage 1 Foundations: PHP, the framework and your first project
Goal. Install Laravel, start a project, understand where files live and how a request becomes a response.
Web Frameworks Core
A web framework supplies the repeating parts of a web application so you can write only what is unique: routing, request handling, templates, database access, authentication, validation and testing. Without one, every project invents its own folder layout and security habits.
Laravel is a full-stack PHP framework. It follows the model-view-controller idea but also bundles queues, mail, scheduling, caching and a command-line tool. It is opinionated, which means a new developer can join a Laravel team and find things in expected places. Know the PHP Roadmap basics first: classes, namespaces, interfaces, traits and type declarations.
Practice. List five tasks every web app repeats and say which Laravel feature you expect to handle each.
Installing Laravel Core
You need PHP (a recent supported version), Composer and a database. The usual route is the Laravel installer or Composer's create-project command. Use a supported PHP version, because older ones no longer receive security fixes.
composer global require laravel/installer
laravel new shop
cd shop
php artisan serveThe installer asks about a starter kit, testing framework and database. New projects default to SQLite, which needs no server and suits learning. Visit the local address printed by artisan serve and you should see the welcome page.
Practice. Install PHP and Composer, create a project named shop and open its welcome page.
Laravel Herd Recommended
Herd is a native development environment for macOS and Windows. It bundles PHP, a web server and local domains, so you can serve a project at a name such as shop.test without configuring Nginx or Docker. You can switch PHP versions per site, and paid tiers add services such as databases and mail catching.
It is the quickest way to start on those systems. Teams on other platforms, or who want a production-like setup, often choose Sail or their own containers instead. Pick one environment, learn it, and avoid mixing several on one machine.
Sail is the alternative: a Docker Compose wrapper started with sail up. Creating a new Laravel project with either gives the same skeleton. Both are optional.
Practice. Park your projects folder in Herd or run artisan serve, and open two projects under different local domains.
Package Management Core
PHP dependencies are managed by Composer. The composer.json file declares what you need, and composer.lock records exact versions for repeatable installs. Commit both. Use composer require to add a package, composer update to upgrade within allowed ranges and composer install on servers and in CI.
The vendor folder holds installed code and should never be edited or committed. Laravel discovers many packages automatically, registering their providers. Check a package's maintenance, licence and downloads before depending on it, and use composer audit to see known security advisories. Front-end assets are handled separately with npm and Vite.
Practice. Add a small package with Composer, read its provider or readme, then remove it and check the lock file changes.
Configuration in Laravel Core
Settings live in the config directory as PHP files that return arrays. Values that differ between environments, such as database credentials and API keys, come from the .env file through the env() helper, which you should call only inside config files. Everywhere else, read settings with config('app.name').
// config/services.php
'payments' => ['key' => env('PAYMENTS_KEY')],
// anywhere else
$key = config('services.payments.key');In production you cache the config for speed, and after caching env() outside config files returns null. That is the most common configuration bug.
Practice. Add a custom config value, read it from a route, then cache the config and confirm it still works.
Project structure: the main directories Core
The App Directory holds your application code: models, controllers, jobs, policies and providers. The Public Directory is the web root, containing index.php and compiled assets, and is the only folder the web server should expose. The Resources Directory contains views, raw CSS and JavaScript, and language files. The Storage Directory in Laravel holds logs, cached files, sessions and uploaded files. The Vendor Directory is for Composer packages. The Database Directory in Laravel Projects contains migrations, seeders and factories.
Other folders: routes for route files, config, tests and bootstrap. Recent versions have a slimmer skeleton than older ones, with fewer files by default and middleware configured in bootstrap/app.php.
Practice. Find where a request enters the app and trace it from public/index.php to your route file.
Request–Response Flow Core
Every request goes through public/index.php, which boots the application and hands the request to the HTTP layer. Global middleware runs, the router matches a route, route middleware runs, then a controller method or closure produces a response. The response travels back out through the middleware stack and is sent to the browser.
Along the way, the service container builds the classes you ask for, and service providers register and boot framework services. Knowing this order explains many bugs: why session data is missing on a route (the session middleware did not run), why a redirect loops, or why an exception handler produced a given page.
Practice. Add temporary logging to a middleware and a controller and confirm the order they run in.
Stage 2 Routing, controllers and middleware
Goal. Define routes cleanly, organise logic in controllers and apply middleware, limits and cross-origin rules.
Routes Core
Routes map a URL and an HTTP method to code. Web routes live in routes/web.php and get sessions, cookies and CSRF protection. API routes, installed with php artisan install:api in recent versions, live in routes/api.php, are stateless and have an /api prefix.
List them with php artisan route:list, which shows methods, URIs, names and middleware. Keep route files thin: they should declare what exists, and controllers should hold the logic. The framework checks routes in order, so a broad pattern placed early can swallow a specific one placed later.
Practice. Run route:list in a fresh project and explain every row.
Basic Routes Core
The simplest route takes a URI and a closure. Methods exist for each HTTP verb: get, post, put, patch, delete. Use match or any for several verbs, though explicit routes are clearer.
Route::get('/hello', fn () => 'Hello');
Route::post('/orders', [OrderController::class, 'store']);HTML forms can only send GET and POST, so use a hidden _method field through the @method('PUT') Blade directive for the others. Add the @csrf directive in every form that changes state, or the request will be rejected with a 419 status.
Practice. Create GET and POST routes for a contact form and submit it from a Blade view with the CSRF token.
Route Parameters Core
Segments in braces capture values and pass them to the closure or controller in order. Optional parameters end in a question mark and need a default. Constraints restrict what matches.
Route::get('/posts/{post}', [PostController::class, 'show'])
->whereNumber('post');
Route::get('/users/{name?}', fn (?string $name = 'guest') => $name);Use constraints so malformed values fail with a 404 early. Parameters are matched by position, not by name, in closures; with route model binding they are matched by type and name. Do not trust parameters: they are user input and must be validated like any other.
Practice. Write a route with an optional parameter and a numeric constraint and test good and bad URLs.
Named Routes Core
Naming a route lets you generate its URL without hard-coding paths, so changing a path later does not break links.
Route::get('/orders/{order}', [OrderController::class, 'show'])
->name('orders.show');
$url = route('orders.show', ['order' => 12]);Use dotted names that follow the resource and action, such as orders.index. Named routes work in redirects (to_route), in Blade (route()) and in tests. Extra parameters become the query string. It is a good habit to name every route you link to.
Practice. Name your routes and replace every hard-coded URL in your views with route().
Route Groups Core
Groups share attributes among several routes, such as middleware, a prefix, a name prefix, a controller or a domain. They keep route files tidy and make permissions harder to forget.
Route::middleware('auth')->prefix('admin')->name('admin.')
->group(function () {
Route::get('/users', [UserController::class, 'index'])
->name('users.index');
});Groups can nest, and attributes combine. Group by access level first: public, authenticated, admin. Avoid deeply nested groups that readers cannot follow.
Route caching with route:cache speeds up production; view routes and redirect routes avoid controllers for static pages. Cross-Origin Resource Sharing (CORS) is configured in config/cors.php, and a health route is registered at /up by default.
Practice. Group all admin routes under a prefix, a name prefix and the auth middleware.
Route Model Binding Core
Route model binding turns an identifier in the URL into a model instance. Type-hint the model in the controller with a parameter name matching the route segment, and Laravel fetches it or returns a 404.
Route::get('/posts/{post}', [PostController::class, 'show']);
public function show(Post $post)
{
return view('posts.show', compact('post'));
}By default it matches on the primary key. Use {post:slug} to bind by another column, and scoped bindings for nested resources so a comment is found only within its post. Remember that binding does not check permissions; authorise separately.
Practice. Bind a post by slug instead of ID and add a nested comment route using scoped bindings.
Basic Controllers Core
Controllers group the request-handling code for a feature. Create one with php artisan make:controller. Each public method handles a route, receives dependencies through type-hints and returns a response.
class PostController extends Controller
{
public function index()
{
return view('posts.index', ['posts' => Post::latest()->get()]);
}
}Keep controllers thin: validate, call a model or service, return a response. When a controller grows, move business logic into action classes or services so it can be tested and reused.
Practice. Move the logic out of a route closure into a controller method and keep the controller under 10 lines per method.
Resource Controllers Core
A resource controller covers standard create, read, update and delete actions: index, create, store, show, edit, update and destroy. One line registers all routes with conventional names.
Route::resource('posts', PostController::class);
Route::apiResource('posts', PostController::class);Generate the controller with make:controller PostController --resource --model=Post. The apiResource variant omits create and edit, which are only for HTML forms. Use only or except to limit actions. Following the convention makes URLs predictable and helps other developers navigate.
Single-action controllers use one __invoke method and suit one-off actions.
Practice. Build a full posts resource and run route:list to match each route to its method.
Middleware Core
Middleware wraps the request: each layer can inspect or change the request, stop it early, or act on the response. Built-in examples handle authentication, sessions, CSRF verification and rate limiting. Create your own with make:middleware.
public function handle(Request $request, Closure $next)
{
if (! $request->user()?->is_admin) {
abort(403);
}
return $next($request);
}Code before $next runs on the way in, code after it on the way out. In recent versions, middleware is registered and aliased in bootstrap/app.php rather than a Kernel class.
Global vs. route middleware: global runs on every request, route middleware only where attached, and in recent versions both are configured in bootstrap/app.php.
Practice. Write a middleware that logs the request path and duration and attach it to a route group.
Rate Limiting Recommended
Rate limiting caps how often a client can call an endpoint, protecting logins and APIs from abuse. The throttle middleware applies named limiters defined in a service provider, which can key on the user, the IP address or any other value.
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
Route::middleware('throttle:api')->group(/* routes */);Exceeded limits return a 429 response with retry headers. The cache store holds the counters, so use a shared one such as Redis in multi-server setups.
Practice. Limit a login route to five attempts a minute and test that the sixth gets a 429.
Stage 3 Requests, responses and views
Goal. Read input, build every kind of response and render pages with Blade.
Retrieving Data and Files Core
The Request object carries query strings, form fields, JSON bodies, headers, cookies and uploaded files. Type-hint it in a controller method to receive it.
$name = $request->input('name', 'guest');
$data = $request->only(['title', 'body']);
if ($request->hasFile('avatar')) {
$path = $request->file('avatar')->store('avatars');
}Never pass $request->all() straight into a model: validate first and use the validated array. Uploaded files expose the original name, size and MIME type, but treat all of those as untrusted.
Practice. Build a profile form that accepts text and an image and stores the image on the default disk.
Creating Responses Core
Returning a string, array or model from a route is enough for simple cases; Laravel converts arrays and models to JSON and strings to HTML. For control, use the response() helper to set status codes, headers and cookies.
return response('Created', 201)
->header('X-Request-Id', $id)
->cookie('seen', '1', 60);Choose correct status codes: 201 for creation, 204 for no content, 404 for missing, 422 for validation failure. Clients and tests depend on these more than on the body text.
Practice. Return three responses with different status codes and check them with a command-line HTTP client.
JSON Responses in Laravel Core
For APIs, return JSON with response()->json() or simply return an array or an Eloquent model, which Laravel serialises for you. Use API resources, covered later, to control the shape of the output instead of exposing columns directly.
return response()->json([
'data' => $order,
'message' => 'Created',
], 201);Be consistent about the envelope, error format and date format across endpoints. Hide sensitive attributes with the model's $hidden property. See API Design Roadmap for design principles.
File Responses in Laravel use download() and file(), and streamed responses suit big exports. Redirects use to_route() and with() for flashed messages.
Practice. Return a JSON list with consistent keys and hide one sensitive attribute.
Views in Laravel Core
Views live in resources/views and are returned with view('posts.index', $data), where dots map to folders. Views contain presentation only; they should not run queries or hold business rules.
return view('posts.show', ['post' => $post]);
// or
return view('posts.show')->with('post', $post);Share data across many views with view composers or View::share, in moderation. Check that a view exists with view()->exists() when names are dynamic.
Practice. Create a views folder structure for posts and render index and show views from a controller.
Displaying Data in Laravel Views Core
Blade prints values with double curly braces, which escape HTML automatically and protect against cross-site scripting. Use the raw syntax with exclamation marks only for content you fully trust and have sanitised.
<h1>{{ $post->title }}</h1>
<p>{{ $post->created_at->diffForHumans() }}</p>Use {{ $value ?? 'default' }} for missing values. When writing a JavaScript framework template inside Blade, prefix the braces with an at sign so Blade skips them. Format dates and money in presenters or accessors, not inline everywhere.
Practice. Show a list of posts with escaped titles and relative dates, and test that an HTML title is displayed as text.
Blade Templating Core
Blade compiles templates into plain PHP and caches them, so it is fast. You can write normal PHP inside it, but directives cover nearly everything. Files end in .blade.php. Comments written as Blade comments never reach the browser.
The main building blocks are directives for conditionals and loops, layouts for shared structure, components for reusable pieces, and slots to fill them. Treat Blade as a way to keep HTML readable; if a template becomes full of logic, move that logic into the controller, a view model or a component class.
Practice. Convert a plain PHP page to Blade and remove all raw PHP tags.
Blade Directives Core
Directives start with an at sign: @if, @foreach, @auth, @guest, @csrf, @method, @include, @error, @can, @vite. They turn into PHP at compile time.
@auth
<p>Hello, {{ auth()->user()->name }}</p>
@endauth
@error('email')
<span>{{ $message }}</span>
@enderrorWrite custom directives with Blade::directive only when a pattern repeats often; components are usually the better tool.
Blade loops use @foreach, @forelse and the $loop variable, and conditional statements in Blade templates use @if, @unless and @isset.
Practice. Use @auth, @guest and @error in a login view.
Layouts in Blade Core
Layouts give pages a shared frame. The modern approach is a layout component: a Blade component that prints its content through {{ $slot }}, used as a tag in pages. The older approach uses @extends, @section and @yield, and you will still see it in many projects.
<x-layout title="Posts">
<h1>All posts</h1>
</x-layout>Use stacks (@push and @stack) to add page-specific scripts to the layout. Keep one layout per major area, such as public and admin.
Practice. Create a layout component with a header, a nav and a slot and use it in three pages.
Components in Laravel Core
Blade components are reusable pieces of interface. Anonymous components are a single Blade file in resources/views/components; class-based components add a PHP class for logic. Props are declared with @props, and extra attributes are merged through $attributes.
{{-- components/alert.blade.php --}}
@props(['type' => 'info'])
<div {{ $attributes->class(['alert', "alert-$type"]) }}>{{ $slot }}</div>
<x-alert type="error">Save failed</x-alert>Use named slots for sections like a card header. Components replace most copied HTML.
Practice. Build an alert component with a type prop and a dismiss slot and use it for three message kinds.
Laravel for Frontend Recommended
Laravel works with several front-end approaches. Blade alone suits server-rendered sites. Livewire adds interactivity written in PHP. Inertia connects Laravel to a Vue or React front end without building a separate API. A full single-page application against an API is also possible. Assets are built with Vite, which ships with new projects.
Choose by team skill and need: if your team knows PHP and wants fast results, Livewire or Blade; if it has strong JavaScript skills, Inertia or an API with React Roadmap or Vue Roadmap. Avoid mixing all approaches in one project.
Inertia links Laravel to Vue or React, and Blade and Livewire integration keeps interactivity in PHP. Bootstrap is an older CSS option; recent kits prefer Tailwind.
Practice. Write a short note on which front-end approach you would pick for a booking system and why.
Stage 4 Validation, errors and debugging
Goal. Validate input, handle failures gracefully and find bugs quickly.
Form Validation Core
Validation checks input before you use it. In a controller call $request->validate() with rules; on failure Laravel redirects back with errors and old input, or returns a 422 JSON response for API requests.
$data = $request->validate([
'title' => ['required', 'string', 'max:120'],
'email' => ['required', 'email', 'unique:users,email'],
'tags' => ['array', 'max:5'],
]);For larger forms, create a Form Request class with make:request, holding rules and authorisation. Use the returned array to create models, never the raw request.
Manual validation uses Validator::make for rows outside requests. Customizing error messages is done through messages() and attributes() in a Form Request.
Practice. Validate a registration form with five rules and show the errors next to each field.
Error Messages in Validation Core
Errors are available in views through the $errors bag, which Laravel shares with every view. Show a message beside a field with @error('field') and restore what the user typed using old('field').
<input name="email" value="{{ old('email') }}">
@error('email')
<p class="text-red-600">{{ $message }}</p>
@enderrorFor APIs, the 422 response includes an errors object keyed by field. Make sure the front end displays them in a consistent place.
Practice. Build a form that preserves input after failed validation and shows an error under each field.
Handling Exceptions Core
Uncaught exceptions are processed by the framework's exception handler, which logs them and renders a response: a debug page locally and a generic error page in production. In recent versions the handler is configured in bootstrap/app.php with withExceptions.
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (PaymentFailed $e, Request $request) {
return response()->json(['message' => 'Payment failed'], 402);
});
})Create custom exception classes for domain failures and let them render themselves. Never leave debug mode on in production, since it exposes secrets.
HTTP exceptions come from abort(404) or abort(403) and use views in resources/views/errors.
Practice. Create a custom exception that renders a friendly JSON response and a custom HTML page.
Logging Basics Core
Laravel logs through Monolog. Write messages with the Log facade or the logger() helper at levels from debug to emergency, and add context arrays.
Log::info('Order created', ['order_id' => $order->id]);
Log::error('Payment failed', ['error' => $e->getMessage()]);By default logs go to storage/logs. Use the right level so important events stand out. Never log passwords, tokens or full card numbers. Structured context makes searching much easier than free text.
Log Stacks and Messages: a stack channel sends one message to several channels, such as daily files and Slack.
Practice. Log an order event with context and find it in the log file.
Debugging Basics Core
Start with what the error says. Locally, the error page shows the stack trace, request data and queries. Use dump() to print a value and keep going, dd() to print and stop, and ray() or a step debugger with Xdebug for harder problems. Check logs, then reproduce with a failing test.
Common causes: a stale cache (clear config, route and view caches), a missing migration, a wrong environment variable, or a mismatched PHP version. Change one thing at a time. Remove debug calls before committing.
Debugbar is a toolbar for local development that shows queries and timings.
Practice. Introduce a bug on purpose, then find it using dump, the log and a failing test.
Telescope Recommended
Telescope is a first-party debugging dashboard that records requests, exceptions, queries, jobs, mail, notifications, cache operations and more, and lets you browse them in the browser. It is useful in local and staging environments to see what happened inside a request or job.
It stores entries in the database, so prune old data on a schedule and restrict access with its gate. Running it in production needs care for performance and privacy, because it records request data. Use Pulse, covered later, for production metrics.
Practice. Install Telescope locally, trigger a failing job and inspect it in the dashboard.
Stage 5 Databases and Eloquent
Goal. Design tables with migrations, query data efficiently and model relationships with Eloquent.
Database Configuration Core
Connections are defined in config/database.php and chosen through .env variables such as DB_CONNECTION, DB_HOST, DB_DATABASE. Laravel supports SQLite, MySQL and MariaDB, PostgreSQL and SQL Server. New projects default to SQLite for ease.
Use separate databases for local, testing and production, and never point tests at real data. You can define several connections and choose one per model. Credentials belong in the environment, not in code. After editing .env, restart queue workers and clear cached config. Know basic SQL Roadmap before going deep into the ORM.
Practice. Switch a project from SQLite to MySQL or PostgreSQL and run the migrations.
Database Migrations and Seeders Core
Migrations are versioned PHP files that describe schema changes, so every environment can reach the same structure. Create them with make:migration, run with migrate, undo with migrate:rollback.
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->timestamps();
});Never edit a migration that has run on a shared environment; add a new one. Seeders and factories fill the database with test or demo data. migrate:fresh --seed rebuilds a local database from scratch.
Practice. Write migrations for users, posts and comments with foreign keys, plus a seeder using factories.
Query Builder Core
The query builder is a fluent interface for building SQL without writing strings. It binds parameters for you, which prevents SQL injection when you use it as intended.
$rows = DB::table('orders')
->where('status', 'paid')
->whereBetween('created_at', [$from, $to])
->groupBy('customer_id')
->selectRaw('customer_id, sum(total) as spent')
->get();It returns plain objects, not models. Use it for reports and complex queries where models add no value. Be careful with raw expressions: bind values with placeholders instead of concatenating user input.
Practice. Write a query that returns the top five customers by spend in a date range.
Eloquent ORM Core
Eloquent is Laravel's active-record ORM: each table has a model class, and each model instance represents a row. Models know their table name by convention (the plural snake case of the class), primary key and timestamps.
class Post extends Model
{
protected $fillable = ['title', 'body'];
}
$posts = Post::where('published', true)->latest()->get();Define $fillable to guard against mass assignment. Eloquent is convenient, but it can hide expensive queries, so check what runs. For heavy data processing, drop to the query builder.
Practice. Create a Post model with factory and migration in one command using make:model -mf.
CRUD Operations in Eloquent ORM Core
Create with Post::create($data) or new Post and save(). Read with find, first, findOrFail and query chains. Update by changing attributes and calling save(), or update(). Delete with delete(), or use soft deletes to keep rows with a timestamp.
$post = Post::create($data);
$post->update(['title' => 'New title']);
$post->delete();
Post::withTrashed()->find($id)->restore();Use firstOrCreate and updateOrCreate for upserts, and transactions via DB::transaction for multi-step writes.
Practice. Implement complete create, edit and delete for posts, with soft deletes and a restore action.
Eloquent Relationships Core
Relationships are methods on a model that return how it links to others: hasOne, hasMany, belongsTo, belongsToMany for pivot tables, plus through and polymorphic relations.
class User extends Model {
public function posts() { return $this->hasMany(Post::class); }
}
$users = User::with('posts')->get(); // eager loadAccessing a relationship in a loop without eager loading runs one query per row, the N+1 problem. Fix it with with() and watch for it with Debugbar. Use withCount for counts, and Model::preventLazyLoading() in development to catch mistakes.
Practice. Model users, posts and tags (many to many) and list each user with a post count using one query.
Eloquent Casts and Accessors Recommended
Casts convert attributes between database and PHP types, such as dates, booleans, arrays, enums and encrypted values. Accessors and mutators change how an attribute is read or written.
protected function casts(): array
{
return ['meta' => 'array', 'status' => OrderStatus::class];
}
protected function fullName(): Attribute
{
return Attribute::get(fn () => "$this->first $this->last");
}Use enum casts for fixed sets of values. Keep accessors cheap and free of queries.
Query scopes name reusable conditions, such as published(), and global scopes apply automatically.
Practice. Cast a JSON column to an array and an integer status to a PHP enum.
Pagination Core
Large lists should be split into pages. Call paginate(15) to get a paginator that knows the total, simplePaginate for next and previous only, or cursorPaginate for large, fast-changing tables.
$posts = Post::latest()->paginate(15);
// view
{{ $posts->links() }}Paginators serialise neatly to JSON for APIs. Offset pagination slows on deep pages and can skip rows when data changes, which is where cursor pagination helps. Keep query strings with withQueryString() when filtering.
Practice. Paginate a filtered list and make the filter survive page changes.
Stage 6 Authentication and authorisation
Goal. Sign users in, protect APIs and decide who may do what.
Authentication in Laravel Core
Authentication proves who a user is. Laravel provides guards, which define how users are identified (session for web, tokens for APIs), and providers, which define where users are fetched from. The auth() helper and Auth facade give access to the current user.
Route::middleware('auth')->get('/dashboard', fn () => view('dashboard'));
$user = auth()->user();Most projects use a starter kit rather than build login flows by hand, since password reset, email verification and two-factor flows have many security details.
Practice. Protect a dashboard route and redirect guests to the login page.
Manual Authentication Recommended
You can authenticate without a starter kit. Auth::attempt checks credentials and starts a session; regenerate the session ID to prevent session fixation, and log out by invalidating it.
if (Auth::attempt($credentials, $remember)) {
$request->session()->regenerate();
return redirect()->intended('/dashboard');
}
Auth::logout();
$request->session()->invalidate();Throttle login attempts and return the same message for unknown users and wrong passwords. Hash passwords with the built-in hasher, never store them in plain text.
Practice. Implement login and logout by hand with rate limiting and a generic error message.
Starter Kits Core
Starter kits scaffold authentication and basic account pages: registration, login, password reset, email verification and profile editing. They give you code in your own project that you can read and change, not a hidden package.
Recent Laravel releases offer official kits for React, Vue and Livewire, and the older kits remain available. Choose the front-end stack you plan to use. Review the generated code before shipping, since it becomes yours to maintain.
Authentication with starter kits works out of the box. Breeze is the minimal option, Jetstream adds teams and two-factor authentication, and Livewire starter kits keep the interface in PHP. Breeze and Jetstream are now in maintenance.
Practice. Create a project with a starter kit and read every file it added.
Sanctum Core
Sanctum offers two things: cookie-based session authentication for your own single-page front end, and simple personal access tokens for mobile apps and third parties.
$token = $user->createToken('mobile', ['orders:read'])->plainTextToken;
Route::middleware('auth:sanctum')->get('/me', fn (Request $r) => $r->user());Tokens carry abilities you can check. Store tokens securely on the client and expire or revoke them. For first-party SPAs on the same domain, prefer the cookie flow with CSRF protection. Install the API scaffolding with install:api in recent versions.
OAuth 2.0 API authentication with Laravel Passport is a full OAuth server, needed only when third parties request access on behalf of users.
Practice. Issue a token with limited abilities and call a protected endpoint with it.
Authorization Core
Authorisation decides what an authenticated user may do. Laravel offers gates, simple closures, and policies, classes that group rules for a model. Check them with can, authorize, the @can Blade directive or the can middleware.
Authentication tells you who; authorisation tells you what. A common bug is checking only that the user is logged in and then loading a record by ID, which lets anyone read anyone else's data. Authorise every access to a specific record, and test negative cases.
Practice. Write a test that proves one user cannot view another user's order.
Authorization Gates Core
A gate is a closure that returns whether a user can perform an action. Define them in a service provider, and call them anywhere.
Gate::define('manage-billing', fn (User $user) => $user->is_owner);
if (Gate::allows('manage-billing')) { /* ... */ }
Gate::authorize('manage-billing'); // throws a 403Gates fit actions not tied to a model, such as viewing an admin area. Use Gate::before for a super-admin shortcut with care. When a gate starts to accumulate model logic, move it into a policy.
Practice. Define a gate for an admin-only page and use it in a route and in a Blade view.
Authorization Policies Core
A policy is a class that holds authorisation rules for one model, with methods such as view, update and delete. Generate one with make:policy PostPolicy --model=Post. Laravel finds policies by naming convention.
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
// controller
$this->authorize('update', $post);Policies keep rules in one place and are easy to unit test. Return a denial with a message when useful, and use viewAny for list pages.
Practice. Create a PostPolicy and test each method for owner and non-owner.
Encryption & Hashing Core
Hashing is one-way and is for passwords: Hash::make and Hash::check use bcrypt or argon with a cost factor. Encryption is two-way and for data you must read back later: Crypt::encryptString uses the application key.
$hash = Hash::make($password);
Hash::check($password, $hash);
$secret = Crypt::encryptString($token);Protect the APP_KEY; losing or rotating it without care makes encrypted data unreadable. Do not encrypt passwords. Use the encrypted cast for sensitive columns. Pair these with HTTPS everywhere.
Practice. Store an API token in an encrypted column and explain why passwords use hashing instead.
Stage 7 Application architecture and background work
Goal. Use the container, events, queues, scheduling, notifications and storage to build features that scale past a single request.
Dependency Injection Core
Dependency injection means a class receives what it needs instead of creating it. Laravel's service container does this automatically: type-hint a class in a controller method, constructor or job, and the container builds it and its own dependencies.
class OrderController extends Controller
{
public function __construct(private PaymentGateway $gateway) {}
}
$this->app->bind(PaymentGateway::class, StripeGateway::class);Bind an interface to an implementation in a service provider, and swap it in tests with a fake. It is the main route to testable code. See Software Design and Architecture Roadmap for the principles.
Practice. Define an interface for a notifier, bind two implementations and swap them in a test.
Facades Core
Facades give a static-looking interface to objects in the container: Cache::get(), Log::info(), Mail::to(). They resolve the real object behind the scenes, so they can be faked in tests, unlike true static calls.
Cache::put('key', $value, 600);
Mail::fake();
Mail::assertSent(WelcomeMail::class);They are short and readable, but they hide a class's dependencies. Many teams use facades in controllers and jobs and constructor injection in domain code. Real-time facades, created by prefixing a namespace, are rare and rarely needed.
Practice. Fake the Mail and Queue facades in a test and assert on what was dispatched.
Artisan Console Core
Artisan is the command-line tool. It generates code (make:model, make:controller), runs migrations, clears caches, starts queue workers and lists routes. php artisan list shows everything, and --help explains a command.
php artisan make:command SendReminders
protected $signature = 'reminders:send {--dry-run}';
public function handle(): int {
$this->info('Sent');
return self::SUCCESS;
}Write your own commands for maintenance and imports. Keep them thin and call the same services your web code uses. artisan tinker opens an interactive shell for experiments.
Practice. Write a command that prunes old records with a --dry-run option and a progress bar.
Events and Listeners Recommended
Events announce that something happened; listeners react. This separates the cause from its side effects, so an order-placing class does not need to know about emails, analytics and stock updates.
OrderPlaced::dispatch($order);
class SendReceipt implements ShouldQueue
{
public function handle(OrderPlaced $event): void { /* ... */ }
}Recent versions discover listeners automatically by their handle method type-hint. Queue slow listeners. Do not overuse events: when flow is hard to follow, a plain method call is clearer.
Practice. Dispatch an OrderPlaced event with two queued listeners and assert both run in a test.
Queues & Jobs Core
Queues move slow work out of the request: sending email, resizing images, calling APIs, generating reports. A job is a class with a handle method, dispatched to a queue, and a worker process picks it up.
class ProcessUpload implements ShouldQueue
{
public $tries = 3;
public $backoff = 30;
public function handle(): void { /* ... */ }
}
ProcessUpload::dispatch($upload);Run workers with queue:work under a process supervisor, and restart them on deployment. Make jobs idempotent, because they can run twice. Configure retries, backoff and the failed jobs table. Use Redis or the database as the driver; Horizon gives a dashboard for Redis queues. See Redis Roadmap.
Practice. Dispatch a job that fails twice then succeeds, and watch retries and the failed jobs table.
Task Scheduling Recommended
The scheduler replaces many cron entries with one: a single cron line runs schedule:run every minute, and your code defines what runs when.
// routes/console.php
Schedule::command('reminders:send')->dailyAt('08:00');
Schedule::job(new PruneSessions)->hourly()->onOneServer();Use withoutOverlapping for long tasks and onOneServer when several servers run the scheduler. Output and failures can be pinged to a monitoring service. Test the commands themselves, not the timing.
Practice. Schedule a daily cleanup command and run schedule:list to confirm it is registered.
Notifications Recommended
A notification is a message sent through one or more channels: mail, database, Slack, SMS and others. One class describes the message for each channel.
class InvoicePaid extends Notification implements ShouldQueue
{
public function via($notifiable) { return ['mail', 'database']; }
public function toMail($notifiable) {
return (new MailMessage)->line('Your invoice was paid.');
}
}
$user->notify(new InvoicePaid($invoice));Queue them so requests stay fast. Let users choose their channels. Test with Notification::fake(). Configure a real mail provider in production and check deliverability settings.
Localization with lang files and trans_choice helps when messages need several languages.
Practice. Send an in-app and an email notification when an invoice is paid and show unread ones in the UI.
File Storage Core
The filesystem layer gives one API over local disks, S3-compatible storage and others. Configure disks in config/filesystems.php and use the Storage facade.
$path = $request->file('doc')->store('docs', 's3');
Storage::disk('s3')->temporaryUrl($path, now()->addMinutes(10));The public disk, linked with storage:link, serves files from storage/app/public. Keep private files on a private disk and issue temporary URLs. Validate uploads for type and size and never trust the client's file name. Use cloud storage on multi-server setups.
Practice. Upload a file to a private disk and serve it through a signed temporary link.
Stage 8 Testing and code quality
Goal. Write fast, trustworthy tests and keep the code consistent.
Tests Core
Tests prove that code works and keep it working as it changes. Laravel creates a tests directory with Feature and Unit folders and a test runner configured for an in-memory or separate test database. Run them with php artisan test.
Write tests for behaviour that matters: money, permissions, validation and anything that broke before. Make them fast so people run them. A reliable suite lets you upgrade Laravel and refactor with confidence. Run tests in CI on every push.
Practice. Run the test suite on a new project and make a deliberately failing test pass.
Unit and Feature Tests Core
Unit tests exercise a single class in isolation without booting the framework fully. Feature tests send requests through the application, touching routes, middleware, validation, the database and views. In Laravel, most valuable tests are feature tests.
public function test_guest_cannot_create_post(): void
{
$this->post('/posts', ['title' => 'x'])->assertRedirect('/login');
}
public function test_user_can_create_post(): void
{
$this->actingAs(User::factory()->create())
->post('/posts', ['title' => 'Hello', 'body' => 'Text'])
->assertRedirect();
$this->assertDatabaseHas('posts', ['title' => 'Hello']);
}Use factories for data and the RefreshDatabase trait to reset state. Fake external services.
PHPUnit in Laravel testing provides the base TestCase and assertions that these examples use.
Practice. Write feature tests for the happy path and two failure paths of one endpoint.
Pest Testing Framework Recommended
Pest is a test framework on top of PHPUnit with shorter, more readable syntax, and the installer lets you choose it for new projects. Tests are functions, with expectations that chain.
it('lists published posts', function () {
Post::factory()->count(3)->create();
$this->get('/posts')->assertOk();
expect(Post::count())->toBe(3);
});It supports datasets, hooks, architecture tests and parallel runs. Everything you know about Laravel's testing helpers works in Pest. Pick Pest or PHPUnit per project and keep to it.
Practice. Rewrite two PHPUnit tests in Pest and compare the readability.
Pint Recommended
Pint is Laravel's code style fixer built on PHP-CS-Fixer. It ships with new projects and applies the Laravel coding style with no configuration; a pint.json file customises rules.
./vendor/bin/pint
./vendor/bin/pint --test # check only, for CIRun it before committing or as a CI step so reviews focus on logic instead of formatting. Pair it with static analysis such as PHPStan or Larastan, which catches type errors that tests might miss. See Code Review Roadmap.
Pulse is the production dashboard for slow requests and queues.
Practice. Run Pint on a messy file, review the diff, and add the --test check to your CI.
Stage 9 Deployment and operations
Goal. Configure, optimise and ship a Laravel app, and choose a hosting approach.
Deployment Configuration Core
Production needs different settings: APP_ENV=production, APP_DEBUG=false, a real APP_KEY, production database and cache credentials, and queue and mail drivers. Point the web server's document root at public, never at the project root.
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache && php artisan route:cache && php artisan view:cache
php artisan queue:restartSet file permissions so only storage and bootstrap/cache are writable. Keep secrets in the environment, run migrations carefully, and have a rollback plan.
Practice. Write a deployment script and run it against a fresh server or container.
Optimization Recommended
Laravel is fast enough for most apps if you avoid common problems. Cache config, routes, views and events in production (php artisan optimize). Remove N+1 queries with eager loading, add database indexes, paginate lists and cache expensive results.
$stats = Cache::remember('dashboard.stats', 300, fn () => Stats::build());Move slow work to queues, serve assets through a CDN, and enable OPcache. Measure before and after with a profiler or Pulse. Most slowness is in database queries, not in PHP.
Practice. Profile a slow page, fix the top query problem and record the before and after times.
Octane Optional
Octane keeps the application in memory between requests, using servers such as FrankenPHP, Swoole or RoadRunner, which can give a large throughput boost. The framework boots once rather than on every request.
The catch is state: static properties, singletons and container instances persist across requests, so leaks and stale data become possible. Avoid storing request data in long-lived objects, and test under load. Use it when profiling shows framework boot time matters; many apps do not need it.
Practice. Run a simple benchmark with and without Octane and list one state-leak risk in your code.
Laravel Forge Optional
Forge provisions and manages servers on cloud providers you connect: it installs PHP, Nginx, a database and Redis, sets up SSL certificates, queue workers, scheduler entries and deployment from Git. You still own the servers and their updates.
It suits small and medium teams that want control without writing server automation. Compare with managed platforms and containers on cost, scaling needs and how much operations work you want to take on.
Laravel Cloud is the managed alternative from the same team. Laravel for full stack work combines these skills with Livewire or Inertia, tests and deployment.
Practice. Compare Forge, a managed platform and a container setup for a small app and write the trade-offs.
Projects to build
Build these in order. Each one should end up in a public repository with a short README that explains your decisions.
- Personal blog with admin areaBeginner
Build a blog with public post pages, an admin area to create, edit and delete posts, and image uploads. Use migrations, a resource controller, Blade layouts, form validation and route model binding by slug.
Routing · Blade layouts · Eloquent CRUD · Validation · File storage
- Task manager with teamsIntermediate
Build a task app where users belong to teams, create tasks with comments and tags, and see only their team's data. Add policies so users cannot touch other teams' records, and paginate and filter lists.
Eloquent relationships · Policies · Pagination · Authentication · Query scopes
- REST API with token authenticationIntermediate
Build a JSON API for a bookstore with API resources, validation, filtering, pagination, Sanctum tokens with abilities, rate limiting and a feature test for each endpoint.
API resources · Sanctum · Rate limiting · Feature tests · Error handling
- Order system with queues and notificationsAdvanced
Build a shop checkout where placing an order dispatches events, sends a queued confirmation email, generates a PDF invoice in a job, and notifies the user in the app. Handle retries, failures and idempotent jobs.
Events · Queues · Notifications · Scheduling · Failed jobs
- Deployed multi-tenant dashboardAdvanced
Build a small software-as-a-service dashboard with tenant-scoped data, billing-style roles, Pulse or Telescope, a CI pipeline running Pint and tests, and a live deployment with caching, queue workers and the scheduler configured.
Global scopes · Authorization · CI · Deployment configuration · Optimization
Interview questions
Try to answer each question out loud before you open the answer.
Explain the Laravel request lifecycle.Junior
The web server sends every request to public/index.php, which boots the application and the service container. Service providers register and boot services. The request goes through global middleware, then the router matches a route, route middleware runs, and a controller or closure returns a response. The response passes back through the middleware and is sent to the browser. Knowing the order explains issues with sessions, authentication and redirects.
What is the service container and why does it matter?Mid
It is Laravel's dependency injection container. When you type-hint a class in a controller, job or constructor, the container builds it, including its own dependencies. You bind interfaces to implementations in service providers. It matters because it decouples code from concrete classes, makes behaviour swappable and lets tests replace real services with fakes.
What is the N+1 query problem and how do you fix it in Eloquent?Junior
It occurs when you load a list of models and then access a relationship on each one, which runs one extra query per row. Fix it with eager loading: Post::with('author')->get(). Use withCount for counts and select only needed columns. Detect it with Debugbar or Telescope, and call Model::preventLazyLoading() in non-production environments to fail loudly.
How do you protect against mass assignment?Junior
Mass assignment means filling a model from an array of input, which could set columns the user should not control, like is_admin. Define $fillable as an allow list or $guarded, and build models from validated data, not from $request->all(). Prefer Form Requests, which return only validated fields.
What is the difference between a gate and a policy?Mid
A gate is a closure for a single action not tied to a model, like accessing an admin area. A policy is a class grouping rules for one model: view, update, delete. Use gates for simple global checks and policies for resource permissions. Check them with authorize, can or @can, and test both allowed and denied cases.
How do queues work, and what do you do about failures?Mid
A job class is dispatched to a queue backed by Redis or the database, and a worker process runs handle asynchronously. For failures, set $tries and $backoff, define a failed method, and keep a failed jobs table so jobs can be inspected and retried. Make jobs idempotent, run workers under a supervisor, and restart them on deployment so they pick up new code.
Why can env() return null in production?Mid
When you cache configuration with config:cache, the .env file is no longer loaded, so calls to env() outside config files return null. The rule is to call env() only in config files and read values elsewhere with config(). Always clear and rebuild the config cache during deployment.
How do you secure a Laravel application?Mid
Keep debug off in production and the app key and secrets out of Git. Rely on CSRF protection for forms and escape output in Blade. Use Eloquent and the query builder with bound parameters. Validate all input, authorise each record access, hash passwords, rate limit sensitive endpoints, and keep dependencies updated with Composer audit. Serve over HTTPS and restrict file uploads by type and size.
When would you use Sanctum rather than Passport?Mid
Sanctum is simpler: cookie sessions for your own single-page app and personal access tokens for mobile or simple API clients. Passport implements the whole OAuth 2.0 protocol, for when third-party applications need delegated access with scopes and refresh tokens. If you are not building a platform for external developers, choose Sanctum.
How do you test a Laravel application?Junior
Mostly with feature tests that send requests through the app and assert on responses and the database, using factories and the RefreshDatabase trait. Unit tests cover isolated logic. I fake external effects with Mail::fake(), Queue::fake() and Http::fake(), test authorisation both ways, and run the suite in CI with Pint. Pest or PHPUnit are both fine; consistency matters more.
What are facades and what is the trade-off?Mid
Facades are static-looking proxies to objects in the container, such as Cache::get(). They are short and can be faked in tests. The trade-off is hidden dependencies: a class's constructor no longer shows what it needs, and overuse can blur design. Many teams use them in controllers and jobs and prefer constructor injection for core domain classes.
How would you design a REST API in Laravel?Mid
Use apiResource routes, controllers that validate with Form Requests and return API resources to shape output, consistent error format with proper status codes, pagination, filtering, authentication with Sanctum, rate limiting and versioned URL prefixes. Write feature tests for each endpoint and document the contract. Keep the controllers thin and put business logic in action classes.
How do you deploy and optimise a Laravel app?Senior
Set production environment values, install without dev dependencies, run migrations with --force, and cache config, routes, views and events. Run queue workers under a supervisor and a scheduler cron entry, restart workers after deploys, and point the web root at public. For speed, remove N+1 queries, index columns, cache results, offload work to queues, enable OPcache and measure before changing anything.
What is the difference between middleware and a policy?Junior
Middleware wraps requests and applies to routes or groups, so it suits cross-cutting rules such as being logged in, being verified or being within a rate limit. A policy decides whether a user may act on a specific record, so it needs the model. Use middleware for who can reach an area and policies for who can act on a thing.
How do you handle a long-running import without exhausting memory?Senior
Do not load everything at once. Read the file in chunks or lazily, use lazy() or cursor() for queries, and process batches in queued jobs with Bus::batch, so failure of one chunk does not lose the rest. Validate rows, collect errors into a report, and make jobs idempotent so retries are safe. Report progress to the user.
What changed in recent Laravel versions that you should know?Mid
New projects have a slimmer skeleton: middleware, routing options and exception handling are configured in bootstrap/app.php, the HTTP Kernel was removed, API routes are installed on demand, SQLite is the default database, and there is a built-in health route. Official starter kits for React, Vue and Livewire replaced older choices as the recommended start. Always check the upgrade guide for the version you use.
Common mistakes
- Putting business logic in controllers or Blade views: move it into models, actions or services so it can be tested and reused.
- Passing
$request->all()into models: validate and use the validated array with $fillable defined. - Loading relationships inside loops: eager load with
with()and check query counts in Debugbar. - Calling
env()outside config files: read fromconfig()or caching will break the value. - Checking only that a user is logged in: authorise access to each record with policies, and test the denied case.
- Running slow work in the request: dispatch queued jobs and keep requests fast.
- Editing migrations that already ran elsewhere: add a new migration instead.
- Deploying with debug on or without clearing caches: use a deployment script that sets environment values and rebuilds caches.
Ready-for-interview checklist
- I can explain the request lifecycle from index.php to a response
- I can build CRUD with a resource controller, Form Request and Blade views
- I can write migrations, factories and seeders and keep a schema under version control
- I can model one-to-many and many-to-many relationships and eager load them
- I can explain and fix the N+1 problem
- I can implement login, tokens and authorisation with policies
- I can use the container and explain facades versus injection
- I can dispatch jobs, handle retries and run workers safely
- I can write feature tests with factories and fakes
- I can configure caching and environment values for production
- I can describe the front-end options: Blade, Livewire and Inertia
- I can explain how I would secure a Laravel app
- I can use Pint and a CI pipeline for consistent quality
Related roadmaps
- PHP RoadmapModern PHP, OOP, Composer, security and testing.
- API Design RoadmapREST, gRPC, GraphQL, versioning, auth, pagination and rate limits.
- SQL RoadmapQueries, joins, window functions, indexes and transactions.
- Docker RoadmapImages, containers, networking, volumes, Compose and security.
- Redis RoadmapData types, caching patterns, persistence, clustering and streams.
- Git and GitHub RoadmapCommits, branches, rebasing, pull requests, Actions and recovery.
FAQ
Is Laravel still worth learning?
Yes, if you want PHP jobs, freelance work or fast product development. It has a large community, a mature ecosystem and regular releases. It is less common in some large enterprises that standardise on other stacks, so check the job market near you. The ideas about routing, ORM, queues and testing transfer to other frameworks.
Do I need to learn PHP before Laravel?
You need the basics: classes, namespaces, interfaces, traits, arrays and type declarations. Laravel code uses modern PHP features heavily, so learning them first saves confusion. A couple of weeks on the language, then a project, is a sensible start.
Livewire, Inertia or a separate front end?
Choose by team skills. Livewire keeps you in PHP and Blade and is quick for dashboards and forms. Inertia suits teams comfortable with Vue or React who want one codebase. A separate API and front end suits larger teams, mobile clients or public APIs. Pick one per project.
How long does it take to learn Laravel?
With PHP basics, you can build a working CRUD app in two to three weeks. Reaching interview-ready level, with relationships, authorisation, queues, testing and deployment, usually takes ten to fourteen weeks of steady practice and one or two complete projects.
Should I use Sail, Herd or Docker myself?
For learning, Herd on Mac or Windows is quickest, and artisan serve with SQLite is the lightest option. For teams, Sail gives everyone the same containers. Use whatever matches production closely enough to avoid surprises.