All About Writing Custom Blade Directives

Hammad Ahmed

Introduction

Often, you will find yourself writing complex logic and conditionals in your views. They not only muck up your pretty Blade templates but also induce logic into your views until you sweep them out with custom Blade directives. Here come Blade directives to rescue your views.

Blade Directives

Laravel Blade is a templating engine that compiles its special syntax back into PHP and HTML. Its special syntax includes directives. Directives are sugar-added functions hiding complex or ugly code behind them. Blade includes lots of built-in directives and also allows you to define custom ones. The built-in ones are more than enough for small projects. But as you find yourself repeating complex functionality in your code, it is a smell that you need to refactor to custom Blade directives.

Defining a Custom Blade Directive

You can define a custom Blade directive like this:

\Blade::directive('directive_name', function ($expression) {
    return $expression;
});

The $expression parameter is optional in case if the directive is used without anything inside the round brackets.

Let's start with a simple Hello World custom directive which you want to be able to use like this:

<p>@hello('World')</p>

This, of course, is a terrible example. But for the sake of easy explanation for this easy thing, it tailors right. Now, we have to declare this custom directive and the place for this task is the boot method of the AppServiceProvider.php file.

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Perform post-registration booting of services.
     *
     * @return void
     */
    public function boot()
    {
        Blade::directive('hello', function ($expression) {
            return "<?php echo 'Hello ' . {$expression}; ?>";
        });
    }

    /**
     * Register bindings in the container.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

If you have read any old tutorial, then you may know that the $expression passed to your directive callback used to include the outermost parentheses with the expression. But this was changed and they were omitted in Laravel 5.3. In Laravel 5.2- They had to be stripped with something like this: str_replace(['(',')',' '], '', $expression). But don't worry, you don't have to to do anything like that if you are using Laravel 5.3+.

In this way, our custom directive gets loaded correctly and can be used in any Blade template. The string returned by the closure of the directive just echoes the string 'Hello ' concatenated by the expression passed, all of which is wrapped in PHP tags. So, @hello('World') gets resulted in 'Hello World' and `@hello('Hammad') get resulted in 'Hello Hammad'. This is it. You have made your first custom Blade directive and are ready to challenge Lord Otwell... But wait, this is only a trivial example. Now that you have got your hands dirty, I must arm you with some better and useful examples before you... Come, let me show you more.

Example: Hiding Paid Content

Imagine you have a subscription-based vlog like Laracasts or Scotch and you want to hide your premium videos from unsubscribed users. You could manually put an IF block in your view and check if the user has a subscription. This would be fine for one or two views, but for every view, it can get tedious. Like, you want to show premium videos to subscribed users and hide annoying ads from them. With custom Blade directives, this is a cinch.

Let's get started by doing a fresh install of Laravel:

laravel new vlog

Now, modify the users' table migration to add a new property - isSubscribed:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('username')->unique();
            $table->string('email')->unique();
            $table->string('password');
            $table->boolean('isSubscribed')->default(false);
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}

We have the user in place. Now, create the model and migration for videos:

php artisan make:model Video -m

Finally, add the necessary columns to the videos table:


<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateVideosTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('videos', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title');
            $table->string('file')->unique();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('videos');
    }
}

Now that we have everything in place, let's see how we can hide those videos from unsubscribed users. See, you should first try to think how you'd use the directive before you start implementing it's functionality. This is a nice practice to get into. Maybe like this:

<h1>As~Seen~On~Internet.io</h1>
<p>Premium Videos soon-to-be-available on every browser window near you.</p>
<div id="app">
    <div class="premium-video">
        <h2>Premium Video</h2>
        @subscribed
            <video src="video_for_paid_users_only.mp4"></video>
        @unsubscribed
            <p>Bummer! Looks like you need a subscription to access this video.</p>
        @endsubscribed
    </div>
    <div class="annoying-ads">
        <h2>Annoying Ads</h2>
        @subscribed
            {{-- Annoying Ads - not allowed here --}}
        @unsubscribed
            @foreach ($annoyingAds as $ad)
                {{-- Sponsorships: more than a hundred annyoing companies --}}
                <div class="despicable">{!! $ad !!}</div>
            @endforeach
        @endsubscribed
    </div>
</div>

This is how clean it looks when we do good use of custom Blade directives. Here, we have namely used three custom ones: @subscribed, @unsubscribed and @endsubscribed. Now, we need them to be functional to fulfill the destiny of our premium users and sponsors. Let's think in code: What @subscribed does is that it only checks if there is a user logged in and if there is one, it further checks if he has a subscription and if he has one: it returns true. The unsubscribed directive is used in place of the else block of the conditional. And finally, the endsubscribed directive ends the conditional with an end if. Let's jot it down.

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Perform post-registration booting of services.
     *
     * @return void
     */
    public function boot()
    {
        Blade::directive('subscribed', function () {
            $conditon = false;

            // check if the user is authenticated
            if (Auth::check()) {
                // check if the user has a subscription
                $condition = Auth::user()->isSubscribed;
            }

            return "<?php if ($condition) { ?>";
        });

        Blade::directive('unsubscribed', function () {
            return "<?php } else { ?>";
        });

        Blade::directive('endsubscribed', function () {
            return "<?php } ?>";
        });
    }
}

