Reaching for Facades

Published on by

Reaching for Facades image

Facades, people seem to love them or hate them. Either way, they are a natural part of what Laravel is today. Laravel Facades, however, aren't strictly facades; are they? Instead, they are static accessors to resolve a class from the container.

When I first started with Laravel, I hated it, and after three years of using Larvel - I finally started to accept it. I was your typical Laravel hater of today. It was when I learned to embrace Laravel for what it is, instead of trying to fight the framework of my way of thinking, that I learned to love it. Some might say I am one of its biggest advocates now.

One of the things I hated for a time was Facades. I was in the grumpy camp complaining about static method calls, blah blah blah. But I didn't know how they worked in Laravel. I was joining in with the noise repeating what other developers had said without knowing what I was talking about.

Fast forward to today, and I understand how Facades work - and you know what? I have definitely changed my tune. I want to write this tutorial not so that you can all agree with me, although you should, but so that you, too, can understand how facades work and where they have their benefits.

This isn't strictly a tutorial, as I will be walking you through existing code that I have written, whereas I usually would write the code as I write the tutorial so I can explain the natural refactoring points. The code I am going to walk you through is the Get Send Stack Laravel package that you can find on GitHub here.

While building this package, I did what I usually do and started building an API integration using the HTTP Facade - using an interface/contract to leverage the DI container to inject the instance when needed. Let me take you through these stages of code. We will start with not using the DI container first.

class AddSubscriberController
{
public function __invoke(AddSubscriberRequest $request)
{
$client = new Client(
url: strval(config('services.sendstack.url')),
token: strval(config('services.sendstack.token')),
);
 
try {
$subscriber = $client->subscribers()->create(
request: new SubscriberRequest(
email: $request->get('email'),
firstName: $request->get('first_name'),
lastName: $request->get('last_name'),
optIn: $request->get('opt_in'),
),
);
} catch (Throwable $exception) {
throw new FailedToSubscribeException(
message: $exception->getMessage(),
previous: $exception,
);
}
 
// return redirect or response.
}
}

So this is a 'little' long-winded, but it is clear to see what you are doing. You create the client, send the request through the client, and catch a potential exception. Finally, returning either a redirect or a response depending if it were an API or web controller. This code isn't bad. You can test it. You can ensure behavior within the controller with little effort.

However, if the package changed how you integrated with it, everywhere you were newing up the client to work with the API, you would have to go through your code base and make all the changes as required. This is a perfect time to look at refactoring, as you are saving yourself future work by working smarter. Using the DI container directly, let's look at a refactored version of the above code.

class AddSubscriberController
{
public function __construct(
private readonly ClientContract $client,
) {}
 
public function __invoke(AddSubscriberRequest $request)
{
try {
$subscriber = $this->client->subscribers()->create(
request: new SubscriberRequest(
email: $request->get('email'),
firstName: $request->get('first_name'),
lastName: $request->get('last_name'),
optIn: $request->get('opt_in'),
),
);
} catch (Throwable $exception) {
throw new FailedToSubscribeException(
message: $exception->getMessage(),
previous: $exception,
);
}
 
// return redirect or response.
}
}

Cleaner and more manageable now, we are injecting from the DI container the contract/interface, which will resolve the client for us - as the package service provider has detailed the instructions on how to build the client. There is nothing wrong with this approach; it is a pattern I use heavily in my code. I can replace the implementation to get a different result and still use the same package API as I am using the interface/contract. But again, while I am using the container - am I fighting the framework? One of the things many of us like about Laravel is its developer experience, and we can thank Eloquent a lot for that. We don't have to mess around with juggling the container to create a new model or anything like that. We are very used to statically calling what we want when we want. So let's look at the above example using the Facade I created with the package.

class AddSubscriberController
{
public function __invoke(AddSubscriberRequest $request)
{
try {
$subscriber = SendStack::subscribers()->create(
request: new SubscriberRequest(
email: $request->get('email'),
firstName: $request->get('first_name'),
lastName: $request->get('last_name'),
optIn: $request->get('opt_in'),
),
);
} catch (Throwable $exception) {
throw new FailedToSubscribeException(
message: $exception->getMessage(),
previous: $exception,
);
}
 
// return redirect or response.
}
}

