Skip to main content
Custom directives let you extend the template language with your own #name($expression) syntax. A directive is a PHP callable that receives the expression string from the template and returns the PHP source code to embed in the compiled output.

How custom directives work

When Lex compiles a template, it encounters #directivename($expression) and looks up directivename in the DirectiveRegistry. If a handler is registered, Lex calls it with the raw expression string and splices the returned PHP code directly into the compiled template file.
Directive handlers run at compile time, not at render time. The callable executes once when the template is compiled to PHP. On subsequent requests the compiled file is served from cache — your handler is not called again until the cache is cleared or the template changes.

Registering a directive

Call directive() on the Lexer instance. The method is fluent and returns $this.
$lexer->directive(string $name, callable $handler): static
Handler signature:
callable(string $expression): string
The $expression parameter is the raw string inside the parentheses in the template — exactly as the template author wrote it, including any leading/trailing whitespace. Your handler must return a valid PHP code string (typically a <?php ... ?> tag).

Examples

Formats a Unix timestamp or date string using date().
$lexer->directive('datetime', function (string $expression): string {
    return "<?php echo date('d/m/Y H:i', strtotime({$expression})); ?>";
});
Template:
<p>Published: #datetime($post->created_at)</p>
<p>Updated:   #datetime($post->updated_at)</p>
Compiled output:
<p>Published: <?php echo date('d/m/Y H:i', strtotime($post->created_at)); ?></p>
<p>Updated:   <?php echo date('d/m/Y H:i', strtotime($post->updated_at)); ?></p>

Registering multiple directives

Chain directive() calls for a clean setup:
use Wik\Lexer\Lexer;

$lexer = (new Lexer())
    ->paths([__DIR__ . '/views'])
    ->directive('datetime',  fn($e) => "<?php echo date('d/m/Y H:i', strtotime({$e})); ?>")
    ->directive('money',     fn($e) => "<?php echo '$' . number_format((float)({$e}), 2); ?>")
    ->directive('uppercase', fn($e) => "<?php echo strtoupper((string)({$e})); ?>");

Template syntax

Custom directives use the same #name($expression) syntax as built-in directives:
#directivename($expression)
The expression is anything inside the outer parentheses. You can pass variables, method calls, or PHP expressions:
#datetime($post->created_at)
#datetime(strtotime('+7 days'))
#money($cart->subtotal + $cart->shipping)
#uppercase($user->displayName())
To output a literal # without triggering a directive, prefix it with a backslash:
\#datetime($post->created_at)  {{-- renders literally: #datetime($post->created_at) --}}

Directive name rules

  • Must start with a letter (az, AZ).
  • May contain letters, digits, and underscores — no spaces or hyphens.
  • Names are case-sensitive: datetime and DateTime are distinct.
  • Registering a name that matches a built-in directive name (e.g. foreach, if) is not recommended and may produce unexpected behaviour.

Testing a custom directive

You can unit-test your directive handler independently by constructing a DirectiveRegistry and Compiler directly:
use PHPUnit\Framework\TestCase;
use Wik\Lexer\Cache\FileCache;
use Wik\Lexer\Compiler\Compiler;
use Wik\Lexer\Support\DirectiveRegistry;

final class MoneyDirectiveTest extends TestCase
{
    public function testMoneyDirectiveOutput(): void
    {
        $registry = new DirectiveRegistry();
        $registry->register(
            'money',
            fn($e) => "<?php echo '$' . number_format((float)({$e}), 2); ?>"
        );

        $compiler = new Compiler(
            $registry,
            new FileCache(sys_get_temp_dir() . '/lexer_test_' . uniqid())
        );

        $nodes = $compiler->parse('#money($order->total)');

        $compiled = '';
        foreach ($nodes as $node) {
            $compiled .= $node->compile();
        }

        $this->assertStringContainsString('number_format', $compiled);
        $this->assertStringContainsString('$order->total', $compiled);
    }
}

Sandbox mode restriction

Custom directives are forbidden in SandboxConfig::secure() mode. If a user-submitted template references a custom directive and sandbox mode blocks custom directives, Lex throws a TemplateSyntaxException at compile time.You can selectively re-allow specific trusted directives using withAllowedDirectives():
use Wik\Lexer\Security\SandboxConfig;

$config = SandboxConfig::secure()
    ->withAllowedDirectives(['datetime', 'money']);

$lexer->enableSandbox()->setSandboxConfig($config);

Build docs developers (and LLMs) love