Laravel Blade Filters

LaravelPosted on

For me, the only missing feature from Laravel is using filters in template level. Of course, there are some workarounds, but still, it’s not the cleanest and the most readable. Now let’s see, how to implement that in Laravel.

The Problem

Blade is an awesome and flexible template engine and really the only missing piece – at least for me – is the feature to have chainable filters. Of course, there are many workarounds, but still, the cleanest solution is that what for example django’s template engine or twig has. Let’s see a concrete example:

// Blade
{{ Str::limit(Str::title('this is not a title'), 10) }}

// django
{{ 'SOME STRING' | title | truncatechars:10 }}

This is a very basic and totally useless example, but it shows, that we need to nest functions in each other if we want to make a multi-step transformation on the string. In the django example, it’s also visible that we can easily read the filters and understand what’s going on.

Depending on the project we might make some small transformations on the values directly in the blade template. So after a while, it can be a bit frustrating and hard to read. That’s why we try to implement this approach in Blade as well.

The Custom ViewServiceProvider

If we take a look at the Illuminate\Views\ViewServiceProvider, we can see how the compiler was registered in the core. We have no other job, but to replace the compiler in the registerBladeEngine() method.

So let’s create a service provider with the php artisan make:provider BladeFiltersServiceProvider, and modify it a bit. Here is the code:

<?php

namespace App\Providers;

use App\Services\BladeFiltersCompiler;
use Illuminate\View\ViewServiceProvider;
use Illuminate\View\Engines\CompilerEngine;

class BladeFiltersServiceProvider extends ViewServiceProvider
{
    /**
     * Register the Blade engine implementation.
     *
     * @param  \Illuminate\View\Engines\EngineResolver  $resolver
     * @return void
     */
    public function registerBladeEngine($resolver)
    {
        $this->app->singleton('blade.compiler', function () {
            return new BladeFiltersCompiler(
                $this->app['files'], $this->app['config']['view.compiled']
            );
        });

        $resolver->register('blade', function () {
            return new CompilerEngine($this->app['blade.compiler']);
        });
    }
}
Don’t forget to register it in your app.php config file!

As we see, it’s not an ordinary service provider, since we extend the original ViewServiceProvider and not the ordinary ServiceProvider class. Except for registering another compiler, our provider does exactly the same thing like the original one. It’s very important to make as few core-like changes as possible.

The Custom Compiler

First of all, let’s clear what does the compiler do. It’s very simple. Parses the proper blade expressions – echoes and directives – and convert them to real PHP stuff. Here we need to be careful as well. As I mentioned before, we should not affect core functionality, only as little as possible.

In our case it means, we only want to extend that method that parses the {{ }} expressions. If we take a look at the original compiler, we can see, the compiled result of this expression {{ ‘Pine Code’ }} is this: <?php echo e(‘Pine Code’); ?>. Anything you put between the double braces, goes inside of the e() function. It means, {{ ‘string’ | filter }} will be <?php echo e(‘string’ | filter); ?>. But of course, this would cause an error.

The e() helper function just makes sure the rendered content is safe.

So our job is, to hook in the compiled content, and parse the filters from the e() function before it will be rendered in a real file. Let’s see the compiler, then go through it.

<?php

namespace App\Services;

use Illuminate\View\Compilers\BladeCompiler;

class BladeFiltersCompiler extends BladeCompiler
{
    /**
     * Compile the "regular" echo statements.
     *
     * @param  string  $value
     * @return string
     */
    protected function compileRegularEchos($value)
    {
        $value = parent::compileRegularEchos($value);

        return preg_replace_callback('/(?<=<\?php\secho\se\()(.*)(?=\);\s\?>)/u', function ($matches) {
            return $this->parseFilters($matches[0]);
        }, $value);
    }

