Home › Web Development › Laravel
WEB DEVELOPMENT / LARAVEL
Learn Laravel from PHP framework fundamentals and MVC architecture to routing, controllers, Blade, Eloquent, databases, APIs, authentication, security, testing, performance and deployment.
Every concept comes with a short, copyable example using current Laravel patterns and secure defaults.
THE BASICS
Laravel is a PHP web application framework. It provides structure and conventions for building web applications and backend systems — routing, request handling, database access, validation, authentication scaffolding, queues, caching, testing tools and a defined place for everything to live.
Laravel is not a programming language. It’s written in PHP, and Laravel applications are PHP applications. Everything you know about PHP classes, namespaces, interfaces, traits, type declarations and Composer applies directly — Laravel adds structure on top rather than replacing the language beneath. See our PHP guide for those foundations.
It’s also not frontend-only or backend-only in any narrow sense. Laravel can serve complete server-rendered pages through Blade, expose JSON APIs for a React or mobile frontend, or do both in the same application.
The value is mostly in what you don’t have to build: routing, an ORM, validation, authentication scaffolding, queue handling, caching, a testing setup and a migration system all come ready. The cost is learning its conventions and accepting a certain way of organising code. For a three-page site that’s overhead with no return; for an application with users, permissions, business rules and data relationships, it saves a great deal of work. Laravel is a good fit for many projects, not for every project.
WHERE LARAVEL FITS
PHP is the language; Laravel is a framework written in it. Laravel gives you routing, an ORM, validation and conventions — PHP gives you the syntax, classes, types and standard library those tools are built from. Laravel does not replace PHP knowledge; it depends on it.
They solve different problems and are not alternatives. React builds the interface in the browser; Laravel handles routing, data, business rules and authorisation on the server. A common architecture has React calling a Laravel API over HTTP and rendering the JSON it returns.
Laravel and WordPress are both PHP, and both build websites, but they start from opposite ends. WordPress gives you a working content system to extend; Laravel gives you a framework to build with. Neither is better — the right choice depends on whether your project is mostly content or mostly custom logic. A magazine site in Laravel means rebuilding what WordPress already does; a multi-tenant SaaS in WordPress means fighting a content model that wasn’t designed for it. See our CMS guides.
ARCHITECTURE
Every Laravel request follows the same path. It enters through a single public entry point, the framework bootstraps, middleware runs, the router matches the URL to a handler, a controller coordinates the work, models read and write data, and a response goes back — rendered HTML from Blade, or JSON for an API client.
MVC is the organising idea: models own data and business rules, controllers handle requests, views handle presentation. It’s a useful mental model and Laravel’s directory structure reflects it.
Real applications outgrow the three-box diagram though. Most non-trivial Laravel projects also have form requests for validation, services or action classes for business logic, policies for authorisation, jobs for background work, events and listeners for decoupling, and API resources for shaping JSON. MVC describes the skeleton; those layers are where the application actually lives. Not every project needs all of them, and adding layers before you need them is its own mistake.
<?php
use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;
// Basic routes by HTTP verb
Route::get('/posts', [PostController::class, 'index']);
Route::post('/posts', [PostController::class, 'store']);
Route::put('/posts/{post}', [PostController::class, 'update']);
Route::delete('/posts/{post}', [PostController::class, 'destroy']);
// Route parameters
Route::get('/posts/{id}', function (string $id) {
return $id;
});
// Route model binding — Laravel resolves the model for you
Route::get('/posts/{post}', function (Post $post) {
return view('posts.show', ['post' => $post]);
});
// Named routes avoid hard-coded URLs
Route::get('/dashboard', [DashboardController::class, 'index'])
->name('dashboard');
// redirect()->route('dashboard');
// route('dashboard') inside Blade
// Groups share middleware and prefixes
Route::middleware(['auth'])
->prefix('admin')
->name('admin.')
->group(function () {
Route::get('/users', [UserController::class, 'index'])->name('users');
});
// Resource routes create the conventional CRUD set
Route::resource('products', ProductController::class);ROUTING
Routes map an HTTP method and URL to something that handles it. They live in routes/web.php for browser-facing pages and routes/api.php for API endpoints, which get different middleware — sessions and CSRF for web, stateless handling for API.
Route parameters in braces capture parts of the URL. Route model binding takes that further: type-hint a model and Laravel fetches the matching record automatically, returning a 404 if it doesn’t exist. That removes a lookup and a not-found check from every controller method.
Named routes let you refer to a route by name rather than URL, so changing a path doesn’t mean hunting through views and redirects. Route groups apply shared middleware, URL prefixes and name prefixes to several routes at once — the usual way to protect an admin area.
Route::resource() generates the conventional CRUD routes in one line. Convenient, though worth knowing what it creates: php artisan route:list shows every registered route, and it’s the first command to reach for when a URL isn’t behaving.
HANDLING REQUESTS
Controllers receive requests and return responses. The healthiest ones are thin: validate, delegate, respond. A controller method that runs twenty lines of business logic, builds a query and formats output is doing three jobs, and it’ll be duplicated the moment a second entry point needs the same behaviour.
Middleware runs before or after the request reaches your controller, forming a pipeline. Authentication, CSRF verification, rate limiting, logging and redirecting unverified users are all middleware. Because it’s applied per route or per group, cross-cutting concerns stay out of controllers entirely.
The request object gives access to query parameters, form data, JSON input, headers and uploaded files. Use $request->input() or validated() rather than touching superglobals.
Responses can be a Blade view, a redirect, JSON, a file download or a bare status code. For APIs, returning the right HTTP status matters as much as the body — a 200 with an error message inside tells automated clients everything worked.
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StorePostRequest;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index(Request $request)
{
$posts = Post::with('author')
->latest()
->paginate(20);
return view('posts.index', ['posts' => $posts]);
}
public function show(Post $post) // route model binding
{
return view('posts.show', ['post' => $post]);
}
public function store(StorePostRequest $request)
{
// Validation already ran in the form request
$post = Post::create($request->validated());
return redirect()
->route('posts.show', $post)
->with('status', 'Post created.');
}
// API-style response
public function apiIndex()
{
return response()->json(Post::paginate(20));
}
}{{-- resources/views/layouts/app.blade.php --}}
<!DOCTYPE html>
<html lang="en">
<head>
<title>@yield('title', 'My Application')</title>
</head>
<body>
@include('partials.header')
<main>
@yield('content')
</main>
</body>
</html>
{{-- resources/views/posts/index.blade.php --}}
@extends('layouts.app')
@section('title', 'Posts')
@section('content')
{{-- Escaped output — safe by default --}}
<h1>{{ $title }}</h1>
@if($posts->isNotEmpty())
@foreach($posts as $post)
<article>
<h2>{{ $post->title }}</h2>
<p>{{ Str::limit($post->body, 120) }}</p>
</article>
@endforeach
{{ $posts->links() }}
@else
<p>No posts yet.</p>
@endif
{{-- A Blade component with a slot --}}
<x-alert type="success">
Your changes were saved.
</x-alert>
{{-- {!! $html !!} outputs raw HTML — only for trusted content --}}
@endsectionVIEWS
Blade is Laravel’s templating engine. Templates live in resources/views, compile to plain PHP and are cached, so the directives cost nothing at runtime.
The most important thing about Blade is that {{ }} escapes output by default. Variables rendered that way can’t be interpreted as markup, which prevents most cross-site scripting without you remembering to do anything. The raw syntax {!! !!} opts out of that protection and should only ever be used with content you fully control or have sanitised.
Directives cover the usual logic: @if, @foreach, @forelse, @auth, @csrf. Layouts with @extends and @yield keep shared page structure in one file. Components go further, giving reusable pieces — buttons, alerts, cards, form fields — their own class and template, with slots for content. They’re the Blade equivalent of the component thinking React encourages, and worth using for anything that appears more than twice.
ELOQUENT
Eloquent is Laravel’s ORM. Each model maps to a database table, and instances map to rows, so you write PHP rather than SQL for most operations.
The $fillable property is a security control, not boilerplate. It lists which attributes may be set through mass assignment, so a request containing an unexpected is_admin field can’t quietly set it. Without it, passing request data straight into create() is genuinely dangerous.
Relationships describe how tables connect: hasOne for one-to-one, hasMany and belongsTo for one-to-many, belongsToMany for many-to-many through a pivot table, plus has-many-through and polymorphic relationships for more involved structures. Defined once on the model, they’re then available as properties and query methods.
The N+1 query problem is the one performance issue every Laravel developer meets. Loop over 50 posts accessing $post->author and you’ve run 51 queries: one for the posts, one per post for its author. Eager loading with with() fetches the related records in a second query instead. It’s invisible in development with ten rows and crippling in production with ten thousand — which is why query counting during development matters.
The query builder sits beneath Eloquent and can be used directly through the DB facade, which suits reporting queries and complex joins where models add nothing. Both use parameter binding, so neither concatenates your input into SQL.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
// Mass assignment protection — only these may be set in bulk
protected $fillable = ['name', 'email'];
protected $hidden = ['password', 'remember_token'];
public function profile()
{
return $this->hasOne(Profile::class);
}
public function posts()
{
return $this->hasMany(Post::class);
}
public function roles()
{
return $this->belongsToMany(Role::class); // pivot table
}
// Reusable query scope
public function scopeActive($query)
{
return $query->where('active', true);
}
}
// Reading
$users = User::active()->orderBy('name')->get();
$user = User::findOrFail($id);
// Writing
$user = User::create(['name' => 'Alex', 'email' => 'alex@example.com']);
$user->update(['name' => 'Sam']);
$user->delete();
// N+1 problem: 1 query + 1 per post
$posts = Post::all();
foreach ($posts as $post) { echo $post->author->name; }
// Eager loading: 2 queries total
$posts = Post::with('author')->get();
// Query builder for reporting work
$totals = DB::table('orders')
->select('status', DB::raw('count(*) as total'))
->groupBy('status')
->get();<?php
// database/migrations/xxxx_create_posts_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->string('slug')->unique();
$table->text('body');
$table->boolean('published')->default(false);
$table->timestamps();
// Index columns you filter or join on
$table->index(['published', 'created_at']);
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};
// Seeding with factories
User::factory()->count(10)->has(Post::factory()->count(3))->create();
// Transactions: all steps succeed, or none do
DB::transaction(function () use ($order, $items) {
$order->save();
foreach ($items as $item) {
$item->product->decrement('stock');
}
});DATABASE
Migrations define database structure in version-controlled PHP. Each has an up() that applies a change and a down() that reverses it, so schema history travels with your code and every environment can be brought to the same state with php artisan migrate. This is the main practical advantage over changing tables by hand — the structure is reproducible and reviewable.
Add indexes in migrations for columns you filter, sort or join on. They speed reads at the cost of storage and slightly slower writes, so index deliberately rather than everywhere.
Factories define how to generate realistic model data, and seeders use them to populate a database for development and testing. Together they mean a new developer can go from clone to working application with data in two commands, and tests can build exactly the scenario they need.
Transactions wrap several operations so they all succeed or all roll back. Anything touching multiple tables — creating an order while adjusting stock, transferring a value — needs one, or a failure halfway leaves inconsistent data.
Laravel supports several database systems including MySQL, MariaDB, PostgreSQL and SQLite, configured through environment variables. Which to use is a project decision.
VALIDATION
Validation in Laravel happens on the server, which is the only place it counts. Client-side checks improve the experience; they’re trivially bypassed.
For simple cases, $request->validate() in the controller is enough — it stops execution on failure, redirecting back with errors and old input for web requests, or returning a 422 with a structured error body for API requests. That dual behaviour is why the same validation works for both.
For anything substantial, a form request class moves the rules and authorisation into their own file. The controller then receives an already-validated request, and $request->validated() returns only the fields you declared — a natural guard against unexpected input reaching create().
CSRF protection is applied automatically to web routes. Blade’s @csrf directive adds the token to a form, and middleware verifies it, preventing another site from causing a logged-in user’s browser to submit a request they didn’t intend. Don’t disable it because a form is failing — that error usually means a missing token or an expired session. API routes use token or session authentication instead of CSRF tokens.
<?php
// Inline validation in a controller
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users,email'],
'age' => ['nullable', 'integer', 'min:18'],
'document' => ['nullable', 'file', 'mimes:pdf', 'max:2048'],
]);
// A form request keeps rules out of the controller
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create', Post::class);
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string'],
];
}
public function messages(): array
{
return ['title.required' => 'Please enter a title.'];
}
}<form method="POST" action="{{ route('posts.store') }}">
@csrf
<label for="title">Title</label>
<input id="title" name="title" value="{{ old('title') }}"
@error('title') aria-invalid="true" @enderror>
@error('title')
<p>{{ $message }}</p>
@enderror
<button type="submit">Save post</button>
</form><?php
// A gate: a simple ability check
Gate::define('view-reports', function (User $user) {
return $user->role === 'admin';
});
if (Gate::allows('view-reports')) { /* ... */ }
// A policy: authorisation organised around a model
namespace App\Policies;
class PostPolicy
{
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id || $user->isAdmin();
}
public function delete(User $user, Post $post): bool
{
return $user->isAdmin();
}
}
// Enforced in the controller
public function update(Request $request, Post $post)
{
$this->authorize('update', $post); // throws 403 if denied
// ...
}
// In Blade — UX only, never the actual protection
@can('update', $post)
<a href="{{ route('posts.edit', $post) }}">Edit</a>
@endcan
// Protecting routes
Route::middleware(['auth'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
});AUTH
Authentication asks who someone is; authorisation asks what they may do. Conflating them is the most consequential mistake in this area — being logged in does not entitle a user to edit post 42.
Laravel handles authentication through guards and providers, with several official starter packages offering login, registration, password reset and verification. None is mandatory; you can build your own on the underlying pieces. Passwords are hashed automatically — never store them in plaintext.
For authorisation, gates suit standalone ability checks, while policies organise the rules for a particular model in one class. Both are used through authorize() in controllers, the can middleware on routes, and @can in Blade.
That Blade directive deserves a warning: hiding a button is not authorisation. It improves the interface for people who shouldn’t see the option; it stops nobody from calling the route directly. Enforce permission in the controller or middleware on every protected action.
Sanctum provides authentication for APIs and single-page applications — API tokens with abilities for mobile and third-party clients, or cookie-based session authentication for a first-party SPA on the same domain. It’s one option among several depending on architecture, not a requirement.
APIS
Laravel is widely used as an API backend. API routes live in routes/api.php, return JSON, and use token or Sanctum authentication rather than session cookies and CSRF tokens.
API resources are the piece people skip and later regret. Returning a model directly exposes your database columns as your public contract, so a schema change breaks every client. A resource class defines exactly which fields appear and how they’re named, keeping the API stable while the database evolves — and keeping internal fields out of the response.
Status codes carry meaning: 200 for success, 201 for created, 401 for unauthenticated, 403 for forbidden, 404 for missing, 422 for validation failures and 500 for server errors. Laravel returns 422 with a structured errors object automatically when validation fails on an API request.
Pagination should be the default for collections — paginate() returns data alongside pagination metadata. Add rate limiting with throttle middleware on public and authentication endpoints, serve over HTTPS, validate everything, authorise every action, and make sure detailed exceptions never reach clients in production. More on API design in our website APIs guide.
<?php
// An API resource shapes the JSON contract
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'author' => new UserResource($this->whenLoaded('author')),
'published' => (bool) $this->published,
'createdAt' => $this->created_at->toIso8601String(),
];
}
}
// The controller
class PostApiController extends Controller
{
public function index()
{
return PostResource::collection(
Post::with('author')->paginate(20)
);
}
public function store(StorePostRequest $request)
{
$post = Post::create($request->validated());
return (new PostResource($post))
->response()
->setStatusCode(201);
}
}
// Protected, rate-limited API routes
Route::middleware(['auth:sanctum', 'throttle:60,1'])->group(function () {
Route::apiResource('posts', PostApiController::class);
});INTEGRATION
The pattern for any JavaScript frontend is the same: Laravel exposes JSON endpoints, the frontend calls them over HTTP, and everything that must be trusted — validation, authorisation, business rules, database access — stays on the Laravel side. A React app hiding an admin button changes nothing about what the API will accept.
For a first-party React app on the same domain, Sanctum’s SPA authentication uses cookies and existing session infrastructure. For mobile apps or third-party clients, API tokens fit better. Laravel also supports server-rendered pages with islands of JavaScript interactivity — not every project needs a separate frontend application, and that choice has real consequences for complexity and SEO.
WordPress and Laravel can coexist. A common arrangement has WordPress handling editorial content and exposing it through its REST API, with a Laravel application consuming that content alongside its own data. It’s an architecture worth choosing deliberately rather than by default — two systems means two deployments, two sets of updates and two places for things to break.
// Fetching from a Laravel API endpoint
async function loadPosts() {
const response = await fetch("/api/posts", {
headers: {
"Accept": "application/json",
"Authorization": `Bearer ${token}`,
},
});
// fetch does not reject on 4xx or 5xx
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
const { data, meta } = await response.json();
return { posts: data, pagination: meta };
}
// Handling Laravel validation errors (422)
async function createPost(payload) {
const response = await fetch("/api/posts", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
body: JSON.stringify(payload),
});
if (response.status === 422) {
const { errors } = await response.json();
return { errors }; // { title: ["Please enter a title."] }
}
return { post: await response.json() };
}<?php
// A service class holds business logic
namespace App\Services;
class OrderService
{
public function __construct(
private PaymentGateway $gateway,
private InventoryService $inventory,
) {}
public function place(User $user, array $items): Order
{
return DB::transaction(function () use ($user, $items) {
$order = $user->orders()->create(['status' => 'pending']);
$this->inventory->reserve($items);
$this->gateway->charge($order->total());
return $order;
});
}
}
// The container injects dependencies automatically
class OrderController extends Controller
{
public function __construct(private OrderService $orders) {}
public function store(PlaceOrderRequest $request)
{
$order = $this->orders->place($request->user(), $request->validated());
return redirect()->route('orders.show', $order);
}
}
// Config reads from the environment — never hard-code secrets
// config/services.php
return [
'gateway' => [
'key' => env('GATEWAY_KEY'),
],
];
// Read through config(), not env(), outside config files
$key = config('services.gateway.key');STRUCTURE
When controllers grow past coordinating, move the work into a service or action class. The test is reuse and testability: logic needed by both a web controller and an API controller belongs somewhere both can call. Don’t create a service layer for every model on principle — over-layering a simple CRUD application adds indirection and nothing else. The same applies to repository patterns, which Laravel does not require and which often just wrap Eloquent in a thinner Eloquent.
Dependency injection means a class declares what it needs in its constructor rather than creating it. Laravel’s service container resolves those type-hinted dependencies automatically, including into controller methods. The practical payoff is testing: you can substitute a fake payment gateway without touching the class being tested.
Service providers are where bindings and bootstrapping are registered — telling the container which implementation to use for an interface, registering event listeners, publishing package configuration.
Configuration lives in config/ and reads from the .env file. Two rules matter: .env must never be committed to version control, and outside config files you should read values through config() rather than env(), because cached configuration in production makes env() return null.
BACKGROUND WORK
Slow work doesn’t belong in a web request. Queues let you dispatch a job and respond immediately, while a background worker does the actual work — sending email, processing images, generating reports, importing data, calling slow third-party APIs.
Queues need a driver (database, Redis and others are supported) and a running worker process, which is a real deployment consideration: no worker means jobs pile up silently. Laravel handles retries and records failures so they can be inspected and retried rather than vanishing.
Events and listeners decouple related behaviour. An OrderPlaced event can have listeners that send a confirmation, update analytics and notify the warehouse — each in its own class, each addable without touching the code that placed the order. Overuse makes flow hard to follow, so reserve it for genuinely independent reactions.
Notifications send messages across channels, with mail and database available by default and others through additional packages. Mailables define emails as classes with Blade templates, and queuing them is usually right, since SMTP is slow.
Task scheduling defines recurring work in PHP — cleanups, reports, synchronisation — run by a single cron entry calling Laravel’s scheduler, rather than a growing list of crontab lines.
<?php
// A queued job
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendWelcomeEmail implements ShouldQueue
{
public $tries = 3;
public function __construct(public User $user) {}
public function handle(): void
{
Mail::to($this->user)->send(new WelcomeMail($this->user));
}
}
SendWelcomeEmail::dispatch($user);
SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(10));
// An event with independent listeners
OrderPlaced::dispatch($order);
// Listeners: SendConfirmation, UpdateAnalytics, NotifyWarehouse
// Scheduled tasks (routes/console.php or the scheduler)
Schedule::command('reports:daily')->dailyAt('02:00');
Schedule::call(fn () => Cache::forget('stats'))->hourly();
// Running a worker
// php artisan queue:work --tries=3<?php
// Cache an expensive result
$stats = Cache::remember('dashboard.stats', now()->addMinutes(15), function () {
return [
'orders' => Order::whereMonth('created_at', now()->month)->count(),
'revenue' => Order::sum('total'),
];
});
Cache::forget('dashboard.stats'); // invalidate when data changes
// Storing an uploaded file safely
$request->validate([
'document' => ['required', 'file', 'mimes:pdf,jpg,png', 'max:2048'],
]);
// store() generates a safe random filename
$path = $request->file('document')->store('documents');
// Public files, served through the storage link
$publicPath = $request->file('avatar')->store('avatars', 'public');
$url = Storage::disk('public')->url($publicPath);
// Private files: serve through a controller with an authorisation check
public function download(Document $document)
{
$this->authorize('view', $document);
return Storage::download($document->path);
}CACHING & FILES
Caching stores the result of expensive work so it isn’t repeated. Cache::remember() is the common pattern: return the cached value if present, otherwise run the callback and store it. The genuinely hard part isn’t caching — it’s invalidation. A cached dashboard showing figures from twenty minutes ago is worse than a slightly slower accurate one, so decide when each cached value must be cleared.
Laravel supports several cache drivers. Redis is commonly used for caching, sessions and queues in production because it’s fast and shared across servers, but it isn’t required — file and database drivers work fine for smaller applications.
The filesystem abstraction gives one API across local disks and cloud storage, so moving from local files to object storage is a configuration change rather than a rewrite.
Uploads need layered checks: validate the type and size through validation rules, let store() generate the filename rather than trusting the submitted one, and keep files that need permission checks out of the public disk — serve those through a controller that authorises the request first. Files in the public disk are readable by anyone with the URL.
TOOLING
Artisan is Laravel’s command-line interface. It generates classes, runs migrations, manages caches, starts queue workers and lists routes, and you can write your own commands for maintenance tasks.
Composer manages PHP dependencies. composer install installs exactly what’s in the lock file — which is what you run on a server. composer update resolves new versions and rewrites the lock file, which belongs in development, tested, and committed. Running it directly on production is a good way to deploy an untested dependency upgrade.
The project structure follows conventions worth knowing: app/ holds application code with Http/ for controllers, requests and middleware and Models/ for Eloquent models; routes/ separates web and API routes; resources/views/ holds Blade templates; database/ holds migrations, factories and seeders; config/ holds configuration; storage/ holds logs, cache and uploads; tests/ holds tests; and public/ is the only directory the web server should expose.
That last point matters for deployment: the document root points at public/, so everything else — including .env — sits outside the web-accessible path.
# Inspecting the application php artisan route:list php artisan about php artisan migrate:status # Generating classes php artisan make:controller PostController --resource php artisan make:model Product -m php artisan make:request StorePostRequest php artisan make:policy PostPolicy --model=Post php artisan make:job SendWelcomeEmail # Database php artisan migrate php artisan migrate:rollback php artisan db:seed # Clearing caches during development php artisan config:clear php artisan cache:clear php artisan view:clear # Queue worker php artisan queue:work # Dependencies composer install # use this on servers composer update # in development, then commit the lock file
<?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; // fresh database per test
public function test_guests_cannot_create_posts(): void
{
$response = $this->post('/posts', ['title' => 'Test']);
$response->assertRedirect('/login');
}
public function test_a_user_can_create_a_post(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->post('/posts', [
'title' => 'My first post',
'body' => 'Some content.',
]);
$response->assertRedirect();
$this->assertDatabaseHas('posts', ['title' => 'My first post']);
}
public function test_validation_errors_are_returned(): void
{
$user = User::factory()->create();
$this->actingAs($user)
->postJson('/api/posts', [])
->assertStatus(422)
->assertJsonValidationErrors(['title', 'body']);
}
public function test_users_cannot_edit_others_posts(): void
{
$post = Post::factory()->create();
$this->actingAs(User::factory()->create())
->put("/posts/{$post->id}", ['title' => 'Hijacked'])
->assertForbidden();
}
}TESTING
Laravel ships with a testing setup, and the framework’s test helpers are a large part of why testing here feels worthwhile rather than tedious.
Feature tests exercise a complete request: send it, assert on the status, redirect, session state or JSON. They give the best return for the effort because they test what users actually do rather than internal wiring. Unit tests cover individual classes in isolation — useful for calculation-heavy services.
The RefreshDatabase trait gives each test a clean database, and factories build exactly the records the scenario needs. Assertions like assertDatabaseHas() check the result at the data level, and actingAs() authenticates as a given user in a line.
If you test nothing else, test authorisation. The tests worth writing first are the negative ones: a guest can’t reach a protected route, a user can’t edit someone else’s record, an invalid payload returns 422. Those are the failures that matter most and the ones manual testing consistently misses, because you’re logged in as an admin while you work.
SECURITY
Laravel provides good defaults, and the crucial caveat is that defaults are not immunity. A framework can prevent categories of mistake; it can’t prevent you from writing insecure code.
SQL injection is largely handled because Eloquent and the query builder use parameter binding. Raw expressions with user input concatenated in bypass that protection entirely — if you need raw SQL, bind the parameters.
Mass assignment protection comes from $fillable or $guarded. Without it, a request containing extra fields can set columns you never intended, which is how a registration form quietly grants admin rights.
XSS is mitigated by Blade escaping output in {{ }}. The raw syntax opts out; use it only with content you control or have sanitised with a maintained library.
Passwords are hashed by Laravel’s hashing services. Never store them in plaintext, and never use MD5 or SHA-1 for the purpose.
Rate limiting through throttle middleware protects login forms, password resets and public API endpoints from brute force and abuse. And the most common production mistake: debug mode left enabled, which exposes file paths, configuration and sometimes credentials on any error page.
[ ] Validate all input server-side [ ] Use form requests for substantial rules [ ] Authorise every protected action with gates or policies [ ] Set $fillable or $guarded on every model [ ] Keep .env out of version control [ ] Disable debug mode in production [ ] Serve everything over HTTPS [ ] Apply rate limiting to auth and API endpoints [ ] Validate file uploads and store them safely [ ] Never expose stack traces to users [ ] Keep PHP, Laravel and packages updated [ ] Review third-party dependencies [ ] Use database transactions for related writes [ ] Log security events without logging secrets
<?php
// Eager load to avoid N+1 queries
$posts = Post::with(['author', 'tags'])->paginate(20);
// Select only the columns you need
$users = User::select(['id', 'name', 'email'])->get();
// Paginate rather than loading everything
$orders = Order::latest()->paginate(50);
// Chunk large datasets to keep memory flat
Order::where('status', 'pending')->chunkById(500, function ($orders) {
foreach ($orders as $order) {
ProcessOrder::dispatch($order);
}
});
// Cache expensive aggregates
$revenue = Cache::remember('revenue.month', now()->addHour(), function () {
return Order::whereMonth('created_at', now()->month)->sum('total');
});
// Move slow work off the request
GenerateMonthlyReport::dispatch($month);
# Production optimisation commands
# php artisan config:cache
# php artisan route:cache
# php artisan view:cachePERFORMANCE
In most Laravel applications the bottleneck is the database, and specifically the number of queries rather than their individual speed. Measure before optimising — a query-logging tool during development tells you more than any amount of guessing.
Eager loading is the highest-value fix, since N+1 problems appear naturally as soon as you loop over related data. After that: index the columns you filter and join on, select only the columns you need, paginate collections rather than loading everything, and chunk large datasets so memory stays flat.
Caching helps for expensive aggregates and repeated reads, provided you’ve thought about invalidation. Queues improve perceived performance by moving slow work out of the request entirely — often a bigger win than optimising the work itself.
In production, config:cache, route:cache and view:cache reduce per-request work, and OPcache should be enabled at the PHP level. Run a supported PHP version. Backend response time is one input to overall page speed alongside assets, images and front-end work — see website speed for the full picture.
DEPLOYMENT
A Laravel application needs a PHP runtime of a supported version with the required extensions, a web server such as Nginx or Apache with the document root pointing at public/, a database, Composer available during deployment, writable storage, and HTTPS. If you use queues or scheduled tasks, you also need a supervised worker process and a cron entry — both easy to forget, and both silent when missing.
A typical deployment workflow runs: commit to version control, run tests, install dependencies from the lock file, set environment configuration, run migrations, rebuild caches, restart workers, then monitor. Automating it through CI/CD makes deployments repeatable and reduces the chance of skipping a step. Which CI provider you use is a preference, not a technical requirement.
Two things belong in version control discipline: never commit .env or any production credentials, and commit the Composer lock file so servers install exactly what you tested. Hosting requirements are covered further in our hosting and domains guides.
APPLICATIONS
Laravel does not make a website SEO-friendly automatically, and nothing about the framework affects rankings by itself. What it does give you is control: clean URLs through routing, server-rendered HTML through Blade, per-page titles and meta descriptions, canonical URLs, real internal links and structured data where appropriate.
Server-rendered Blade output does have one structural advantage worth stating plainly: the content is in the initial HTML response, so it doesn’t depend on a crawler executing JavaScript. That removes a category of problem rather than guaranteeing anything — content quality, relevance, performance and accessibility still determine results. See our website SEO guides.
Laravel applications commonly serve both web and API routes: Blade pages for public content where server rendering helps, and JSON endpoints for a React dashboard or mobile app behind a login. That’s often the most practical architecture — you get crawlable content where it matters and a proper API where it’s needed, from one codebase and one set of business rules.
Custom database-driven systems built around specific business processes.
Internal tools with roles, permissions and reporting.
Backends for React frontends, mobile apps and third-party integrations.
Products, carts, orders, payments and fulfilment workflows.
Content systems shaped around a specific editorial model.
Multi-tenant platforms with subscriptions and per-account data.
Availability, scheduling, reminders and confirmations.
Customers, activity history, permissions and reporting.
Courses, enrolment, progress tracking and content delivery.
PRACTICE
Form requests for anything beyond trivial rules.
Policies and gates, enforced in the backend.
Validate, delegate, respond.
Understand what each query actually runs.
Load related data intentionally.
Environment variables, never in the repository.
Keep the request fast.
Auth, permissions and money paths first.
Everywhere, enforced.
Logs, errors and slow queries.
Business logic, queries and formatting all in one method.
Trusting request data because the form looked fine.
Checking that someone is logged in, not what they may do.
Reaching for raw SQL where Eloquent would do, losing binding safety.
Fine with ten rows, crippling with ten thousand.
Queries scanning whole tables as data grows.
Credentials in code instead of environment variables.
Production credentials in the repository history.
Exposing paths, config and sometimes credentials.
Client-side checks treated as protection.
Turning off middleware to make a form submit.
Raw syntax used with untrusted content.
No type or size validation, public storage for private files.
Errors returned as 200, or raw exceptions leaked.
Services and repositories for a simple CRUD screen.
Related writes that can fail halfway.
Emails and reports blocking the response instead of queued.
Auth and payments verified only by clicking around.
DEBUGGING
Laravel’s logs in storage/logs are the first place to look — exceptions, stack traces and anything you’ve logged yourself. Log useful context such as the user id and the operation, but never passwords, tokens or personal data.
php artisan route:list answers most routing confusion by showing exactly what’s registered, with which middleware. php artisan about shows the environment and cache state, which frequently explains why a change appears to have no effect — a cached config or route file in development is a classic false alarm, cleared with config:clear and route:clear.
For database problems, look at the queries themselves: how many ran, in what order, with what bindings. Query-logging and debugging packages are common in development and should not be installed in production.
For API problems, the response status is the fastest signal: 401 means authentication, 403 means authorisation, 422 means validation with the failing fields listed, and 500 means an exception you’ll find in the logs. In production, error detail belongs in logs and monitoring — never in the response.
Find the exact request that fails.
storage/logs holds the exception and stack trace.
Method, route, payload, headers and authenticated user.
route:list confirms what actually matched.
Which queries ran, how many, and with what bindings.
Status code and body, especially for APIs.
Config and route caches can mask a change.
Cover the case with a test before deploying.
# See every registered route and its middleware
php artisan route:list
php artisan route:list --path=api
# Environment and cache state
php artisan about
php artisan migrate:status
# Clear caches when changes seem to have no effect
php artisan config:clear
php artisan route:clear
php artisan view:clear
php artisan cache:clear
# Tail the log while reproducing the issue
tail -f storage/logs/laravel.log
<?php
// Log with context, never with secrets
Log::info('Order placed', ['order_id' => $order->id, 'user_id' => $user->id]);
Log::error('Payment failed', ['order_id' => $order->id]);
// Inspect the queries a block of code runs
DB::listen(fn ($query) => Log::debug($query->sql, $query->bindings));REFERENCE
A categorised quick reference for the features covered above. For authoritative documentation on any of them, use the official Laravel documentation at laravel.com/docs, and php.net for the language itself.
Route::get()Route::resource()->name()->middleware()make:controllermake:model -mmigrateroute:listreturn view()return redirect()response()->json()::all()::find()::where()::create()->update()->delete()hasOne()hasMany()belongsTo()belongsToMany()with()paginate()chunkById()DB::transaction()Schema::create()$table->id()foreignId()->index()$request->validate()FormRequestvalidated(){{ $var }}@if@foreach@csrf@extends<x-component>auth()->user()@canauthorize()middleware('auth')Job::dispatch()ShouldQueuequeue:workCache::remember()Storage::disk()->store()INTERVIEW PREP
Concise, technically accurate answers to the Laravel questions that come up most often in interviews and technical assessments.
LEARNING PATH
PHP and object-oriented PHP sit before Laravel deliberately. Laravel’s conventions make more sense when you understand classes, interfaces, namespaces and Composer — most “Laravel is confusing” frustration is actually PHP unfamiliarity in a framework costume.
These are projects to build yourself rather than templates to download. Each introduces a specific set of problems, and each will raise questions — particularly around authorisation and data relationships — that reading alone never does.
Beginner — routes, controllers, Blade and Eloquent.
Beginner — CRUD, validation and relationships.
Intermediate — authentication, policies and relationships.
Intermediate — resources, validation and Sanctum.
Intermediate — categories, stock, transactions and reporting.
Advanced — carts, orders, payments and business rules.
Advanced — roles, permissions, activity history and reports.
Advanced — API-driven frontend and backend integration.
Advanced — multi-tenancy, queues and subscription concepts.
NEXT TOPICS
Learn the server-side programming language Laravel is built with.
Learn how a frontend UI library communicates with Laravel APIs.
Understand the browser-side language used alongside Laravel applications.
Learn how Laravel can expose and consume backend APIs.
Understand the markup delivered by server-rendered Laravel applications.
Learn how Laravel-generated interfaces are styled responsively.
Explore WordPress and other PHP-based content platforms.
Understand the server environment Laravel applications run on.
FAQ
Short answers to the questions people ask most about Laravel and PHP framework development.
Learn how PHP, Laravel routing, MVC, Eloquent, APIs, authentication, security, testing and deployment come together to build modern web applications.