Laravel Facades: Icing on the Twitter cake | Clean Integrations Part 4

Published 24/12/2021 | 1972 views

Facades 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.

Facades are actually incredibly simple to create!

I have bled a simple Twitter integration dry. Over the course of several blog posts, we've covered a lot of ground:

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!

Like what you see?

If you enjoy reading my content, please consider sponsoring me. I don't spend it on cups of coffee; it all goes towards freeing up more of my time to work on open source, tutorials and more posts like this one.