    /**
     * Parse the blade filters and pass them to the echo.
     *
     * @param  string  $value
     * @return string
     */
    protected function parseFilters($value)
    {
        if (! preg_match('/(?=(?:[^\'\"\`)]*([\'\"\`])[^\'\"\`]*\1)*[^\'\"\`]*$)(\|.*)/u', $value, $matches)) {
            return $value;
        }

        $filters = preg_split('/\|(?=(?:[^\'\"\`]*([\'\"\`])[^\'\"\`]*\1)*[^\'\"\`]*$)/u', $matches[0]);

        if (empty($filters = array_values(array_filter(array_map('trim', $filters))))) {
            return $value;
        }

        foreach ($filters as $key => $filter) {
            $filter = preg_split('/:(?=(?:[^\'\"\`]*([\'\"\`])[^\'\"\`]*\1)*[^\'\"\`]*$)/u', trim($filter));

            $wrapped = sprintf(
                '\Illuminate\Support\Str::%s(%s%s)',
                $filter[0],
                $key === 0 ? rtrim(str_replace($matches[0], '', $value)) : $wrapped,
                isset($filter[1]) ? ",{$filter[1]}" : ''
            );
        }

        return $wrapped;
    }
}
Disclaimer: I’m not a RegExp expert at all. It’s very possible that not every use case is covered by the RegExp, or maybe these are not the best solutions.

So what’s going on here? As it’s visible, we extend the original BladeCompiler, it means our compiler inherits all its functionality. For us, it’s very important, since we just want to add an extra little layer on the top of the existing compiler.

In the compileRegularEchos() method, we let the compiler do its job, but instead of returning the compiled result instantly, we pass it to our own logic to look for and parse filters inside the previously compiled e() functions. So every match we have for the regex, we pass it to the closure that we called in the preg_replace_callback function, where we call the parseFilters() method. And here comes the “trick”.

In the parseFilters() method we look for any potential filters. What does it mean? Anything that starts a | operator which is not wrapped in , or (). For instance:

{{ 'string' | filter1 | filter2 }} // Yes

{{ $var | filter1 | filter2 }} // Yes

{{ '|string|' }} // No

{{ '|string|' | filter1 | filter2  }} // Yes

{{ func($var | FLAG) }} // No

// And so on...

If there is no match, we just return with the original content – it means nothing will be replaced. But if there is any match, we can start to transform the filters and their parameters to real functions. Also, we need to make sure, we split the filters from each other and the filters from their parameters only by the valid : and | characters. It means, if they are wrapped in -s or -s, we handle them as a parameter and not as a match for the preg_split.

So, since we are talking about echoing out something, probably we talk about strings or numeric values. To transforming these values, Laravel offers the perfect place with the Str facade. It has a few reasons:

  • quite much built-in methods we can use as filters,
  • easily extendable with macros,
  • mostly convenient parameter ordering.
Speaking of parameter ordering, it’s essential, that we can use only a fix order. It means the string/numeric value MUST be the first and any other parameter can follow it. Some PHP and some Laravel string functions does not follow this convention.

For example, this filter {{ ‘string’ | title }} would be this: \Illuminate\Support\Facades\Str::title(‘string’). If there are more filters, going from left to right is like going from inside outside. It means the converted Str methods will be nested to each other.

Also, since we just pass the given filter parameters to the Str function without any modification, it means we are able to use dynamic filter parameters as well:

{{ 'string' | limit:2 }} // st...
// \Illuminate\Support\Facades\Str::limit('string',2)


$limit = 2;
{{ 'string' | limit:$limit }} // st...
// \Illuminate\Support\Facades\Str::limit('string',$limit)

This way, we really can have filters that work with previously defined variables. So we see now, how the filter parsing works, let’s move on, how to create custom filters by defining macros.

Str::macro() and Custom Filters

Now we see that, if we want a custom filter, we need to make a macro for the Str facade. Typically you may create these macros in a dedicated service provider like FiltersServiceProvider or StrServiceProvider. Anything you find good. Then in the boot() method you can define your filters (macros) as you want.

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Str;
use Illuminate\Support\ServiceProvider;

class StrServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        // Date formatting macro
        Str::macro('date', function ($value, $format = 'Y-m-d') {
            return date($format, strtotime($value));
        });
    }
}

After you registered your service provider, all the macros are available and callable from the Str facade. Note, to make your macros work as filters, you must pass the value parameter as first, as it was mentioned before. From now you can call the date filter easily:

{{ '1999/12/31' | date:'F j, Y' }}

And that’s it. Now, we can define our own filters and use them along with the built-in Str methods (that have compatible parameter ordering), chain them, pass the parameters and watch for the result!

The Blade Filters Package

We wrote a package, that handles the filter parsing and compiling behind the scenes. You just need to pull in the package, and ready to go. No setup, no configuration, no vendor publishing. Also, it comes with some built-in filters, that you can explore in the documentation.

If you have any idea or suggestion connected to the package, be brave to open an issue or a PR.

Need a web developer? Maybe we can help, get in touch!

Similar Posts

More content in Laravel category