Why I use global helpers instead of facades

By @samuel · 2021-11-19 17:47 (edited)

Why I use global helpers instead of facades

When you want to provide easy access to a class that's bound in the service container, the traditional approach is creating a facade:

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton(SEOManager::class);
    }
}

class SEO extends Facade
{
     protected static function getFacadeAccessor()
     {
         return SEOManager::class;
     }
}

// The actual class
class SEOManager { ... }

Which lets you use syntax like this:

SEO::title('Laravel | ArchTech Forum');
SEO::description('Talk about Laravel and its ecosystem.');

Which is awesome compared to having to fetch the instance from the service container, annotate it, and call methods, e.g.:

/** @var SEOManager $seoManager */
$seoManager = app(SEOManager::class);

$seoManager->title('Laravel | ArchTech Forum');
$seoManager->description('Talk about Laravel and its ecosystem.');

(Or if we're in a class context where we can use DI, injecting the class in the constructor.)

The problem with facades

Facades provide great syntax benefits, but they come with one main problem: you need to duplicate method signatures between the actual class and the facade's /** @method */ annotations.

Because with the facade above, we'd have zero autocomplete or syntax reference if we opened the facade's class.

So when you're implementing a facade, you need to add an annotation for each method:

class SEOManager
{
    public function title(string $title): static { ... }
    public function description(string $description, array $overrides = []): static { ... }
    public function image(string $og, string $twitter = null): static { ... }
}

/**
 * @method static SEOManager title(string $title)
 * @method static SEOManager description(string $description, array $overrides = [])
 * @method static SEOManager image(string $og, string $twitter = null)
 */
class SEO extends Facade { ... }

And that's just three methods. Sure, writing this isn't all that horrible, but the thing is you will need to keep these in sync forever. When you're using a method directly, you can infer the typehints from the method signature, and make use of all the extra @annotations in the method's docblocks.

Also, there are composer aliases. Those let you use SEO instead of e.g. App\Facades\SEO. The problem with aliases is that although they work, many IDEs don't understand them, so calling SEO::title(); would result in red lines in many people's code editor. For that reason, facades often also involve importing the facade class, which sort of defeats one of their original value propositions.

Static classes

A possible solution to this is using static classes. Sometimes, it's okay to keep classes static, even if they're stateful:

class SEOManager
{
    public static string $title;
    public static string $description;
    public static string $image;

    public static function title(string $title)
    {
        static::$title = $title;
    }

    public static function description(string $title, array $overrides = [])
    {
        // ...
    }
}

With a class like this, you only write the logic and can access the same state from anywhere.

In Blade, you might use the class like this:

<title>{{ App\SEOManager::$title }}</title>

And if you register an alias (they're not just for facades), you can use just SEOManager without a namespace:

<title>{{ SEOManager::$title }}</title>

This is often used for package config, e.g. Jetstream lets you call:

Jetstream::useTeamModel(MyTeam::class);

The downsides of this are:

  • the global state will persist throughout the PHP's runtime, meaning even if the container state is reset by Octane or PHPUnit, the data will still be there. So you need to manually reset it in those cases
  • you cannot use DI (this is a downside if you do want to use DI, otherwise it doesn't matter)

Helper functions

Helper functions are my favorite solution to this. They'd be implemented like this:

function seo(string $property = null): SEOManager
{
    if ($property) {
        return seo()->get($property);
    }

    return app(SEOManager::class);
}

And now we can use the logic like this:

seo()
    ->title('Laravel | ArchTech Forum')
    ->description('Talk about Laravel and its ecosystem.');
<title>{{ seo('title') }}</title>

The syntax is clearly the most elegant. And the implementation cost is also the lowest:

  • we don't need any new classes, we just added a function to our helpers file
  • we don't have to duplicate method signatures between the class and facade annotations
  • we don't have to worry about state leaking across requests
  • we don't have to manually @var annotate app(SEOManager::class) calls, because the helper function has a return typehint
  • we don't need to use any composer aliases, since the function is already global

And there are also a couple of new benefits:

  • if a method is public(), it's shown in the callable methods in the IDE autosuggest. We control this by changing the method visibility
  • we don't need to import any classes in any files
  • we can use fluent APIs — each method can return $this and we can just chain calls like: ->title()->description(). Facades either don't allow this or result in a combination of :: and -> syntax
  • we can add special logic using parameters, such as seo('foo') for getting values and seo(['foo' => 'bar']) for setting values.

Closing

Throughout the post I changed the SEOManager method a bit to show different things about these approaches. Our SEO package itself uses a helper with annotated magic on the bound instance, so the real code wouldn't be a good example here, but for example our money package's CurrencyManager uses the currencies() helper as the only way to access it — following the principles described in this article.

The TLDR of this post is:

Facades come with duplication and ugly APIs and they often have messy IDE support. They also feel sort of fake, because you know that you're not making the call on the real object. That may sound like an abstract concern that doesn't translate to anything in practice, but it goes hand in hand with the annotations. Many facades include only a portion of the available methods on the underlying instance, so you sometimes need to resolve it from the container manually anyway.

Static classes require extra work when integrating with other tools or packages, but they can be a better alternative to facades in some cases.

Helpers are in my opinion the best solution to this. One call with no imports — just seo() — and you have the entire object. Your IDE understands everything. And you can have cool syntax like special behavior when parameters are used, or fluent methods.

Hope this article was helpful, especially if you've ever been implementing facades or other types of accessors for container-bound instances. If you disagree with anything or want to provide some context or feedback, feel free to reply, I'm always happy to discuss code design :)