Remember to use Illuminate\Support\Facades\Blade; or do \Blade::directive().

Don't worry about the isSubscribed method. It should belong to the User model.

Precautions

Here are three precautions you should keep in mind while writing custom Blade directives.

First Precaution

If you are still in this stance that the $expression returned by the directive's closure is a PHP statement, you are wrong. It is just a plain old string you came to know about when your PHP journey incarnated. Consider a modified version of our first example:

<p>@greet('Hi', 'Hammad')</p>

In the above example, you can't access the first or second argument separately. You get the whole $expression, everything in the parentheses. You need to break them down like this:

\Blade::directive('hello', function ($expression) {
    list($greet, $name) = explode(', ', $expression);

    return "<?php echo {$greet} . ' ' . {$name}; ?>";
});

Let's break this down. On the left part of the assignment operator is the explode function. The explode function takes a string as input and returns an array of strings, each of which is a substring of the input string split by a delimiter which is given as the first argument. In this case, the delimiter is ', ' and the string is $expression. The $expression is broken down into elements of an array which what we wanted, but not exactly. We wanted a variable for each, not array elements. For assigning each element of the $expression array to a variable, we use the list construct to do that. The list construct is used to assign a list of variables in one operation.

Like array(), list() is not really a function, but a language construct.

In this way, you can now access the variables as if they were passed to a normal function.

Second Precaution

Remember to escape your output. When you use {{ }}, Blade already does that for you. To avoid malicious users from injecting JavaScript alerts and dirty other code into your site, remember to escape HTML. You can make use of the Laravel helper function e() which is the equivalent of using htmlentities().

\Blade::directive('hello', function ($expression) {
    return "<?php echo 'Hello ' . e({$expression}); ?>";
});

Third Precaution

After updating the logic of a Blade directive, you will need to delete all of the cached Blade views. The cached Blade views may be removed using the view:clear Artisan command.

php artisan view:clear

You need to run this command every time you make a change to any of your custom Blade directives.

Custom Blade 'If' Directives: New in Laravel 5.5

While making use of custom Blade directives, you will notice that most of them are just some form of conditionals. These require you to register three separate directives: one for the if, one for else and the third one for endif. Luckily, Laravel 5.5 adds support for simplifying these if directives.

You can now register a custom if directive like this:

// AppServiceProvider.php
...
public function boot()
{
    \Blade::if('admin', function () {
        return auth()->check() && auth()->user()->isAdmin();
    });
}

This register the whole trio: admin, else and endadmin.

Conclusion

This is more than all, you need to know about writing custom Blade directives and cleaning your mucked up templates. But what about the duel? This is only one part of your journey to challenge Lord Otwell. There is a lot about Laravel which you need to know, before you...

Happy Coding!

Hammad Ahmed

6 posts

Laravel and Vue developer. Available for freelance work.