Skip to main content
When a package provides Blade syntax, keep the process that converts templates into PHP separate from the process that executes the generated PHP. If you embed the current configuration value at compile time, cached views may keep displaying a stale value. This page refers to the official Laravel 13.x documentation and the Framework v13.37.0 implementation, and builds on package development basics. For regular template syntax, see Blade. For view namespaces and publishing, see Overriding and updating package views.

Distinguish registration, compilation, and execution

The callback you pass to Blade::directive() returns a string of PHP code to generate, not the value to display. The $expression argument is also the PHP expression written in the template, not an evaluated value. view:cache also performs compilation. Design directive generation callbacks so that they do not retrieve the current user, write to the database, call external APIs, or perform similar work.

Register with a package-specific name

As an example, the package provides @acmeCourierLabel($code), which displays a configured label before a shipment code. This assumes that Composer’s PSR-4 maps Acme\Courier\ to src/ and that the provider is registered through auto-discovery or manually.
src/CourierServiceProvider.php
In this example, the configuration contract is that courier.label is a string and courier.enabled is a boolean. For distributing and merging configuration files, see Package config merging and caching. Register the extensions in the provider’s regular boot() method, and do not wrap them in a runningInConsole() check. The same extensions are needed both for HTTP rendering and for Artisan’s view:cache. Also avoid making a provider that only registers Blade extensions a deferred provider that does not boot until another service is resolved.

Receive arguments as expressions, not values

In this template, the callback receives the string $code. The generated PHP looks like this:
config() and $code are evaluated when this PHP runs. This differs from an implementation in which the callback itself reads config() and returns a fixed string. The callCustomDirective() implementation we examined strips the outer parentheses, trims the expression, and passes it to the callback. It does not convert the argument into a PHP value or split it into individual arguments. This example exposes the directive as syntax that takes exactly one required expression. If you need to handle multiple arguments, do not simply explode() on commas. Arrays, method calls, and string literals can also contain commas. If you want to handle attributes or complex HTML, providing a Blade component makes it easier to separate responsibilities.
Template expressions are PHP written by trusted developers. Do not build template source from user input. Also note that echo in a custom directive does not automatically get the escaping that regular {{ }} provides. In this example, the generated PHP explicitly calls e().

Name collisions are also a compatibility issue

directive() stores handlers in an array keyed by name. Registering the same name overwrites the previous registration, and compileStatement() chooses custom directives before the built-in compilation methods. The official tests even include a case in which a custom registration overrides foreach. Avoid generic names such as label or enabled and built-in names, and add a package-specific prefix. Treat the names and arguments that consumers use in templates as public API, and provide migration steps when you change them.

Evaluate conditionals at runtime with Blade::if

The condition registered in the provider above can be used as follows:
Blade::if() stores the condition callback and then registers directives for the opening, unless, else, and closing parts. In this example, the opening part is converted into the following PHP:
The condition callback that checks the configuration is called at runtime from check(). It does not fix the true or false result at compile time. HTTP processes that use compiled views also need the condition callback to be registered. This syntax toggles what is displayed; it is not authorization for shipment operations. Enforce access control in routes and in the operations themselves as well.

Package updates do not necessarily regenerate views automatically

The Compiler::isExpired() implementation we examined checks whether a cache exists and, depending on configuration, compares the modification times of the source view and the compiled file. It does not track changes to directives written in a provider as dependencies. The official documentation also advises clearing the cache with view:clear after changing directive logic. Run it in the consuming application’s deployment process after the updated package and the consumer’s views are in sync.
Run view:cache when you precompile views in production. Do not clear the cache from the package’s boot() on every request, and do not modify the consuming application’s deployment process without permission during installation. Refreshing the configuration cache and reloading long-running processes should also follow the environment’s regular deployment procedure.

Verify compiled output and rendered output separately

First, verify the generated PHP in a test Laravel application where the provider above is registered. The following method is added to a Feature test that extends Tests\TestCase.
This test confirms the contract for generating PHP, but it cannot verify runtime escaping or conditional branches. In a real package, also add tests for view rendering and for updates.

Overriding and updating package views

Review namespace lookup order and how to maintain published templates.

Integrating package caches with optimize

Review how to integrate building and clearing custom caches into the deployment process.

Primary sources consulted

Last modified on October 17, 2026