How to Get Rid of Magic, Static and Chaos from Latte Filters

This post was updated at September 2020 with fresh know-how.
What is new?

Added options 6 - invokable filter providers.


In the previous post, we looked at how to avoid array magic and duplicates of Latte in Presenter and Components.

Today we'll leverage those tips to make your code around Latte filters easy and smooth to work with.

Do you have your LatteFactory service ready? If not, create it first, because we'll build on it.


<?php

declare(strict_types=1);

namespace App\Latte;

use Latte\Engine;
use Latte\Runtime\FilterInfo;

final class LatteFactory
{
    public function create(): Engine
    {
        $engine = new Engine();
        $engine->setStrictTypes(true);

        return $engine;
    }
}

How to register a new Latte Filter?

This simple question can add easily add an anti-pattern to your code, that spreads like COVID and inspires developers to add more anti-patterns. It's easy to submit to static infinite loop, I did it too.

But let's look at practice... how to add a filter?

Let's say we want to format money. The filter code is not relevant here, so we go with the simplest version possible:

class SomeFilter
{
    public function money(int $amount): string
    {
        return $amount . ' €';
    }
}

We'll use it template like this:

You're total invoice amount:
<strong>{$amount|money}</strong>

Thank you

1. Register Static Magic Loader

This used to be the best practice in 2014. Just add a class and magically delegate called filter name:

 namespace App\Latte;

 use Latte\Engine;
 use Latte\Runtime\FilterInfo;

 final class LatteFactory
 {
     public function create(): Engine
     {
         $engine = new Engine();
         $engine->setStrictTypes(true);
+        $engine->addFilter(null, SomeFilter::class . '::loader');

         return $engine;
    }
 }

And add loader() method:

 class SomeFilter
 {
-    public function money(int $amount): string
+    public static function money(int $amount): string
     {
         return $amount . ' €';
     }

+    public static function loader($arg)
+    {
+        $arg = \func_get_args();
+        $func = \array_shift($arg);
+        if (\method_exists(self::class, $func)) {
+            return \call_user_func_array([self::class, $func], $arg);
+        }
+
+         return null;
     }
 }

This is my favorite magic part:

$engine->addFilter(null, SomeFilter::class . '::loader');

Do you have any what is happening there? I don't.


Pros & Cons

  • We have to use static
  • We can't use any service in SomeFilter, we have to use static only ❌
  • We violate addFilter() method with magic and make it harder to read, maintain and refactor ❌
  • We have one place to add filters ✅

Can we do better?

2. Register Function manually with addFilter()

The addFilter() can be used in the way it's designed for:

 namespace App\Latte;

 use Latte\Engine;
 use Latte\Runtime\FilterInfo;

 final class LatteFactory
 {
     public function create(): Engine
     {
         $engine = new Engine();
         $engine->setStrictTypes(true);
+        $engine->addFilter('money', function (int $amount): string {
+             return $amount . ' €';
+        });

         return $engine;
    }
 }

Straight forward, transparent, and a few lines of code.

Pros & Cons

  • We have very little code ✅
  • The framework part (Latte) is now directly bounded to our application domain - this makes code hard to refactor, decopule from framework or re-use in another context ❌
  • We break dependency inversion principle - we have to edit LatteFactory to add a new filter ❌
  • We made a seed for God class antipattern - soon our LatteFactory will have over 100 of lines with various filters ❌
  • We think it's a good idea, because of short-code-is-the-best fallacy ❌

Can we do better?

3. Add Filter Provider Service?

The previous solution looks fine, if only we could get rid of coupling between framework and our code.

 namespace App\Latte;

 use Latte\Engine;
 use Latte\Runtime\FilterInfo;

 final class LatteFactory
 {
+    private FilterProvider $filterProvider;
+
+    public function __construct(FilterProvider $filterProvider)
+    {
+        $this->filterProvider = $filterProvider;
+    }

     public function create(): Engine
     {
         $engine = new Engine();
         $engine->setStrictTypes(true);

+        foreach ($this->filterProvider->provide() as $filterName => $filterCallback) {
+            $engine->addFilter($filterName, $filterCallback);
+        }

         return $engine;
    }
 }
<?php

final class FilterProvider
{
    /**
     * @return array<string, callable>
     */
    public function provide(): array
    {
        return [
            'money' => function (int $amount): string {
                return $amount . ' €';
            }
        ];
    }
}

The filter class is decoupled - no more hard-coded filters!

Pros & Cons

  • We can add a new filter without every touching LatteFactory
  • We can use services in filters ✅
  • We only moved a seed for God class antipattern - soon our FilterProvider will have over 100 of lines with various filters ❌

Can we do better?

4. Filter Provider Contract

The ultimate solution is almost perfect. We only need to get rid of the God class completely. How can we do that?

The goal is simple:

  • each domain should have its filters, e.g., filters for text should have their class, filters for money should have their class, etc.
  • we can't touch the LatteEngine to add a new filter, nor a new filter service

What if we use autowired arrays feature from Nette 3.0?


 namespace App\Latte;

