My favorite! The Manager-Facade-Driver pattern is something you see throughout Laravel, in caching, sessions, queuing, you name it. And once you understand it, you'll be able to apply it to your own code.
The basic idea: you want to support multiple implementations of the same thing. Different license providers. Different payment processors. Different storage backends. Instead of scattering conditional logic all over your codebase, you create an extensible system.
Here's what this actually means in practice: you can support Statamic CMS today and switch to Filament or October CMS later with just a new driver, no existing code changes. Adding a new implementation requires zero modifications to your existing system. Your application grows by addition, not modification. That's powerful.
Beyond just flexibility, this pattern gives you clean, decoupled code. Your controller doesn't care which provider is actually handling the request. It just calls a consistent interface. You mock the entire driver for tests without touching your production code. The business logic stays pure and testable, separated from the implementation details of any specific provider.
Step 1: Define the Contract (Interface)
Before you write a single driver, you need to agree on what a driver looks like. That's what an interface is for. It's a contract, a promise that says "any license provider that implements this interface will have these exact methods available with these exact signatures." You're not writing the implementation yet, you're just defining the shape. You're saying "here are the three essential operations a license provider must support: listing licenses, adding them, and deleting them." No implementation details, just the blueprint that all your drivers must follow.
In this API, we're establishing that every license provider must support three core operations. First, licenses() returns a collection of all available licenses. Second, addLicense() creates a new license with a name and domain. Third, deleteLicense() removes a license by its key. If a provider can't do these three things, it doesn't belong implementing this interface. That's the whole point, the interface enforces that minimum contract.
// app/Services/License/LicenseContract.php
interface LicenseContract
{
public function licenses(): Collection;
public function addLicense(string $name, string $domain): array;
public function deleteLicense(string $key): ?array;
}
Here's the genius of this approach: your application doesn't care which provider you're actually using. It doesn't need to know if you're talking to Statamic, Filament, October CMS, or a mock object in your tests. Your code just says "give me something that implements LicenseContract" and then calls the methods it knows will be there. That's polymorphism, the ability to work with many different implementations through a single interface. It's the foundation of flexible, testable, maintainable code.
Step 2: Create Specific Implementations (Drivers)
This step is where the contract becomes real. The interface defines what must be done, but each provider has its own API shape, auth scheme, endpoints, and quirks. A driver isolates those details in one place so the rest of your codebase never has to care. Instead of littering controllers and services with conditionals like “if Statamic, call /sites; if Filament, call /licenses,” you keep the logic focused: one driver per provider, one responsibility per class. That means swapping providers is additive, testing is straightforward (mock the driver), and your business logic stays clean and consistent.
// app/Http/Integrations/StatamicAPI.php
final readonly class StatamicAPI implements LicenseContract
{
use SendsRequests;
public function __construct(
protected PendingRequest $request,
) {}
public function licenses(): Collection
{
return $this->send('GET', '/sites')->collect('data');
}
public function addLicense(string $name, string $domain): array
{
return $this->send('POST', '/sites', [
'name' => trim($name),
'domain' => trim($domain),
])->json('data') ?? [];
}
public function deleteLicense(string $key): ?array
{
return $this->send('DELETE', "/sites/{$key}")->json('data');
}
}
Now you have a concrete driver that implements the interface. The StatamicAPI class knows exactly how to talk to Statamic's REST API. It handles authentication, knows which endpoints to call, knows how to parse responses, and transforms everything into the consistent format that the rest of your application expects. It's a fully self-contained implementation of the contract. Someone reading this code can immediately understand how Statamic integration works without having to hunt through conditional logic scattered across your codebase.
We inject a PendingRequest into the constructor. This is Laravel's HTTP client, pre-configured with authentication. Injecting it instead of hardcoding HTTP calls lets you swap in a mock for tests. Your tests run instantly without making real network calls, and you control exactly what responses the driver receives.
Separate drivers exist because different providers have wildly different APIs. Statamic might use /sites endpoints with token authentication. Another CMS might use /licenses with OAuth. A third might use GraphQL. By separating them, each driver only cares about its own provider. No if-statements checking which provider you're using. No conditionals littering the code. Just one driver, one provider, one focused implementation.
// app/Http/Integrations/Concerns/SendsRequests.php
trait SendsRequests
{
protected function send(
string $method,
string $url,
array $data = []): Response
{
$response = match ($method) {
'GET' => $this->request->get($url, $data),
'POST' => $this->request->post($url, $data),
'PUT' => $this->request->put($url, $data),
'DELETE' => $this->request->delete($url, $data),
default => throw new Exception(
message: 'Invalid method provided.',
),
};
if ($response->failed()) {
throw new RequestException(
response: $response,
);
}
return $response;
}
}
The driver delegates HTTP communication to a SendsRequests trait, which handles the actual network calls, error handling, and response parsing in one place. This means every driver that uses it gets the same error handling and request/response lifecycle. It's reusable infrastructure that keeps drivers focused on what matters: translating API responses into the interface your application expects.
Step 3: Create the Manager (Factory + Router)
Here's where the extensibility really pays off. The Manager is an intelligent factory that knows how to create drivers on demand. It's also a router that knows which driver to use and when. When your application says "get me a license provider," the Manager says "One moment, let me check what you need and create it for you." It's the orchestrator between your code and the specific implementations.
// app/Services/License/LicenseManager.php
class LicenseManager extends Manager
{
protected function createStatamicDriver(): LicenseContract
{
return new StatamicAPI(
Http::withToken(config('services.license.driver.statamic.token')),
);
}
public function getDefaultDriver(): string
{
return config('services.license.default');
}
public function driver(string $driver = null): LicenseContract
{
return parent::driver($driver);
}
}
The createStatamicDriver() method is where the driver is constructed. Laravel's Manager class uses convention: when you ask for the "statamic" driver, it automatically calls createStatamicDriver(). No registration needed, no mapping files. Just follow the naming pattern and Laravel finds it. We fetch the API token from configuration and pass a pre-configured HTTP client to the driver. The driver is ready to use immediately.
The getDefaultDriver() method returns which driver to use if you don't explicitly ask for another one. So when you call LicenseFacade::licenses() without specifying, the Manager looks at your config and says "okay, the default is Statamic, I'll use that." This keeps your application flexible, you can change the default driver just by changing an environment variable.
Notice we're pulling credentials from config('services.statamic.token'), not hardcoding them. In production that comes from a real API token in your environment. In testing that comes from a test token. The Manager passes configuration to the driver but never knows or cares where it came from. That separation is crucial for flexibility.
Underneath all this sits Laravel's base Manager class, which handles caching driver instances so you don't create new ones repeatedly, routing between drivers if you have multiple, and calling your custom creator methods. You don't have to implement all that plumbing, you just provide the createXxxDriver() method and trust that Laravel handles the rest. That's the power of using inheritance and convention.
Step 4: Create the Facade (Easy Access)
// app/Services/License/LicenseFacade.php
class LicenseFacade extends Facade
{
protected static function getFacadeAccessor(): string
{
return LicenseManager::class;
}
}
A Facade is a convenient shorthand. Instead of reaching into the service container and calling methods on the Manager, you can just call static methods on the Facade. It also makes the code more readable. Compare these:
// Without facade - verbose
app(LicenseManager::class)->driver()->licenses();
// With facade - concise
LicenseFacade::licenses();
The Facade is just syntactic sugar. Under the hood, LicenseFacade::licenses() calls app(LicenseManager::class)->licenses(). But it reads better in code.
getFacadeAccessor(): This method tells Laravel "when someone calls a method on this Facade, forward it to the LicenseManager instance in the service container." It's the connection between the Facade and the actual Manager.
Static calls are allowed: Facades allow you to call methods statically even though they're actually instance methods on the Manager. This is done through PHP's __callStatic() magic method. It's wonderfully convenient.
Step 5: Register in the Service Provider
Registering the Manager in a service provider is required because Laravel does not automatically know how to construct your custom Manager class. When your controller needs a LicenseManager, Laravel must know how to create it. The service container manages instantiation and lifecycle, so you register the Manager there. Without registration, Laravel cannot resolve the class for injection or use. The service provider is the bootstrap mechanism that ensures your Manager exists and is ready before the first request.
For this API, we use a dedicated LicenseServiceProvider to keep concerns organized. This is cleaner than dumping everything in the AppServiceProvider:
// app/Providers/LicenseServiceProvider.php
final class LicenseServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(LicenseManager::class);
}
public function provides(): array
{
return [LicenseManager::class];
}
}
Then register it in bootstrap/providers.php:
// bootstrap/providers.php
return [
\App\Providers\AppServiceProvider::class,
\App\Providers\LicenseServiceProvider::class,
];
Register behavior. The register() method tells Laravel's container "when someone asks for LicenseManager, create an instance and keep it forever."
Singleton behavior. singleton() means Laravel creates the Manager once and reuses the same instance everywhere. This is important because the Manager caches driver instances. If you created a new Manager each time, you'd lose the cache.
The provides() method: This tells Laravel what services this provider offers. It's optional but helpful for debugging and optimization, Laravel knows what to expect from this provider.
Separate provider. By creating a dedicated LicenseServiceProvider, we keep the license system organized. As your API grows and you need multiple managers (payment providers, notification services, etc.), each gets its own provider. This keeps each provider focused and testable.
Execution timing. Providers are registered when the application boots. The register() method is called before anything else uses the Manager, ensuring it's ready to go.
Step 6: Use in Your Controller
This is where everything comes together. Your controller is simple because the work happens elsewhere. Each method has one job: take the request, use the facade to get data, transform it if needed, and return a response.
Look at the show() method. It calls LicenseFacade::licenses() and gets back data. The mapper transforms it to typed DTOs. The Resource transforms those to JSON. The Response Wrapper packages it. Five lines that are crystal clear about what's happening. No switch statements figuring out which driver to use. No if-statements checking configurations. No API calls buried in the logic. Just clean, straightforward code that reads like documentation for itself.
// app/Http/Controllers/LicenseController.php
public function show(): Responsable
{
return new CollectionResponse(
data: LicenseResource::collection(
resource: LicenseDTOMapper::toDTOCollection(
licenses: LicenseFacade::licenses(),
),
)
);
}
Your controller never has to worry about juggling arrays or missing keys. The controller only focuses on orchestration. Here's what actually happens when a request comes in:
1. GET /api/licenses arrives
2. Controller's show() method runs
3. LicenseFacade::licenses() calls external API
Returns: ['key' => 'lic_123', 'name' => 'License', ...]
4. LicenseDTOMapper::toDTOCollection() converts it
Returns: [LicenseDTO(...), LicenseDTO(...), ...]
Type: array of LicenseDTO
5. LicenseResource::collection() transforms each DTO
Only exposes whitelisted fields, computes values
6. CollectionResponse packages it
Adds status code, headers, JSON structure
7. Client gets:
{
"items": [
{ "key": "lic_123", "name": "License", "domains": ["example.com"], ... }
]
}
Each step and each transformation is explicit and testable.
The create() method is equally obvious. It receives a validated request (thanks FormRequest), passes the data to the facade, maps and transforms the result, and returns it with a 201 status code. One request, one transformation, one response. A junior developer reads this once and understands it completely.
// Good: Thin controller delegating work
public function create(CreateLicenseRequest $request): Responsable
{
return new ModelResponse(
data: new LicenseResource(
resource: LicenseDTOMapper::toDTO(
LicenseFacade::addLicense(
name: $request->validated('name'),
domain: $request->validated('domain'),
)
),
),
status: Response::HTTP_CREATED, //201
);
}
See the pattern? Every method is a straight pipeline from input to output: responsable → resource → mapper → facade.
The destroy() method does something interesting, it defers the actual deletion to happen in the background and immediately returns a 202 Accepted response. This is how you handle async operations gracefully. The client knows the deletion is queued and will happen soon, and they don't have to wait for the external API to respond.
The HTTP status code becomes important here. Notice that we're explicitly passing the status parameter as Response::HTTP_ACCEPTED (or 202). This is necessary because MessageResponse defaults to HTTP 200, and we need a different status code to signal that the operation is being processed asynchronously rather than completed immediately.
// app/Http/Controllers/LicenseController.php
public function destroy(string $key): Responsable
{
defer(
callback: fn () => LicenseFacade::deleteLicense(
key: $key,
),
);
return new MessageResponse(
message: Lang::get('messages.delete_queued'),
status: Response::HTTP_ACCEPTED, //202
);
}
Return HTTP 202 (Accepted) to signal "I received this, it's being processed." Return HTTP 200 (OK) to signal "Here's your confirmation." Your API consumers read the status code and understand the contract. They know 202 means "wait for a webhook or poll for status." They know 200 means "it's done." This clarity reduces confusion and improves the user experience.
There's something remarkable about these three methods: they contain not a single $ variable. No intermediate assignments. No temporary holding places for data in transit. The logic flows directly from input to output, method calls nested inside method calls, each return value piped straight into the next parameter.
This pattern is possible because named arguments and dependency injection work beautifully together. Because each layer returns exactly what the next layer needs, you don't need temporary variables to hold results between steps. Each variable is another thing your brain has to track and that creates cognitive load.
When you compose the calls directly, you eliminate that burden.
You read it top-to-bottom, and the data flow is obvious. No intermediate state to track. No mutations hiding in variables. The data flows through the pipeline, each step transforming it, nothing modified after creation. This immutable pipeline style makes the code harder to get wrong. You can't accidentally reuse a variable. You can't put something in a variable, forget about it, and accidentally pass the old value. Each value exists exactly once, travels through exactly one path, and gets consumed exactly once.
The absence of $ isn't a quirk, it's a architectural signal. It indicates that each component is doing one thing well, transforming input to output with no hidden state or side effects. It means testing is simpler because you know nothing unexpected can happen. It means refactoring is safer because there are no state dependencies to break.
Here's what makes this genuinely powerful: your controller doesn't know which provider it's using. It doesn't care if you're talking to Statamic, Filament, or a mock. It just calls the facade. The facade knows which driver to use based on configuration. Everything underneath is completely decoupled from your HTTP layer, which means in tests you can inject a mock driver and your controller behaves exactly the same way. You're testing controller logic and response formatting without making real API calls or dealing with network latency. That's when you know your architecture is working.
Step 7: Switch Providers via Configuration
Here's where all the architecture pays off. Your .env file has a single setting:
// .env file
LICENSE_DRIVER=statamic
Change that one line and the whole application switches providers. One environment variable controls it. No code changes, no controller redeploys. Update .env and you’re done. Your team doesn’t have to touch the Controller, the Facade, or the Manager registration.
// config/services.php
'license' => [
'default' => env('LICENSE_DRIVER', 'statamic'),
'drivers' => [
'statamic' => [
'url' => env('STATAMIC_API_URL', 'https://statamic.com/api/v1'),
'token' => env('STATAMIC_API_TOKEN', null),
],
],
],
Think about what this actually means. In development, you set LICENSE_DRIVER=local and get instant mock responses without any API calls. Your tests run fast, your feedback loop is tight. In staging, you flip to LICENSE_DRIVER=test and hit the test environment. In production, it's LICENSE_DRIVER=statamic with production credentials from your environment variables.
Your controller code doesn't change. Your service provider doesn't change. Your entire application looks identical across all three environments. The only difference is a single line in .env. That's the entire point of this architecture.
If you need to support a new provider by next week, write a new driver class, create a createNewProviderDriver() method in the Manager, and change .env. No if-statements scattered through your code. No conditional checks sprinkled across your controllers. No risk of accidentally using the wrong provider in the wrong place.
When you need to migrate license providers on a deadline, you avoid refactoring the entire codebase. You write one focused driver class and change one environment variable. Everything keeps working, tests keep passing, and the API stays live.
Putting It All Together
HTTP Request (e.g., POST /api/license with Bearer token)
↓
Router (routes/api.php matches the request)
↓
Controller Action (LicenseController::create)
↓
FormRequest Validation (CreateLicenseRequest checks input)
↓
Service/Facade (LicenseFacade calls API or database)
↓
External API or Database (fetch actual data)
↓
Mapper (convert to DTO)
↓
Resource (DTO → JSON)
↓
Response Wrapper (format in standard structure)
↓
JSON Response to Client
Each layer has a single responsibility. Each layer is testable. Each layer is replaceable. This is the foundation that everything else builds on.