Laravel Facades: Icing on the Twitter cake | Clean Integrations Part 4
Published 24/12/2021 | 2154 viewsFacades are possibly the most misunderstood part of Laravel. Let's create one for our Twitter integration, and see how we can use it to improve our testing experience.
I have bled a simple Twitter integration dry. Over the course of several blog posts, we've covered a lot of ground:
- Integrating with external APIs
- Using doubles to make our test experience better
- Using a manager to handle switching out implementations based on our environment
There is one more thing I want to cover in this series: Facades. Perhaps the most misunderstood part of Laravel. Some will say you should avoid using Facades, that they're a code smell. I understand where they're coming from; when I'm able to use dependency injection, I prefer to. More often than not though, this gatekeeping comes from a lack of understanding as to what Facades actually are.
Let's take a deeper look at Facades, and create one for our Twitter integration to showcase another way of loading our fake implementation in tests.
What is a Facade?
A facade can be thought of as an intermediary layer between your application and the implementation stored in your app's service container. Take a look at the following code.
<?php namespace App\Jobs; use App\Contracts\Services\Twitter;use Illuminate\Bus\Queueable;use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Foundation\Bus\Dispatchable;use Illuminate\Queue\InteractsWithQueue;use Illuminate\Queue\SerializesModels;use Wink\WinkPost; final class SendTweetAboutPublishedPost implements ShouldQueue{ use Dispatchable; use InteractsWithQueue; use Queueable; use SerializesModels; public function __construct(private WinkPost $post) { } public function handle(Twitter $twitter): void { if ($this->postIsPublic() === false) { return; } $postUrl = route('posts.show', $this->post->slug); $twitter->tweet("New blog post available 📬: {$this->post->title}\n\n{$postUrl}"); } private function postIsPublic(): bool { if ($this->post->published === false) { return false; } if ($this->post->publish_date->isFuture()) { return false; } return true; }}
It should look quite familiar; it's the job from our previous posts. Note that we're injecting the Twitter
contract, or interface. This works well for our use-case, but can be confusing for beginners to the framework, particularly if they're coming from a background like WordPress which has no inherent concept of dependency inversion or injection.
One of Laravel's key aims with Facades is in fact to simplify things. Note the following quote from the docs:
Facades have many benefits. They provide a terse, memorable syntax that allows you to use Laravel's features without remembering long class names that must be injected or configured manually. Furthermore, because of their unique usage of PHP's dynamic methods, they are easy to test. [link]
Imagine if instead of worrying about injecting dependencies, we could send a tweet like this.
<?php namespace App\Jobs; use App\Facades\Twitter;use Illuminate\Bus\Queueable;use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Foundation\Bus\Dispatchable;use Illuminate\Queue\InteractsWithQueue;use Illuminate\Queue\SerializesModels;use Wink\WinkPost; final class SendTweetAboutPublishedPost implements ShouldQueue{ use Dispatchable; use InteractsWithQueue; use Queueable; use SerializesModels; public function __construct(private WinkPost $post) { } public function handle(): void { if ($this->postIsPublic() === false) { return; } $postUrl = route('posts.show', $this->post->slug); Twitter::tweet("New blog post available 📬: {$this->post->title}\n\n{$postUrl}"); } private function postIsPublic(): bool { if ($this->post->published === false) { return false; } if ($this->post->publish_date->isFuture()) { return false; } return true; }}
Looks nice, doesn't it? Before you go worrying about static methods (which are very difficult to test), remember that Facades are just a wrapper for the underlying contract and implementation, not the implementation itself. This means that they remain very easy to test, as we'll see, with the caveat of our test requiring the Laravel application to be booted (all feature tests in Laravel apps do this by default).
So, if this syntax seems like something you'd like in your application, how do we go about creating our own Facade?
Creating a Facade
Let's start by create a new file in our application: app/Facades/Twitter.php.
<?php namespace App\Facades; use Illuminate\Support\Facades\Facade; class Twitter extends Facade{ protected static function getFacadeAccessor() { }}
All Facades should override a method: getFacadeAccessor
. This essentially will tell Laravel which key to look for when loading our implementation from the service container. If you look at some examples of Facades in Laravel's own codebase, you'll see a plain string returned here. For example, Illuminate\Support\Facades\Cache
has the following signature.
<?php namespace Illuminate\Support\Facades; class Cache extends Facade{ protected static function getFacadeAccessor() { return 'cache'; }}
However, if we have bound our contract by its fully qualified classname or FQCN, in the container, which we have in the case of our Twitter
contract, then you can simply return that here instead. Let's update our Twitter
Facade with this information in mind.
<?php namespace App\Facades; use Illuminate\Support\Facades\Facade; class Twitter extends Facade{ protected static function getFacadeAccessor() { return \App\Contracts\Services\Twitter::class; }}
That's all you need to get started with a Facade! We can now update our SendTweetAboutPublishedPost
job to use our facade as shown in our earlier example.
<?php namespace App\Jobs; use App\Facades\Twitter;use Illuminate\Bus\Queueable;use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Foundation\Bus\Dispatchable;use Illuminate\Queue\InteractsWithQueue;use Illuminate\Queue\SerializesModels;use Wink\WinkPost; final class SendTweetAboutPublishedPost implements ShouldQueue{ use Dispatchable; use InteractsWithQueue; use Queueable; use SerializesModels; public function __construct(private WinkPost $post) { } public function handle(): void { if ($this->postIsPublic() === false) { return; } $postUrl = route('posts.show', $this->post->slug); Twitter::tweet("New blog post available 📬: {$this->post->title}\n\n{$postUrl}"); } private function postIsPublic(): bool { if ($this->post->published === false) { return false; } if ($this->post->publish_date->isFuture()) { return false; } return true; }}
Faking for tests
All of the tests we wrote in our previous blog post should still work. However, if you've ever used Laravel's built in facades, like Http
or Queue
, you'll know that they tend to ship with a static fake
method that streamlines switching out dependencies in tests. Well, now that we have the power of a Facade at our fingertips, we can actually go full-in on the "Laravel way" by adding a fake
method to our facade.
<?php namespace App\Facades; use Illuminate\Support\Facades\Facade;use Tests\Doubles\FakeTwitterClient; class Twitter extends Facade{ public static function fake(): void { static::swap(new FakeTwitterClient()); } protected static function getFacadeAccessor() { return \App\Contracts\Services\Twitter::class; }}
Our fake
method uses the static swap
found on all Facades to re-bind our Twitter
contract from its current implementation to our test double. We can now update our tests to make use of this.
<?php use App\Facades\Twitter;use App\Jobs\SendTweetAboutPublishedPost;use Database\Factories\WinkPostFactory;use Tests\Doubles\FakeTwitterClient; it('sends a tweet about the published post', function () { Twitter::fake(); $post = WinkPostFactory::new()->create(['title' => 'Getting started with Pest PHP']); $postUrl = route('posts.show', $post->slug); SendTweetAboutPublishedPost::dispatchSync($post); Twitter::assertTweeted(<<<TXT New blog post available 📬: Getting started with Pest PHP $postUrl TXT); });
I think this makes for a very clear test. It is immediately obvious from the start of our test that we're switching out the default implementation. We are given access to a much cleaner syntax, without having to manually retrieve anything from the service container.
This methodology also overcomes a potential issue. Notice that in our fake
method, we use the swap
method, rather than static::$app->bind()
or similar. Unlike bind
, swap
registers a singleton in the container. This is very important for our helper methods, such as assertTweeted
, because if our implementation is bound
(ie. it returns a new instance from the container every time), then we could have lost reference to the tweets stored in our fake implementation by the time we call our assertions. By using this fake
method on our Facade, we override the binding with a singleton, ensuring that when we call the Facade again, we're always referring to the same instance of the Twitter
contract.
Mix and match
If you prefer the improved testing syntax Facades offer, but would rather stick to dependency injection in your app code, there is no technical reason you cannot mix and match.
<?php namespace App\Jobs; use App\Contracts\Services\Twitter; use Illuminate\Bus\Queueable;use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Foundation\Bus\Dispatchable;use Illuminate\Queue\InteractsWithQueue;use Illuminate\Queue\SerializesModels;use Wink\WinkPost; final class SendTweetAboutPublishedPost implements ShouldQueue{ use Dispatchable; use InteractsWithQueue; use Queueable; use SerializesModels; public function __construct(private WinkPost $post) { } public function handle(Twitter $twitter): void { if ($this->postIsPublic() === false) { return; } $postUrl = route('posts.show', $this->post->slug); $twitter->tweet("New blog post available 📬: {$this->post->title}\n\n{$postUrl}"); } private function postIsPublic(): bool { if ($this->post->published === false) { return false; } if ($this->post->publish_date->isFuture()) { return false; } return true; }}
<?php use App\Facades\Twitter; use App\Jobs\SendTweetAboutPublishedPost;use Database\Factories\WinkPostFactory;use Tests\Doubles\FakeTwitterClient; it('sends a tweet about the published post', function () { Twitter::fake(); $post = WinkPostFactory::new()->create(['title' => 'Getting started with Pest PHP']); $postUrl = route('posts.show', $post->slug); SendTweetAboutPublishedPost::dispatchSync($post); Twitter::assertTweeted(<<<TXT New blog post available 📬: Getting started with Pest PHP $postUrl TXT); });
Even though the SendTweetAboutPublishedPost
job injects our Twitter
contract and our test uses the Twitter
facade, everything will work as it did before because they both still reference the same object in the service container. Facades aren't black magic; they're just static wrappers around injectable objects.
I have actually mixed and matched like this numerous times in various applications I've developed. For example, I will quite often inject the Illuminate\Contracts\Events\Dispatcher
contract to fire an event in my application code, whilst using Illuminate\Support\Facades\Event::fake
to prevent any such event actually being fired in my tests.
Final touches
In order to provide accurate IDE autocompletion when working with Facades, it's common practice to add static method docblocks to your Facade. Let's update our Twitter
Facade to support this.
<?php namespace App\Facades; use Illuminate\Support\Facades\Facade; /** * @method static tweet(string $message): void * @method static assertTweeted(string $message): void * @method static assertNothingTweeted(): void */class Twitter extends Facade{ public static function fake(): void { static::swap(new FakeTwitterClient()); } protected static function getFacadeAccessor() { return \App\Contracts\Services\Twitter::class; }}
Note that we provide autocomplete for the assertTweeted
and assertNothingTweeted
methods. Bear in mind that these methods will only be available if you've previously called Twitter::fake()
at the beginning of your test.
Conclusion
I hope that this post has enlightened you on the use of Facades. They're a powerful tool that can help simplify your code, particularly when writing tests. They do come with a warning out of the box though.
However, some care must be taken when using facades. The primary danger of facades is class "scope creep". Since facades are so easy to use and do not require injection, it can be easy to let your classes continue to grow and use many facades in a single class. Using dependency injection, this potential is mitigated by the visual feedback a large constructor gives you that your class is growing too large. So, when using facades, pay special attention to the size of your class so that its scope of responsibility stays narrow. If your class is getting too large, consider splitting it into multiple smaller classes. [link]
Remember, however, that any code used incorrectly can cause more harm than good. So, use Facades carefully and make sure to review your code for potential god-classes. Used correctly, they're invaluable and offer a beautiful developer experience, along with removing lots boilerplate code for frequently used services.
Thanks for taking the time to read my post!
Kind Regards, Luke
Edit
After writing this, Muhammed Sari pointed out that Facades have a ::swap
method for quickly switching out the implementation. I've updated the post to reflect this. Thanks Muhammed!