+use App\Contract\FilterProviderInterface;
 use Latte\Engine;
 use Latte\Runtime\FilterInfo;

 final class LatteFactory
 {
+    private array $filterProvider;
+
+    /**
+     * @param FilterProviderInterface[] $filterProviders
+     */
+    public function __construct(array $filterProviders)
+    {
+        $this->filterProviders = $filterProviders;
+    }

     public function create(): Engine
     {
         $engine = new Engine();
         $engine->setStrictTypes(true);

+        foreach ($this->filterProviders as $filterProvider) {
+            foreach ($filterProvider->provide() as $filterName => $filterCallback) {
+                $engine->addFilter($filterName, $filterCallback);
+            }
+        }

         return $engine;
    }
 }
namespace App\Contract;

interface FilterProviderInterface
{
    /**
     * @return array<string, callable>
     */
    public function provide();
}
+use App\Contract\FilterProviderInterface;

-final class FilterProvider
+final class MoneyFilterProvider implements FilterProviderInterface
 {
     /**
      * @return array<string, callable>
      */
     public function provide(): array
     {
         return [
             'money' => function (int $amount): string {
                 return $amount . ' €';
             }
         ];
     }
 }

Pros & Cons

  • We have decoupled framework and our domain-specific filter ✅
  • To add a new filters, we only need to create a new service ✅
  • We finally use dependency injection at its best - Nette handles registering filters and collecting service for us ✅
  • We add a seed for God method - soon provide() will be full of weird callbacks and long methods ❌

Can we do better?


5. From Callbacks to Private Methods

 use App\Contract\FilterProviderInterface;

 final class MoneyFilterProvider implements FilterProviderInterface
 {
     /**
      * @return array<string, callable>
      */
     public function provide(): array
     {
         return [
             'money' => function (int $amount): string {
-                return $amount . ' €';
+                return $this->money($mount);
             }
         ];
     }

+    private function money(int $amount): string
+    {
+        return $amount . ' €';
+    }
 }

This looks like a duplicated code, right?

But what if money filters grow, included timezones and logged in user country? Is MoneyFilterProvider the best place to handle all this logic?

 use App\Contract\FilterProviderInterface;

 final class MoneyFilterProvider implements FilterProviderInterface
 {
+    private MoneyFormatResolver $moneyFormatResolver;
+
+    public function __construct(MoneyFormatResolver $moneyFormatResolver)
+    {
+       $this->moneyFormatResolver = $moneyFormatResolver;
+    }

     /**
      * @return array<string, callable>
      */
     public function provide(): array
     {
         return [
             'money' => function (int $amount): string {
-                return $this->money($mount);
+                return $this->moneyFormatResolver->resolve($amount);
             }
         ];
     }

-    private function money(int $amount): string
-    {
-        return $amount . ' €';
-    }
 }

Pros & Cons

  • We have decoupled domain logic from filters ✅
  • We can re-use the used-to-be filter logic with MoneyFormatResolver in other places of application ✅
  • We are motivated to use DI and decouple code clearly to new service, if it ever becomes too complex ✅
  • We are ready for any changes that come in the future ✅
  • ~~We think this is the best way, just because it's last ❌~~ Not anymore ↓

My question is: can we do better...?


Update 1 month later with new option:

6. Invokable Filter Providers

In fashion of single-action controller a tip from @FrantisekMasa and @dada_amater for similar approach in filters. It look weird, new... so I had to try it in practise to see for myself.

namespace App\Contract;

interface FilterProviderInterface
{
    public function getName(): string;
}

The filter itself - 1 filter = 1 class:

use App\Contract\FilterProviderInterface;

final class MoneyFilterProvider implements FilterProviderInterface
{
    private MoneyFormatResolver $moneyFormatResolver;

    public function __construct(MoneyFormatResolver $moneyFormatResolver)
    {
       $this->moneyFormatResolver = $moneyFormatResolver;
    }

    public function __invoke(int $amount): string
    {
        return $this->moneyFormatResolver->resolve($amount);
    }
}

The LatteFactory now acceps the whole filter as callable object:

namespace App\Latte;

use Latte\Engine;
use Latte\Runtime\FilterInfo;

final class LatteFactory
{
    /**
     * @var FilterProviders[]
     */
    private array $filterProviders = [];

    /**
     * @param FilterProvider[]
     */
    public function __construct(array $filterProviders)
    {
        $this->filterProviders = $filterProviders;
    }

    public function create(): Engine
    {
        $engine = new Engine();
        $engine->setStrictTypes(true);

        foreach ($this->filterProviders as $filterProvider) {
            $engine->addFilter($filterProvider->getName(), $filterProvider);
        }

        return $engine;
    }
}

Pros & Cons

  • All of the advantages of previous approaches ✅
  • 1 class = 1 rule, this is really challenge to clutter ✅
  • It's very intuitive to use ✅
  • We don't have to maintain duplicated provideFilters() callables with private methods ✅
  • The __invoke() method has no contract, so we can forget to implement it ❌

We compensate this in LatteFactory itself:

public function create(): Engine
{
    $engine = new Engine();
    $engine->setStrictTypes(true);

    foreach ($this->filterProviders as $filterProvider) {
        if (! method_exists($filterProvider, '__invoke')) {
            $message = sprintf('Add "__invoke()" method to filter provider "%s"', get_class($filterProvider));
            throw new ShouldNotHappenException($message);
        }

        $engine->addFilter($filterProvider->getName(), $filterProvider);
    }

    return $engine;
}

That's it!


Happy coding!




Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors. I'd really appreciate it!