November 17th, 2023 Laravel PHP Patterns

Hands-on decoration

The Decorator design pattern, featured in the original book by the Gang of Four, captivates me the most among all the discussed patterns. Its simplicity and power (i.e. wide-ranging use / applicability) makes it my personal favorite.

This simplicity and usefulness, however, is often overlooked and instead the focal points are shifted to "Oh, nice over-engineering" or "Premature abstraction is the root of all evil!" (because there's an interface in use). While I'm not claiming these are never the case, I think that it'd be fair to say it is flagrantly inequitable to tar every single use case and implementation out there with the same brush. Additionally, the Laravel documentation on service decoration is — in my opinion — way too concise.

That's why, in this blog post, I'd like to shed some clearer light on this underdog within the Laravel community and provide a practical example instead of displaying made-up scenarios. The only prerequisite is to set aside any prejudices you might have regarding decorators whilst also possessing a clear and open mind. I'm not claiming to be the sharpest tool in the shed, though I do believe that I have a superb, practical example waiting for you.

This blog post assumes that you have at least some familiarity with the decorator design pattern. If you don't, then I highly recommend that you check out this write-up by Doeke Norg first before continuing.

Case study

While working on a side project of mine, I needed the ability to dispatch and display toast messages. You know, those petty alert messages that appear in the corner of your screen. Like always, since this is a pretty generic problem, I started looking at various package directories to see whether someone had built something in Livewire already. I was definitely not disappointed as there were tons of packages. After having had a thorough look at each and every package, though, I unfortunately came to the conclusion that none of them simultaneously checked all of my criteria:

  • Not bloated — Huge problem with popular packages.
  • Performant — No additional requests needed. Some always flash to the session, and thus incur slight performance penalties.
  • Smart — No need to explicitly tell if it needs to be an event dispatch or a session flash: the library should figure it out itself. It should also keep the toasts a tiny bit longer on the screen if the textual content is a bit too much to read in e.g. 3 seconds.
  • Effortless in use — No need to remember an explicit send call.
  • Top notch DX — Lots of apps are multi-locale, so no need to explicitly call __ or Lang::get.
  • Minimalist design — No intrusive icons, action buttons etc. It should do one job and do it well: display toast messages.

Since such a package did not exist, I decided to bring it into life! I hereby present: Livewire Toaster. (Creative name, isn't it? You should definitely hire me for your branding needs).

I'd like to zoom into the third and fifth bullet points: Smart and Top notch DX. In order to make these happen, I made use of snazzy decorators. You must have also heard about the Open-closed principle, right? You know, the O in SOLID. By using decorators, it was possible to design the code this way so future extensions are a piece of cake.

This characteristic came as a mere bonus thanks to the modelling approach because it was not the goal in and of itself. Please use recipes and tools as an objective compass to guide you in the right direction. The compass itself is not the goal, the goal is the destination!

Laravel's extend method

Before moving on to the actual implementation with some code examples, we first need to establish a common understanding with regards to the Containers extend method and its designated role. I have seen plenty of examples that use this functionality, but unfortunately for the wrong reasons (IMHO). From the docs, verbatim:

For example, when a service is resolved, you may run additional code to decorate or configure the service.

Unfortunately, all of them focused on this last bit:

(...) configure the service.

There's a much better way to achieve this goal: Container Events. It makes much more sense to do some additional configuration in a $this->app->resolving(...) call because they're literally designed for this very purpose. Here's an example from Inertia that uses the afterResolving event (callAfterResolving is an alias):

$this->callAfterResolving('blade.compiler', function ($blade) {
    $blade->directive('inertia', [Directive::class, 'compile']);
    $blade->directive('inertiaHead', [Directive::class, 'compileHead']);
});

Going back to the $this->app->extend(...) method, I think it's much easier to remember its true purpose by thinking of it as "extending the behavior". Setting a property on a resolved object does not necessarily "extend the behavior"; it's more configuration work.

A "good" example

For example, have you ever created a package and had a need to "extend" (read: configure) Laravel's ExceptionHandler? You could choose to go down this decoration path:

$this->app->extend(ExceptionHandler::class, fn ($next) => new Handler($next));

and do something like this:

final class Handler implements ExceptionHandler
{
    public function __construct(
        private ExceptionHandler $handler,
    ) {}

    public function render($request, Throwable $e): mixed
    {
        if ($e instanceof ModelNotFoundException) {
            // do something...
        }

        return $this->handler->render($request, $e);
    }

    // omitted for brevity
}

I'd argue that this is the lesser option, and using Container Events is a much better alternative:

$this->callAfterResolving(ExceptionHandler::class, function (ExceptionHandler $handler) {
    $handler->ignore(SucceededException::class);
    $handler->renderable(fn (SucceededException $e) => $this->app->make(Responder::class)->respond());
});

I hope this makes sense to you. Now let's get to work!

Designing the package

🔗 Livewire Toaster

The first thing that you should almost always do when developing a new feature / package / functionality is experimenting and thinking about the design / models. You might be under the impression that the current state of the package was there from day 0, but nothing could be further from the truth. I think it took me 2-3 weeks to nail the design of the package so I could implement all of the (self-imposed) requirements and pave the path for future additions.

Thought process

I sat down and started thinking about how I'd need to tackle the various problems. To make a long story short, after considering all the requirements and connecting all the dots (and after three rewrites), I decided that I needed multiple components to solve the puzzle:

  • Toaster — The piece responsible for managing and dispatching the toasts.
  • Toast — We need to dispatch a toast, after all.
  • Relay — Multiple implementations to relay toast messages to the right channels, according to what fits best in the current context.
  • Hub - Front-end component that should display the incoming toasts.
  • Collector — The central actor whose role is to collect toast messages and then release them on request. This is important because there are multiple Relay components as we have just discovered. We are going to focus on this one primarily.
  • There are other fine-grained components, but they're not really relevant to make our point here. You can always check out the GitHub repository if you'd like to learn more.

These components all work in tandem to achieve the bigger goal: dispatching and displaying multiple toast messages.

Collector

As we've already learned, this piece sits in the middle of the spectacle and its job is to collect toast messages and then release them when requested. Let's bring this concept to life:

interface Collector
{
    public function collect(Toast $toast): void;

    public function release(): array;
}

Brilliant. We are 50% done already, no kidding. After having defined this abstract model, we need something concrete to represent it. The specific way in which the Collector actually collects those Toast messages is an implementation detail. The simplest design I could come up with is a QueuingCollector:

final class QueuingCollector implements Collector
{
    private array $toasts = [];

    public function collect(Toast $toast): void
    {
        $this->toasts[] = $toast;
    }

    public function release(): array
    {
        $toasts = $this->toasts;
        $this->toasts = [];

        return $toasts;
    }
}

It is called this way, because that's exactly what it does: it enqueues toast messages using a Queue-like data structure. This is going to be our main concretion for our abstract model. Next, we need to register it in the service container:

public function register(): void
{
    $this->app->scoped(Collector::class, QueuingCollector::class);
}

Are we done? Well, kind of. We still need to enhance the bare minimum functionality and add the behavioral flavors on top.

Decoration elation

Smart

The package needs to be smart, in that it should keep the messages a tad longer on-screen if the messages themselves are longer in nature as well. I have yet to come across a human being that can read 300 words in 3 seconds. This entails the accessibility aspect of the toast messages, so calling this an AccessibleCollector would be perfect:

final readonly class AccessibleCollector implements Collector
{
    private const AMOUNT_OF_WORDS = 100;
    private const ONE_SECOND = 1000;

    public function __construct(private Collector $next) {}

    public function collect(Toast $toast): void
    {
        $addend = (int) floor(str_word_count($toast->message->value) / self::AMOUNT_OF_WORDS);
        $addend = $addend * self::ONE_SECOND;

        if ($addend > 0) {
            $toast = ToastBuilder::proto($toast)->duration($toast->duration->value + $addend)->get();
        }

        $this->next->collect($toast);
    }

    public function release(): array
    {
        return $this->next->release();
    }
}

The code itself is not that relevant, but essentialy what it does is add additional on-screen time for every 100th word in the message. There are a few things to remark on here:

  • We are accepting another Collector instance, as this is very typical for decorators. In the end, we will obtain an "onion structure".
  • Not everything has to be altered when decorating. In this case, it's only the collect method that's being decorated. We don't need to extend the behavior of the release method.
  • There is no inheritance. This is composition 101.
  • final readonly is my default.

Top notch DX

I dread this syntax (pseudocode):

Toast::create()
    ->error(__('general.obvious.translation'))
    ->send();

We could be doing much better:

Toaster::error('general.obvious.translation');

And with the decorators now in place, this is trivial to implement. Since this entails the translation aspect of the toast messages, the name TranslatingCollector would be perfect:

final readonly class TranslatingCollector implements Collector
{
    public function __construct(
        private Collector $next,
        private Translator $translator,
    ) {}

    public function collect(Toast $toast): void
    {
        $replacement = $this->translator->get($original = $toast->message->value, $toast->message->replace);

        if (is_string($replacement) && $replacement !== $original) {
            $toast = ToastBuilder::proto($toast)->message($replacement)->get();
        }

        $this->next->collect($toast);
    }

    public function release(): array
    {
        return $this->next->release();
    }
}

Again, the code itself is not that relevant, but what it does is query the translator and check if the original message got replaced with a new one. If it did change, the original Toast gets replaced with a new one and is then forwarded to the next object. I love composition.

Addendum: Effortless in use

Eagle-eyed readers may have noticed that this piece of code does not solve the required, explicit ->send() calls. It has nothing to do with decoration, but rather with the lifecycle of an object. For this, I created a wrapper PendingToast class that wraps the ToastBuilder, and automatically dispatches it whenever the object is about to go out of scope:

public function __destruct()
{
    if (! $this->dispatched) {
        $this->dispatch();
    }
}

Magic? Sure it is, but that's some good magic. Give me more of it.

Tying everything together

Evidently, these decorators won't be doing anything unless we register them with the service container using the extend method:

public function register(): void
{
    $this->app->scoped(Collector::class, QueuingCollector::class);

    $this->app->extend(Collector::class, static fn (Collector $next) => new AccessibleCollector($next));
    $this->app->extend(Collector::class, fn (Collector $next) => new TranslatingCollector($next, $this->app['translator']));
}

Et voilà. We are done... but are we really? What about providing the package users with the ability to toggle certain features? That would be hard, right? Wrong!

Let's define a few flags in the configuration file:

return [

    /**
     * Add an additional second for every 100th word of the toast messages.
     *
     * Supported: true | false
     */
    'accessibility' => true,

    /**
     * Whether messages passed as translation keys should be translated automatically.
     *
     * Supported: true | false
     */
    'translate' => true,
];

The only thing remaining now is to add simple if-checks (disregard the slightly unconventional config object):

public function register(): void
{
    $config = $this->configureService();

    $this->app->scoped(Collector::class, QueuingCollector::class);

    if ($config->wantsAccessibility) {
        $this->app->extend(Collector::class, static fn (Collector $next) => new AccessibleCollector($next));
    }

    if ($config->wantsTranslation) {
        $this->app->extend(Collector::class, fn (Collector $next) => new TranslatingCollector($next, $this->app['translator']));
    }
}

Aaand we are done. I hope you were blown away by the mere simplicity & flexibility of this approach. What's even more remarkable is the fact that besides making use of the decorator pattern, we have also set up a custom Middleware / Pipeline. The responsibilities go like this (very last registration becomes the outer layer of the "onion"):

ToastTranslatingCollectorAccessibleCollectorQueuingCollector

The Toast object goes through a series of layers / pipes before reaching its destination, which is the QueuingCollector. Now imagine that new feature requests come in, or we see points of improvement. Extending the functionality / behavior is child's play:

ToastChatGptCollectorTranslatingCollectorBroadcastingCollectorQueuingCollector

What happened to the AccessibleCollector?.. Don't worry. The package user decided to turn that feature off.

Final words

  • Conditionals in code are unavoidable. However, we can place them so high that they almost "fall off". The earlier those conditionals get applied, the less complexity in the resulting code. Composition is the perfect fit for this.
  • There is no single use case for a design pattern. Your creativity is the only limiting factor here. I still remember my college days at Ghent University where they gave silly coffee examples to explain decorators. Looking back at that lecture now, I just have to cringe at how contrived that example was. Contrast that to a real-world example like this...
  • Decorators can be applied as a Chain of Responsibility. This is noticeable by the $next parameter usage.
  • $this->app->extend deserves way, way more love and affection.
  • SOLID is a good tool to have.
  • Behavioral composition is always going to triumph over data-centric thinking. I can't stress enough how trivial it is to add additional functionality to the package while keeping everything else intact.
  • Proper design is key.
  • Also, please don't forget to star Livewire Toaster.

Join the discussion on X (formerly Twitter)! I'd love to know what you thought about this blog post.

Thanks for reading!