Design Patterns, Explained Simply · Patterns Inside Frameworks
Design Patterns Inside Laravel
A tour of Laravel showing which design pattern each everyday feature is built on (the container, facades, drivers, events, middleware, Artisan, migrations and more) so you recognise and use them deliberately.
The problem
You have used Laravel for a year: Cache::get(), User::factory(), php artisan make:command, middleware, Mail::to(). Each feels like its own API to memorise.
In an interview you are asked "which design patterns does Laravel use, and where?", or a teammate wants to add a custom cache driver and nobody knows where to start. Both are easy once you see that the framework is built from a dozen patterns this course already covered.
Real-world analogy
Knowing the grammar of a language. You can speak by memorising phrases, but once you see the patterns behind them (verb endings, word order) new sentences become obvious. Laravel's features are phrases; the design patterns are its grammar.
The idea
| Pattern | Where Laravel uses it | What to look at |
|---|---|---|
| Dependency Injection | The service container builds classes and injects constructor and method dependencies | Type-hint a class in a controller constructor |
| Service Provider | Bootstrapping: providers register bindings, then boot |
app/Providers/AppServiceProvider.php, bootstrap/providers.php |
| Singleton | $this->app->singleton(...): one shared instance per application |
Database manager, cache manager |
| MVC | Routes, controllers, Eloquent models, Blade views | app/Http/Controllers, resources/views |
| Facade | A static-looking interface to a service in the container | Cache::get(), Mail::to() |
| Proxy | Each facade forwards static calls (__callStatic) to the real object it resolves from the container |
Illuminate\Support\Facades\Facade |
| Adapter | Drivers wrap different libraries behind one Laravel contract | Mail transports, cache stores (Redis, Memcached, files), queue connections, filesystem disks |
| Strategy | The driver is chosen by configuration and swapped without changing calling code | CACHE_STORE, QUEUE_CONNECTION, Cache::store('redis') |
| Factory | Managers create drivers by name; model factories create test models | CacheManager, MailManager; User::factory()->count(3)->create() |
| Builder | The query builder assembles SQL step by step | DB::table('users')->where(...)->orderBy(...)->get() |
| Observer | Eloquent model events and observers; the event dispatcher | creating, created, updated events; php artisan make:observer |
| Chain of Responsibility | Middleware can stop a request (abort(403)) or pass it on with $next($request) |
app/Http/Middleware |
| Decorator | Each middleware wraps the next, adding behaviour before and after | Global, group and route middleware stacked around the controller |
| Pipeline | Illuminate\Pipeline\Pipeline sends a request through the middleware stack |
->send($request)->through($middleware)->then(...) |
| Command | Artisan commands and queued jobs are objects with a handle() method |
php artisan make:command, php artisan make:job |
| Template Method | The framework runs a fixed algorithm and calls your hook methods | Migrations (up/down), form requests (authorize/rules), command handle |
| Repository | Not built in: Eloquent is Active Record. Teams add repositories on top when they need to hide the data source | Your own OrderRepository interface |
Three of these are easy to confuse:
- Facade vs Proxy: Laravel's facades are named after the Facade pattern because they give a simple, static-looking interface. How they work is a Proxy: a stand-in that forwards each call to the real service resolved from the container (which is why tests can swap it with
Mail::fake()orCache::shouldReceive(...)). - Adapter vs Strategy in drivers: each driver adapts a library (Redis, Memcached, Symfony Mailer) to Laravel's contract, and choosing a driver in config is the strategy.
- Chain of Responsibility, Decorator and Pipeline in middleware: a middleware can stop the request (chain), wraps the next one (decorator), and the stack is run by the
Pipelineclass.
The code
A miniature of how Laravel builds these features, in plain PHP: a container, a facade that proxies to a cache manager, two cache drivers chosen by config, model events, an Artisan-style command and a migration run by a template method.
<?php
// Service container (DI + singleton bindings)
class App {
private static array $bindings = [];
private static array $instances = [];
public static function singleton(string $name, callable $factory): void { self::$bindings[$name] = $factory; }
public static function make(string $name): object {
return self::$instances[$name] ??= (self::$bindings[$name])();
}
}
// Adapter + Strategy: drivers behind one contract, chosen by config
interface Store { public function get(string $key): ?string; public function put(string $key, string $value): void; }
class ArrayStore implements Store {
private array $items = [];
public function get(string $key): ?string { return $this->items[$key] ?? null; }
public function put(string $key, string $value): void { $this->items[$key] = $value; }
}
class NullStore implements Store {
public function get(string $key): ?string { return null; }
public function put(string $key, string $value): void {}
}
// Factory: the manager creates the configured driver
class CacheManager {
private ?Store $store = null;
public function __construct(private string $driver) {}
private function create(): Store {
return match ($this->driver) { "array" => new ArrayStore(), "null" => new NullStore() };
}
public function __call(string $method, array $args): mixed { // forward to the driver
$this->store ??= $this->create();
return $this->store->$method(...$args);
}
}
// Facade (a static Proxy): forwards static calls to the service in the container
abstract class Facade {
abstract protected static function accessor(): string;
public static function __callStatic(string $method, array $args): mixed {
return App::make(static::accessor())->$method(...$args);
}
}
class Cache extends Facade { protected static function accessor(): string { return "cache"; } }
// Observer: model events
class Model {
private static array $listeners = [];
public static function created(callable $listener): void { self::$listeners[static::class][] = $listener; }
public static function create(array $attributes): static {
$model = new static();
$model->attributes = $attributes;
foreach (self::$listeners[static::class] ?? [] as $listener) $listener($model);
return $model;
}
public array $attributes = [];
}
class User extends Model {}
// Command: each Artisan command is an object with handle()
abstract class Command {
abstract public function handle(): void;
}
class WarmCache extends Command {
public function handle(): void { Cache::put("greeting", "hello"); echo "cache warmed\n"; }
}
// Template Method: the migrator runs a fixed algorithm and calls your up()
abstract class Migration {
abstract protected function up(): string;
public function run(): void { echo "begin\n", " ", $this->up(), "\n", "commit, recorded in migrations table\n"; }
}
$migration = new class extends Migration {
protected function up(): string { return "create table users"; }
};
// Bootstrapping (what a service provider's register/boot would do)
App::singleton("cache", fn() => new CacheManager("array")); // CACHE_STORE=array
User::created(fn(User $u) => print("welcome email queued for {$u->attributes['email']}\n"));
$migration->run();
(new WarmCache())->handle();
echo "Cache::get: ", Cache::get("greeting") ?? "(miss)", "\n";
User::create(["email" => "[email protected]"]);
Output
begin create table users commit, recorded in migrations table cache warmed Cache::get: hello welcome email queued for [email protected]
Swap "array" for "null" and every Cache:: call changes behaviour without touching the command: that's the Strategy and the container working together.
Common mistakes
- Saying "Laravel uses the Repository pattern." It doesn't ship one; Eloquent models load and save themselves (Active Record). Repositories are an architectural choice you add.
- Treating facades as global static classes. They resolve a service from the container, so they can be faked in tests (
Mail::fake(),Queue::fake()) and swapped by binding a different implementation. - Putting work in a provider's
register. Only bind there; listeners, routes and anything that resolves other services go inboot. - Heavy logic in model observers. Hidden side effects on every
save()surprise teammates and slow bulk updates; prefer explicit events or jobs for important workflows. - Calling
app()everywhere. Ask for dependencies in constructors; using the container as a service locator hides what a class needs.
Key points
- Laravel is built from the patterns in this course: container and providers, MVC, facades as proxies, driver-based Adapter + Strategy via managers (Factory), the Builder in the query builder, Observer in model events, middleware as Chain of Responsibility + Decorator run by a Pipeline, Command in Artisan and jobs, and Template Method in migrations and form requests.
- Recognising the pattern tells you how to extend the feature: bind or extend a driver, add a middleware, write an observer or a command.
- Further reading: Laravel: Facades, Laravel: Eloquent events and Laravel: Middleware.
Practice questions
Are Laravel facades an example of the Facade pattern or the Proxy pattern?
Answer
Both, in different senses. The name comes from the Facade pattern: a simple, static-looking interface to a subsystem. The mechanism is a Proxy: the facade class forwards each static call (__callStatic) to the real object it resolves from the service container. That indirection is what lets tests swap the service with Mail::fake() or shouldReceive().
Which patterns are behind Laravel's cache, mail and queue drivers?
Answer
Each driver is an Adapter that wraps a specific library or service behind one Laravel contract (for example Illuminate\Contracts\Cache\Store). Choosing the driver in configuration is a Strategy, and the manager classes (CacheManager, MailManager) are Factories that create the configured driver; extend() registers custom ones.
Where does Laravel use the Template Method pattern?
Answer
Wherever the framework runs a fixed algorithm and calls methods you override: the migrator calls a migration's up()/down() inside its own bookkeeping, form requests have authorize() and rules() called by the validation flow, and Artisan calls a command's handle() after parsing input.
Open in the app to mark lessons complete, take notes and track your progress.