For one reason or another, Laravel Facades don't get much love. I often read about how they are not a true facade implementation, and let me tell you, they're not 🤷. They're more like proxies than facades. If you think about it, they simply forward calls to their respective classes, effectively intercepting the request. When you start looking at them this way, you realize that facades, when used correctly, can result in a clean and testable code. So, don't get too caught up in the naming, I mean who cares what they are called? And let's see how we can make use of them.
When writing service classes, I'm not a fan of using static methods, they make testing dependent classes HARD. However, I do love the clean calls they offer, like Service::action()
. With Laravel real-time facades, we can achieve this.
Let's take a look at this example:
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use App\Exceptions\CouldNotFetchRates;
use App\Entities\ExchangeRate;
class ECBExchangeRateService
{
public static function getRatesFromApi(): ExchangeRate
{
$response = Http::get('ecb_url'); // Avoid hardcoding URLs, this is just an example
throw_if($response->failed(), CouldNotFetchRates::apiTimeout('ecb'));
return ExchangeRate::from($response->body());
}
}
We have a hyper-simplified service class that attempts to retrieve exchange rates from an API and returns a DTO (Entity, or whatever makes you happy) if everything goes well.
Now we can use this service like so:
<?php
namespace App\Classes;
use App\Services\ECBExchangeRateService;
class AnotherClass
{
public function action(): void
{
$rates = ECBExchangeRateService::getRatesFromApi();
// Do something with the rates
}
}
The code might look clean, but it's not good; it's not testable. We can write feature tests or integration tests, but when it comes to unit testing this class, we can't. There's no way to mock ECBExchangeRateService::getRatesFromApi()
, and unit tests should not have any dependencies (interactions with different classes or systems).
Since we are discussing unit tests, I want to emphasize that should not doesn't mean you don't have to. Sometimes it makes sense to have database interaction in unit tests, for example, to test whether or not a relationship is loaded 🤷. Don't follow the rules blindly; sometimes they make sense, sometimes they don't.
So, to fix this, we need to follow some steps:
getRatesFromApi()
into a regular one;ECBExchangeRateService
(optional);One might argue that this is the correct way to do things, but I'm a very simple guy. I feel that this is an overkill, especially if I know that I won't be changing any implementations for a very long time.
I mean, I literally added the autolink to headers today, so I can link only the real-time section of my article. That's how much I want to keep things simple 😂
With real-time facades, we can turn the 4 steps into 2:
getRatesFromApi()
into a regular one (just remove the static
keyword);Facades
keyword.Your code should look like:
<?php
namespace App\Classes;
use Facades\App\Services\ECBExchangeRateService; // The only change we need
class AnotherClass
{
public function action(): void
{
$rates = ECBExchangeRateService::getRatesFromApi();
// do something with the rates
}
}
That's all we needed to do! Removed 1 keyword, and added another. You can't beat this!
Here is how we can test our code now:
it('does something with the rates', function () {
ECBExchangeRateService::shouldReceive('getRatesFromApi')->once();
(new AnotherClass)->action();
});
I am using Pest.
The ECBExchangeRateService
will be resolved from the container, just as we would do in the 4 steps above, without the need to create extra interfaces or add more code. We maintain our clean, simple approach and ensure testability. And I know some people still won't agree, dismissing it as dark magic. Well, it's not really magic if it is in the docs; read your docs kids!
Remember what I mentioned about thinking of facades as proxies? Let's explain it.
When using Laravel Queues, we dispatch jobs in our code. When you're testing that code, you're not interested in testing if the actual job is working as expected or not; that can be tested separately. Instead, you're interested in whether or not the job has been dispatched, the number of times it's been dispatched, the payload used, etc. So, to achieve this, we would need two implementations, right? Dispatcher
and DispatcherFake
- one that actually dispatches the job to Redis, MySQL, or whatever you've set it for, and the second one that does not dispatch anything, but rather captures those events.
If we were to implement this ourselves, we would need to follow the 4 steps from earlier, and change the bindings of these implementations depending on the context - if we are running tests or if we are running the actual code. Now, Facades make this much simpler, like really simple. Let's see how.
Let's first define our interface:
<?php
namespace App\Contracts;
interface Dispatcher
{
public function dispatch(mixed $job, mixed $handler): mixed
}
Then, we can have two implementations:
<?php
namespace App\Bus;
use PHPUnit\Framework\Assert;
use App\Contracts\Dispatcher as DispatcherContract;
class Dispatcher implements DispatcherContract
{
public function dispatch(mixed $job, mixed $handler): mixed
{
// Actually dispatch this to the DB or whatever driver is set
}
}
class DispatcherFake implements DispatcherContract
{
protected $jobs = [];
public function dispatch(mixed $job, mixed $handler): mixed
{
// We are just recording the dispatches here
$this->jobs[$job] = $handler;
}
// We can add testing helpers
public function assertDispatched(mixed $job)
{
Assert::assertTrue(count($this->jobs[$job]) > 0);
}
public function assertDispatchedTimes(mixed $job, int $times = 1)
{
Assert::assertTrue(count($this->jobs[$job]) === $times);
}
// ... and more methods
}
Now, instead of resolving implementations manually and having to bind multiple ones, we can make use of facades. They intercept the call, and we can choose where we want to forward it!
<?php
namespace App\Facades;
use App\Bus\DispatcherFake;
use Illuminate\Support\Facades\Facade;
class Dispatcher extends Facade
{
protected static function fake()
{
return tap(new DispatcherFake(), function ($fake) {
// This will set the $resolvedInstance to the faked one
// So every time we try to access the underlying
// implementation, the faked object will be returned instead
static::swap($fake);
});
}
protected static function getFacadeAccessor()
{
return 'dispatcher';
}
}
Interested in learning how Facades work under the hood? I've written an article about it.
Now we can simply bind our dispatcher to the application container.
<?php
namespace App\Providers;
use App\Bus\Dispatcher;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind('dispatcher', function ($app) {
return new Dispatcher;
});
}
// ...
}
And that's it! We can now elegantly swap between implementations, and our code is testable with clean calls, and no injections (but with the same effect).
use App\Facades\Dispatcher; // import the facade
it('does dispatches a job', function () {
// This will set the fake implementation as the resolved object
Dispatcher::fake();
// An action that dispatches a job `Dispatcher::dispatch(Job::class, Handler::class);
(new Action)->handle();
// Now you can assert that it has been dispatched
Dispatcher::assertDispatched(Job::class);
});
This is a hyper-simplified example, just to see things from a new perspective. Interestingly, this is how your favorite framework tests things internally. So, facades might not be as much of an anti-pattern as you might think. They might be named incorrectly, but you can see how they simplify things.
Here's a valuable point to consider that might save you a few hours of debugging 🧠.
First, let's have a look at the swap
method
/**
* Hotswap the underlying instance behind the facade.
*
* @param mixed $instance
* @return void
*/
public static function swap($instance)
{
static::$resolvedInstance[static::getFacadeAccessor()] = $instance;
if (isset(static::$app)) {
static::$app->instance(static::getFacadeAccessor(), $instance); // Binding the faked instance to the container
}
}
You can see that when swapping instances, we're not just replacing the cached resolved instance; we're also binding it to the container. This implies that ALL future returned instances, whether using dependency injection or the app()
helper, will yield a fake implementation. So, if you're running integration tests, where some classes use constructor injection and not facades, they will also receive the faked implementation. Nevertheless, if you ever call the fake()
method, you would expect to receive the fake implementation everywhere anyway. However, it's something to bear in mind when writing tests (especially integration tests), where you might want to use the actual class instead.
Don't fight the framework; embrace it and try to make use of what already exists. There are multiple approaches to each problem, and they can all be good. Don't dismiss something just because someone else thinks otherwise; give it a chance. To me, as long as the code is testable, you're on the right path, you shouldn't worry too much about whether or not it follows certain rules.