Framework: Blade Traits
2021 ∙ November ∙ Osm Framework
2 years ago ∙ 2 minutes read
Currently, I'm working on Osm Admin package, and I need a module to inject its HTML markup around some well-known place in a Blade template. However, Blade template extensibility is not a problem that's specific to Osm Admin project. It's a generic problem. Let's solve that.
Problem
New messages
module will provide JavaScript functions for showing automatically disappearing messages. In order to do that, it has to render a <template>
tag in the end of HTML page during server-side page rendering, and then create HTML from it in JavaScript code.
All page templates are based on <x-std-pages::layout>
Blade component:
<x-std-pages::layout>
...
</x-std-pages::layout>
The <x-std-pages::layout>
uses std-pages::layout
template:
<!doctype html>
<html lang="en">
...
<body>
...
@if(isset($footer))
{{ $footer }}
@else
@include('std-pages::footer')
@endif
...
</body>
</html>
The $footer
here is a Blade component slot that may be filled in by the caller template. Let's say that it will be rendered unconditionally:
...
@include('std-pages::footer')
{{ $footer }}
...
The messages
module could render its <template>
tag just after the {{ footer }}
slot. However, currently there is no way for a module to inject its markup into an existing template.
@around
And @proceed
Directives
A module can inject PHP code into any class method using a dynamic trait. In a similar fashion, a module could inject its markup use a kind of Blade template trait.
Let's say that the messages
module defines a Blade template trait in themes/_base/views/messages/traits/std-pages/layout.blade.php
:
@around({{ $footer }})
@proceed
<template id="message-template">
...
</template>
@endaround
The file path specifies that it's a trait by being in the traits/
subdirectory, and that it extends the std-pages::layout
template.
New @around
directive specifies a text to search - {{ $footer }}
, and a text to replace it with - everything till @endaround
.
New @proceed
directive specifies the position of the injected markup relative to the original slot content. In this example, the injected <template>
tag is injected after the original content.
This way, any module could inject their content before and after the original content.
The same file can contain several @around
directives. If @around
directive goes without parameters, the whole template is replaced:
@around
@proceed
<!-- Everything here goes after closing `</html>` tag -->
@endaround
Implementation
Said and done!
I had to dig into how Blade actually works. Before rendering every template, it compiles it into plain PHP template using Compiler::compileString()
method.
Luckily, I already subclassed the Compiler
class earlier, so I overwrote the compileString()
method. The overwritten method checks if any module has a matching file in views/{module}/traits/{path}
directory, and if so, it reads and applies all the @around
directives to the currently compiled Blade template.
Full implementation is 50-60 lines of code, a lot less than I expected! See Compiler
class for more details.