> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Package Blade extensions and the compilation cache

> Examine the Laravel 13 BladeCompiler to learn how to register custom directives and conditionals, separate compile time from runtime, and verify the view cache when a package is updated.

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](/en/advanced/package-development). For regular template syntax, see [Blade](/en/blade). For view namespaces and publishing, see [Overriding and updating package views](/en/advanced/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.

```mermaid theme={null}
flowchart TD
    A["Register extensions<br>in the provider's boot"] --> B["Render the view"]
    B --> C{"Compilation needed?"}
    C -->|Yes| D["Generate PHP code<br>in the directive callback"]
    D --> E["Store the compiled result"]
    C -->|No| F["Use the stored PHP"]
    E --> G["Execute the PHP to produce HTML"]
    F --> G
    G --> H["Read runtime configuration and arguments"]
```

| API | Role of the registered callback | When it runs |
| - | - | - |
| `Blade::directive()` | Generates a string of PHP code | When the target directive is compiled |
| `Blade::if()` | Evaluates the condition | When the generated PHP calls `Blade::check()` |

`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](/en/advanced/package-discovery) or manually.

```php src/CourierServiceProvider.php theme={null}
<?php

namespace Acme\Courier;

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

class CourierServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Blade::directive('acmeCourierLabel', function (string $expression): string {
            return "<?php echo e((string) config('courier.label', 'Tracking').': '.({$expression})); ?>";
        });

        Blade::if('acmeCourierEnabled', function (): bool {
            return (bool) config('courier.enabled', false);
        });
    }
}
```

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](/en/advanced/package-config-merging).

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

```blade theme={null}
@acmeCourierLabel($code)
```

In this template, the callback receives the string `$code`. The generated PHP looks like this:

```php theme={null}
<?php echo e((string) config('courier.label', 'Tracking').': '.($code)); ?>
```

`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](/en/blade) makes it easier to separate responsibilities.

<Warning>
  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()`.
</Warning>

### 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 theme={null}
@acmeCourierEnabled
    @acmeCourierLabel($code)
@else
    Shipment tracking is disabled.
@endacmeCourierEnabled
```

`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:

```php theme={null}
<?php if (\Illuminate\Support\Facades\Blade::check('acmeCourierEnabled')): ?>
```

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.**

| Change | Effect on compiled PHP |
| - | - |
| Changing the code returned by `directive()` | Stale PHP remains, so recompilation is required |
| Changing only the logic of a `Blade::if()` condition callback | The generated `check()` can call the new logic, but the new code must be loaded |
| Changing a directive name or argument contract | Consumers must update their templates and recompile |
| Views already published to the application | Updating views in the package alone does not update the published copies |

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.

```bash theme={null}
php artisan view:clear
php artisan view:cache
```

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`.

```php theme={null}
public function test_label_directive_compiles_to_runtime_lookup(): void
{
    $compiled = \Illuminate\Support\Facades\Blade::compileString(
        '@acmeCourierLabel($code)',
    );

    $this->assertSame(
        "<?php echo e((string) config('courier.label', 'Tracking').': '.(\$code)); ?>",
        $compiled,
    );
}
```

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.

| Test case | Expected result |
| - | - |
| Pass characters that would be interpreted as HTML in the shipment code | They are escaped by the generated `e()` |
| Use the same compiled view and change the label configuration before rendering | The next render displays the new configuration value |
| Render with `courier.enabled` set to true and false | Only the corresponding branch is displayed |
| Boot a fresh application after `view:cache` | The extensions are registered and the result matches rendering without precompilation |
| Deploy after changing the directive's generated code | The new PHP is used after the cache is rebuilt |
| Update the supported Laravel version | The tests for generated code, rendering, and cache usage all pass |

## Related pages

<Columns cols={2}>
  <Card title="Overriding and updating package views" icon="eye" href="/en/advanced/package-views">
    Review namespace lookup order and how to maintain published templates.
  </Card>

  <Card title="Integrating package caches with optimize" icon="gears" href="/en/advanced/package-optimization">
    Review how to integrate building and clearing custom caches into the deployment process.
  </Card>
</Columns>

## Primary sources consulted

* [Laravel documentation: Extending Blade and custom if statements](https://github.com/laravel/docs/blob/12080c72699b99ec6573f7e79ff4834d9001b6bc/blade.md#extending-blade)
* [Laravel documentation: Package views and components](https://github.com/laravel/docs/blob/12080c72699b99ec6573f7e79ff4834d9001b6bc/packages.md#views)
* [BladeCompiler: directive(), callCustomDirective(), compileStatement(), if(), and check()](https://github.com/laravel/framework/blob/v13.37.0/src/Illuminate/View/Compilers/BladeCompiler.php)
* [Compiler: cache checks with isExpired()](https://github.com/laravel/framework/blob/v13.37.0/src/Illuminate/View/Compilers/Compiler.php)
* [BladeCustomTest: custom directives, conditionals, and overriding built-in syntax](https://github.com/laravel/framework/blob/v13.37.0/tests/View/Blade/BladeCustomTest.php)


## Related topics

- [Advanced Topics](/en/advanced/index.md)
- [Overriding and updating package views](/en/advanced/package-views.md)
- [Views](/en/views.md)
- [Redis](/en/redis.md)
- [Blade templates](/en/blade.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.