Fun with Blade directives

Table of Contents

Let's start this off by explaining how the Blade engine works with a few bullet points.

  • You choose a Blade template to render.
  • The engine uses a series of regular expressions to parse and compile the template.
  • The engine produces a plain PHP file and writes it to disk (so that it's cached for future renders).
  • The PHP file is included and output buffers are used to capture the generated HTML.

The most interesting step in the process is that RegEx patterns are used to extract various things from the template and generate the appropriate PHP code. Other template engines use a more traditional tokeniser and parser to process templates, but since Blade is more or less just syntax sugar over regular PHP code, it can do things in a much simpler manner.

What this does mean is that you're basically always dealing with arbitrary strings that might contain plain ol' PHP code.

It's possible to write your own Blade directives. This lets you hide a lot of boilerplate code inside of a directive and simplify your Blade templates.

Blade::directive('example', function (string $expression) {
    // Logic goes here.

Something that catches a lot of new Laravel developers out is the fact that custom directives only receive a single argument to the callback function.

Let's say our the @example() directive here is designed to accept 2 arguments:

@example('Hello, ', 'Ryan')

A less-experienced Laravel developer might expect the callback function to receive two arguments, corresponding to the two arguments we passed through to the actual directive.

That's not the case though. Instead, we receive a string containing the literal text from the Blade template.

Blade::directive('example', function (string $expression) {
    assert($expression === "'Hello, ', 'Ryan'");

So instead of writing regular PHP code inside of the callback and returning a value, we actually instead want to return a string of PHP code. That PHP code will then be inserted into the generated template in place of the original directive.

Blade::directive("example", function (string $expression) {
    return "<?php echo implode(' ', [{$expression}]); ?>";

There are packages out there that extend Laravel to support the more logical "receive the real arguments inside of the callback" approach, but the fact that we have a string here means we can do some fun and creative things.

Custom Blade directives quite often provide a wrapper around a regular PHP function. PHP 8.0 introduced the concept of "named arguments", allowing you to pass arguments to a function out-of-order and instead provide the name of the argument instead.

function hello(string $name, string $greeting = "Hello, ")
    return $greeting . $name;

hello(greeting: "Greetings, ", name: "Ryan");

If we wrap this hello() function inside of a Blade directive, we can actually still use named arguments!

Blade::directive("hello", function (string $expression) {
    return "<?php echo hello({$expression}); ?>";
@hello(greeting: "Greetings,", name: "Ryan")

Since the text between the parentheses is just being inserted inside of our expression, the named arguments are passed along verbatim to the underlying hello() function.

Pretty cool!

Most Laravel developers have probably used "magic variables" in their projects before. The two most common examples are probably the $loop variable available inside of @foreach blocks, and the $message variable inside of an @error block.

Laravel ships with an @auth directive that lets you conditionally do stuff based on whether a user is logged in or not. This is cool, but I want to ship my own @auth directive that injects the current user into the block of code as a $user variable.

Fun fact – you can actually override Laravel's own directives with custom ones, since the Blade compiler checks for custom ones first. I don't recommend doing this though – all of this code is purely for educational and demonstration purposes!

Blade::directive("auth", function (string $expression) {
    $guard = $expression ?: "()";

    return "<?php if (auth()->guard{$guard}->check()): ?>" .
        '<?php $user = auth()->user(); ?>';

Blade::directive("endauth", function () {
    return '<?php unset($user); ?>' . "<?php endif; ?>";

The code above returns multiple statements for each directive. Starting and ending the if block, and also creating and unsetting the magical $user variable.

This code isn't 100% sound, please do not do this in your own apps.

The fact that we can compile directives into any arbitrary PHP code opens up some cool opportunities for things. I've even developed a couple of packages that take advantage of this for caching blocks of Blade code and even creating inline partials!

Another way that we can take advantage of the stringy nature of Blade directives is by writing our own domain-specific languages inside of a Blade directive.

Lots of server-side template engines have the concept of "filters". Here's an example from Twig:

{{ names | join(',') | lower }}

So names is a variable that gets passed to the join() function. The output of join(',') is then sent through to lower and the result of that operation is then output in the template.

What if we wanted to do the same but in a Blade directive, perhaps something like this:

@filter($names | join(',') | lower)

The first step is going to be parsing out the stuff inside of the directive. To get all of the different filters and the variable, we can split the expression by the | tokens, removing any excess whitespace around each part.

$parts = array_map(trim(...), explode("|", $expression));

To keep things simple, we'll assume that the first part is always a valid PHP expression.

$subject = $parts[0];
$filters = array_slice($parts, 1);

Each of the filters will map to a Closure that accepts the current value of $subject, as well as any arguments we pass to the filter. We'll need somewhere to store those callback functions.

Warning: The code you're about to see contains magic.

class Filters
    protected array $filters = [];

    public function __construct(protected mixed $subject)
        $this->addFilter("join", function (array $subject, string $glue = "") {
            return implode($glue, $subject);

        $this->addFilter("lower", function (string $subject) {
            return strtolower($subject);

    public function addFilter(string $name, Closure $callback): void
        $this->filters[$name] = $callback;

    public function get(): mixed
        return $this->subject;

    public function __call(string $name, array $args)
        if (!isset($this->filters[$name])) {
            throw new Exception("Unrecognised filter [{$name}].");

        $this->subject = $this->filters[$name]($this->subject, ...$args);

        return $this;

    public function __get(string $name)
        return $this->{$name}();

Each filter is registered with the class upon instantiation. To actually invoke a filter, you either call a non-existent method on the class or access a non-existent property.

The Blade directive then needs to translate the string of filters into a series of method calls on a Filters object.

return sprintf(
<?php echo (new \App\Filters(%s))
    ->get(); ?>
    implode("\n    ->", $filters)

The $subject is passed to the constructor, then each of the filters is chained onto the object as a method or property. That triggers the __call() or __get() method on the object which runs the filter over $subject.

Then at the very end, the ->get() method is called to retrieve the final value and output in the rendered template.

I warned you, here lies magic.

So the Blade example above would be converted into something like this:

echo (new \App\Filters($names))

Passing ['Ryan', 'Jane', 'John'] through this set of filters would produce ryan,jane,john.

This is some seriously weird and wacky stuff – something you'll probably never want to use in a real application – but it's cool to mess around with regardless.

Perhaps you'll take some of these ideas away and build some cool Blade directives of your own for fun and magical purposes.

Enjoyed this post or found it useful? Please consider sharing it on Twitter.