Though it’s far from the top of the list of most celebrated features, Laravel’s Blade templating engine makes it really nice to work with data on the front-end of our applications. With built-in helpers for handling loops, conditionals, and sub-views, Blade gives us a nice way to write dynamic templates that don’t feel like a bunch of PHP mixed in with HTML.
Were you aware you can author your own Laravel Blade directives? The syntax is probably a little under-documented, but it can be an incredibly useful tool if you find yourself applying the same patterns over and over. In this post, I want to show you a Blade directive I find myself using in pretty much every application I build: @activeIfInRouteGroup
.
What does it do?
In practice, @activeIfInRouteGroup
is pretty simple — we often want to apply a specific class to “active” links in our primary navigation; if we’re on the “My Account” page, for example, it’s nice to distinguish that link as active in the site navigation.
Normally, we’d accomplish this by retrieving the current route name (or URL), then comparing it against the link to see if the link is pointing to the page we’re already on. While this works, it gets awfully repetitive. What if instead you could specify “mark this as an active link if we’re on any page with a route matching this pattern?”
This is where @activeIfInRouteGroup
comes in! By adding a single line to each link, we’re able to simplify the process!
1 2 3 4 5 6 7 8 9 10 11 |
<ul class="nav"> <li class="@activeIfInRouteGroup('news.*')"> <a href="{{ route('news.index') }}">News</a> </li> <li class="@activeIfInRouteGroup('reports.*')"> <a href="{{ route('reports.index') }}">Reports</a> </li> <li class="@activeIfInRouteGroup('users.*')"> <a href="{{ route('users.index') }}">Users</a> </li> </ul> |
Whether we’re on reports.index
, reports.show
, reports.edit
, or reports.literallyAnythingElse
, the <li>
surrounding the “Reports” link will always be given the class of “active
“!
Defining our custom Blade directive
Custom Blade directives are registered in the boot()
method of the AppServiceProvider
, located in app/Providers/AppServiceProvider.php
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?php namespace App\Providers; use Illuminate\Support\Facades\Blade; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { /** * Bootstrap any application services. * * @return void */ public function boot() { Blade::directive('activeIfInRouteGroup', function ($expression) { return "<?php echo Route::currentRouteNamed($expression) ? 'active' : ''; ?>"; }); } } |
The magic here comes from Laravel’s Route::currentRouteNamed()
method, which accepts one or more patterns and returns whether or not the current route matches the pattern(s).
Assuming you’re naming your routes within Laravel, this enables you to say “I’m at some route that starts with ‘reports’, so I want to make sure that the ‘Reports’ link is highlighted.”
By the way, that’s the entire Blade directive. Let’s look at it again:
1 2 3 |
Blade::directive('activeIfInRouteGroup', function ($expression) { return "<?php echo Route::currentRouteNamed($expression) ? 'active' : ''; ?>"; }); |
The Blade::directive()
method accepts two arguments:
- The directive name, which will used to create the Blade directive beginning with “@” (e.g.
@activeIfInRouteGroup
) - A callback that accepts the value that was passed into the directive and returns a PHP expression.
This is where writing custom Laravel Blade directives gets a little confusing. Remember: we’re not writing the output of the directive, but rather the PHP that should appear in the page.
Laravel’s Blade syntax by itself isn’t valid HTML, so it has to be transpiled down into regular HTML so that a browser can understand it. Blade is what’s commonly referred to as “syntactic sugar”: it makes writing the code nicer, but under the hood it’s really using the core features of the language.
For example, imagine the following Blade file:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<main> @if ($reports->empty()) <p>No reports have been generated yet!</p> @else <ul class="report-list"> @foreach ($reports as $report) <li> <a href="{{ route('reports.show', ['report' => $report]) }}">{{ $report->title }}</a> </li> @endforeach </ul> @endif </main> |
When you load the page, Laravel’s Blade engine is going to attempt to create a standard PHP template from this file, which will get stored in storage/framework/views
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<main> <?php if ($reports->empty()): ?> <p>No reports have been generated yet!</p> <?php else: ?> <ul class="report-list"> <?php foreach ($reports as $report): ?> <li> <a href="<?php echo htmlspecialchars(route('reports.show', ['report' => $report])); ?>"><?php echo htmlspecialchars($report->title); ?></a> </li> <?php endforeach; ?> </ul> <?php endif; ?> </main> |
It’s the same code — and you could write regular PHP like that in your views and skip over Blade entirely — but Blade is a little nicer to write. In our case, our @activeIfInRouteGroup
directive turns into:
1 |
<?php echo Route::currentRouteNamed($expression) ? 'active' : ''; ?> |
Remember: the $expression
that’s passed to the directive doesn’t get handled by the directive definition, but is the value that will be passed to the PHP statement we’re defining. Instead, the directive definition tells the Blade engine what PHP should be injected.
Testing Laravel Blade directives
Another stumbling block is writing unit tests for custom Laravel Blade directives: since our Blade::directive()
call is registering a callback that’s responsible for returning a PHP statement, testing Blade directives requires getting a little bit meta:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
<?php namespace Tests\Unit; use Illuminate\Support\Facades\Route; use Tests\TestCase; /** * Tests for custom Blade directives, defined in AppServiceProvider.php. * * @group Templates */ class BladeTest extends TestCase { private $blade; public function setUp(): void { parent::setUp(); $this->blade = resolve('blade.compiler'); } /** * @test */ public function activeIfInRouteGroup_should_print_active_when_true() { Route::shouldReceive('currentRouteNamed') ->once() ->andReturn(true); $this->assertDirectiveOutput( 'active', '@activeIfInRouteGroup($group)', ['group' => 'groupname'], 'Expected to see "active" printed to the screen.' ); } /** * @test */ public function activeIfInRouteGroup_should_print_nothing_when_false() { Route::shouldReceive('currentRouteNamed') ->once() ->andReturn(false); $this->assertDirectiveOutput( '', '@activeIfInRouteGroup($group)', ['group' => 'groupname'], 'Expected to not see anything printed.' ); } /** * Evaluate a Blade expression with the given $variables in scope. * * @param string $expected The expected output. * @param string $expression The Blade directive, as it would be written in a view. * @param array $variables Variables to extract() into the scope of the eval() statement. * @param string $message A message to display if the output does not match $expected. */ protected function assertDirectiveOutput( string $expected, string $expression = '', array $variables = [], string $message = '' ) { $compiled = $this->blade->compileString($expression); /* * Normally using eval() would be a big no-no, but when you're working on a templating * engine it's difficult to avoid. */ ob_start(); extract($variables); eval(' ?>' . $compiled . '<?php '); $output = ob_get_clean(); $this->assertEquals($expected, $output, $message); } } |
In this test class, we have four methods:
setUp()
, which is a standard PHPUnit fixture method. The only thing special we’re doing is ensuring that the Blade compiler is loaded and available at$this->blade
.activeIfInRouteGroup_should_print_active_when_true()
, which verifies that “active” is echoed whenRoute::currentRouteName()
returns true.activeIfInRouteGroup_should_print_active_when_false()
, which verifies our negative case (e.g. we’re not in the given route group)assertDirectiveOutput()
, a custom PHPUnit assertion that compiles the Blade directive, then compares the output against our expected output.
In each of the two test methods, we’re using the Route
facade’s built-in Mockery capabilities to create a test double — when the Blade directive calls Route::currentRouteNamed()
, we’re able to stub the value. With our test double in place, we don’t have to worry about what the actual current route is, as we’ve told the Route
facade what it should return.
The assertDirectiveOutput()
method gets a little gnarly, as we need to rely on the infamous, “Oh God, I hope I never have to use this” eval()
PHP construct. We test the directive output by following three steps:
- Compile the Blade directive into PHP that we can execute.
- Start PHP’s output buffering, then
extract
some variables into the scope (e.g. make sure$group
isn’t seen as undefined) - Run
eval()
on our compiled Blade directive, then retrieve the output buffer.
If all goes according to plan, we’ll have either “active” or an empty string in the $output
variable, depending on whether or not Route::currentRouteNamed()
returned true or false, respectively.
That’s it!
Congratulations, we’ve written (and tested) our first custom Laravel Blade directive!
As you go on to write more directives of your own, remember: the callback passed to Blade::directive()
should return the PHP expression to be evaluated, not the actual result! Once you’re able to remember that, you’ve unlocked the secret to writing your own custom Blade directives!
Leave a Reply