No more containers to worry about - and we are getting back that familiar Laravel feeling that we were missing. The upside here is that the developer experience is straightforward, the implementation looks clean, and we achieve the same result. What are the downsides? Because there are some, of course. The only downside is that you cannot switch implementation as the Facade is static to its implementation. But in my experience, moving from Provider A to Provider B when you are talking external services is more complex than creating and binding a new implementation to the container. People who always bang this drum look at the problem with a narrow ideological scope. In reality, changing providers is a considerable effort, not just from a code perspective - so there is always enough time to focus on implementing something different where you need to. Sometimes the new provider has something the older one doesn't. Maybe you have to send additional data through in your requests etc.

My point here is that while the SOLID principles are great, and you should look to them for advice - they are often an unrealistic dream that aren't going to work in practice, or you are going to spend so long writing the feature that the scope changes before you have finished. Fighting the framework at every turn does not help you build good products. You create good products by accepting less than perfect and acknowledging change may be required.

How does this relate to Facades? as you can see from the code examples, Facades make it easier in many ways. Neither way is incorrect, and neither way is correct. A Facade will allow a friendlier implementation but will force you down a particular path. Using the container will allow you more flexibility moving forward, but it isn't a magic bullet and comes with its own risks. Simply newing up instances when you need them is easy, but also lazy when there are better ways to achieve the same result.

What does a Facade actually look like? Here is the exact code from the package.

declare(strict_types=1);
 
namespace SendStack\Laravel\Facades;
 
use Illuminate\Support\Facades\Facade;
use SendStack\Laravel\Contracts\ClientContract;
use SendStack\Laravel\Http\Resources\SubscribersResource;
use SendStack\Laravel\Http\Resources\TagResource;
 
/**
* @method static SubscribersResource subscribers()
* @method static TagResource tags()
* @method static bool isActiveSubscriber(string $email)
*
* @see ClientContract
*/
class SendStack extends Facade
{
protected static function getFacadeAccessor()
{
return ClientContract::class;
}
}

It has a protected static method to get the class it needs to construct and build, and the class we are extending will forward all static calls into this class once resolved from the container. People talk about it like it is a dirty word, but in reality, it is no different from creating a container alias, really, other than the syntax. In my example, I have added docblocks for the methods on the implementation/interface to allow better IDE completion - but this is just an extra step I like to take.

The moral of this story is that Facades are not evil and can actually be very helpful - so ignore the haters and embrace them as I have. You will be happier for it and will be a lot more productive.

Steve McDougall photo

Technical writer at Laravel News, Developer Advocate at Treblle. API specialist, veteran PHP/Laravel engineer. YouTube livestreamer.

Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Lucky Media logo

Lucky Media

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo

LaraJobs

The official Laravel job board

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate logo

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate

Build your SaaS application in hours. Out-of-the-box multi-tenancy and seamless Stripe integration. Supports subscriptions and one-time purchases, allowing you to focus on building and creating without repetitive setup tasks.

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector
MongoDB logo

MongoDB

Enhance your PHP applications with the powerful integration of MongoDB and Laravel, empowering developers to build applications with ease and efficiency. Support transactional, search, analytics and mobile use cases while using the familiar Eloquent APIs. Discover how MongoDB's flexible, modern database can transform your Laravel applications.

MongoDB

The latest

View all →
Asymmetric Property Visibility in PHP 8.4 image

Asymmetric Property Visibility in PHP 8.4

Read article
Access Laravel Pulse Data as a JSON API image

Access Laravel Pulse Data as a JSON API

Read article
Laravel Forge adds Statamic Integration image

Laravel Forge adds Statamic Integration

Read article
Transform Data into Type-safe DTOs with this PHP Package image

Transform Data into Type-safe DTOs with this PHP Package

Read article
PHPxWorld - The resurgence of PHP meet-ups with Chris Morrell image

PHPxWorld - The resurgence of PHP meet-ups with Chris Morrell

Read article
Herd Executable Support and Pest 3 Mutation Testing in PhpStorm 2024.3 image

Herd Executable Support and Pest 3 Mutation Testing in PhpStorm 2024.3

Read article