Introduction

Enclaves

Enclaves are a Forte feature that allows you transform specific parts of your application, safely extending Blade to add new features or change behaviors without affecting third-party packages or other parts of your project. The easiest way to think about Enclaves is that they are isolated compilation environments for Blade.

As an example of what Enclaves allow you to do, suppose we had the following Blade template:

1@php
2 $people = [
3 'Alice',
4 'Bob',
5 'Charlie',
6 ];
7@endphp
8
9
10<ul>
11 <li #foreach="$users as $user">{{ $user }}</li>
12</ul>

The #foreach syntax is not valid Blade by default. However, we can add this functionality to our application like so:

1<?php
2
3namespace App\Providers;
4
5use Illuminate\Support\ServiceProvider;
6use Forte\Facades\Forte;
7
8class AppServiceProvider extends ServiceProvider
9{
10 public function boot(): void
11 {
12 Forte::app()
13 ->elementForeachAttributes();
14 }
15}

The elementForeachAttributes method will enable a document rewriter that will compile the #foreach syntax into valid Blade behind the scenes; because you are using Forte's Enclave feature, it will apply only to your application's views.

#Default Rewriters

Forte provides a number of default rewriters:

#Conditional Elements

The conditional elements rewriter allows you to author Blade templates like so:

1<div #if="count($people) > 0">
2 ...
3</div>
4<p #else>
5 The count of people was not greater than zero.
6</p>

The above template is equivalent to the following Blade:

1@if (count($people) > 0)
2 <div>...</div>
3@else
4 <p>The count of people was not greater than zero.</p>
5@endif

You can also use #else-if to create conditional chains across sibling elements:

1<div #if="$status === 'active'">
2 Active user
3</div>
4<div #else-if="$status === 'pending'">
5 Pending approval
6</div>
7<p #else>
8 Unknown status
9</p>

This is equivalent to the following Blade:

1@if ($status === 'active')
2 <div>Active user</div>
3@elseif ($status === 'pending')
4 <div>Pending approval</div>
5@else
6 <p>Unknown status</p>
7@endif

To enable conditional elements, call the elementConditionalAttributes method on the target Enclave:

1<?php
2
3namespace App\Providers;
4
5use Illuminate\Support\ServiceProvider;
6use Forte\Facades\Forte;
7
8class AppServiceProvider extends ServiceProvider
9{
10 public function boot(): void
11 {
12 Forte::app()
13 ->elementConditionalAttributes();
14 }
15}

The remaining examples in this section show only the Enclave method call. Register each one in your service provider's boot method the same way.

#Element Foreach Attributes

The foreach attribute rewriter allows using Blade's @foreach directive as an HTML attribute:

1<ul>
2 <li #foreach="$users as $user">{{ $user }}</li>
3</ul>

The above template is equivalent to the following Blade:

1<ul>
2 @foreach ($users as $user)
3 <li>{{ $user }}</li>
4 @endforeach
5</ul>

To enable foreach attributes:

1<?php
2
3Forte::app()
4 ->elementForeachAttributes();

#Element Forelse Attributes

The forelse rewriter allows you to write Blade's forelse and empty directives using HTML attributes:

1<li #forelse="$users as $user">{{ $user->name }}</li>
2<p #empty>No users</p>

The above template is equivalent to the following Blade:

1@forelse ($users as $user)
2 <li>{{ $user->name }}</li>
3@empty
4 <p>No users</p>
5@endforelse

To enable forelse attributes:

1<?php
2
3Forte::app()
4 ->elementForelseAttributes();

#Mixed PHP Directives

The mixed PHP directives rewriter analyzes your Blade template, and recompiles instances of the @php (...) directive, allowing for its safe usage in templates that also use the @php ... @endphp block form:

1@php ($name = '...')
2
3@php
4 $greeting = 'Hello, ' . $name;
5@endphp

To enable mixed PHP directives:

1<?php
2
3Forte::app()
4 ->allowMixedPhpDirectives();

#Directive Argument Hoisting

The directive argument hoisting rewriter will automatically "hoist" the first argument supplied to certain directives. It does this by creating a temporary variable and passing that to the directive instead.

As an example, the following Blade template:

1@json([
2 'hello',
3 'names' => [
4 'one',
5 ],
6])

would typically throw a ParseError with a message similar to the following:

1Unclosed '[' on line 3 does not match ')'

However, with Forte's directive argument hoisting, the output would become the expected JSON structure:

1{"0":"hello","names":["one"]}

To enable directive argument hoisting:

1<?php
2
3Forte::app()
4 ->hoistDirectiveArguments();

#Combining Attribute Directives

You can place multiple attribute directives on the same HTML element. When you enable more than one attribute directive rewriter, Forte coordinates them so they interact correctly on shared elements.

#Attribute Order Determines Nesting

When an element carries multiple attribute directives, the leftmost attribute becomes the outermost wrapper in the compiled output. You control nesting through attribute order:

1<ul #if="$show" #foreach="$items as $item">
2 <li>{{ $item }}</li>
3</ul>

Because #if appears before #foreach, the conditional wraps the loop. This is equivalent to:

1@if ($show)
2 @foreach ($items as $item)
3 <ul>
4 <li>{{ $item }}</li>
5 </ul>
6 @endforeach
7@endif

Reversing the attribute order reverses the nesting:

1<ul #foreach="$items as $item" #if="$item->visible">
2 <li>{{ $item }}</li>
3</ul>

Here #foreach is outermost and #if filters inside the loop. This is equivalent to:

1@foreach ($items as $item)
2 @if ($item->visible)
3 <ul>
4 <li>{{ $item }}</li>
5 </ul>
6 @endif
7@endforeach

Only attribute order matters
The order in which you call elementConditionalAttributes(), elementForeachAttributes(), and elementForelseAttributes() in your service provider does not affect nesting. Only the left-to-right order of attributes in your HTML determines which directive wraps which.

#Conditional Sibling Pairing

When #if or #else-if appears on an element, Forte looks at the next sibling element to check whether it carries #else-if or #else. If it does, the two elements are connected into a single conditional chain. Whitespace-only text nodes between siblings (blank lines, indentation) are skipped during this search:

1<div #if="$isAdmin">
2 Admin controls
3</div>
4
5<div #else-if="$isModerator">
6 Moderator controls
7</div>
8
9<p #else>
10 Standard view
11</p>

The blank lines between elements do not break the chain. Forte skips whitespace text nodes and pairs the siblings correctly.

If a non-whitespace element or text node sits between an #if element and a potential #else sibling, the chain breaks and each conditional is treated independently:

1<div #if="$a">A</div>
2<p>separator</p>
3<div #else>This will not pair with the #if above</div>

#Multiple Conditionals on One Element

When an element has more than one #if attribute, only the leftmost #if pairs with #else-if or #else siblings. Additional #if attributes on the same element wrap independently and do not connect to sibling branches:

1<ul #if="$hasPermission" #foreach="$items as $item" #if="$item->visible">
2 <li>{{ $item }}</li>
3</ul>
4<p #else>No permission</p>

The first #if="$hasPermission" (leftmost) pairs with the <p #else> sibling. The second #if="$item->visible" wraps inside the #foreach independently, with no connection to the #else.

#Enabling Multiple Default Rewriters

Enable multiple default rewriters by chaining their method calls. As an example, the following enables all default rewriters:

1<?php
2
3namespace App\Providers;
4
5use Illuminate\Support\ServiceProvider;
6use Forte\Facades\Forte;
7
8class AppServiceProvider extends ServiceProvider
9{
10 public function boot(): void
11 {
12 Forte::app()
13 ->elementConditionalAttributes()
14 ->elementForeachAttributes()
15 ->elementForelseAttributes()
16 ->allowMixedPhpDirectives()
17 ->hoistDirectiveArguments();
18 }
19}

#Changing the Default Attribute Prefix

The default attribute-based rewriters all use # as a prefix:

1<div #if="$total > 0">
2 ...
3</div>

The # prefix was chosen to help prevent collisions and compatibility issues with frameworks such as Livewire, Alpine, and Vue.js. You can change this to any prefix you prefer, but doing so may cause conflicts that are difficult to debug.

To change the prefix, pass the desired prefix as the first argument to all rewriters:

1<?php
2
3namespace App\Providers;
4
5use Illuminate\Support\ServiceProvider;
6use Forte\Facades\Forte;
7
8class AppServiceProvider extends ServiceProvider
9{
10 public function boot(): void
11 {
12 Forte::app()
13 ->elementConditionalAttributes('my-prefix')
14 ->elementForeachAttributes('my-prefix')
15 ->elementForelseAttributes('my-prefix');
16 }
17}

#Registering Custom Rewriters

Beyond the built-in rewriters, you can register your own transformers on an enclave. Forte supports three registration methods depending on the transformer type.

#Visitor Rewriters

The use method registers RewriteVisitor implementations. You can pass a class name, an instance, or an array:

1<?php
2
3use Forte\Enclaves\Enclave;
4use Forte\Rewriting\NodePath;
5use Forte\Rewriting\Visitor;
6
7class HighlightVisitor extends Visitor
8{
9 public function enter(NodePath $path): void
10 {
11 // Visitor logic
12 }
13}
14
15$enclave = new Enclave;
16$enclave->use(HighlightVisitor::class);
17
18$enclave->hasRewriters(); // true
19$enclave->rewriterCount(); // 1

When registering multiple rewriters at once, pass an array to use or call useMany:

1<?php
2
3use Forte\Enclaves\Enclave;
4use Forte\Rewriting\NodePath;
5use Forte\Rewriting\Visitor;
6
7class VisitorA extends Visitor
8{
9 public function enter(NodePath $path): void {}
10}
11
12class VisitorB extends Visitor
13{
14 public function enter(NodePath $path): void {}
15}
16
17$enclave = new Enclave;
18$enclave->use([VisitorA::class, VisitorB::class]);
19
20$enclave->rewriterCount(); // 2

#AST Rewriters

The apply method registers AstRewriter instances such as the built-in rewrite passes:

1<?php
2
3use Forte\Enclaves\Enclave;
4use Forte\Facades\Forte;
5use Forte\Rewriting\Passes\Elements\AddClass;
6use Forte\Rewriting\Passes\Elements\RenameTag;
7
8$enclave = new Enclave;
9$enclave->apply(
10 new AddClass('div', 'mt-4'),
11 new RenameTag('b', 'strong')
12);
13
14$doc = Forte::parse('<div><b>Hello</b></div>');
15$result = $enclave->transformDocument($doc);
16
17$result->render(); // '<div class="mt-4"><strong>Hello</strong></div>'

#Callback Transformers

The transform method registers a closure that receives each NodePath during traversal:

1<?php
2
3use Forte\Ast\Elements\ElementNode;
4use Forte\Enclaves\Enclave;
5use Forte\Facades\Forte;
6use Forte\Rewriting\NodePath;
7
8$enclave = new Enclave;
9$enclave->transform(function (NodePath $path): void {
10 $node = $path->node();
11
12 if ($node instanceof ElementNode && $node->tagNameText() === 'b') {
13 $path->replaceWith('<strong>'.$node->innerContent().'</strong>');
14 }
15});
16
17$doc = Forte::parse('<b>Hello</b>');
18$result = $enclave->transformDocument($doc);
19
20$result->render(); // '<strong>Hello</strong>'

#Rewriter Priority

The use method accepts an optional second argument to control execution order. Higher priority rewriters run first:

1<?php
2
3use Forte\Enclaves\Enclave;
4use Forte\Rewriting\NodePath;
5use Forte\Rewriting\Visitor;
6
7class LowPriorityVisitor extends Visitor
8{
9 public function enter(NodePath $path): void {}
10}
11
12class HighPriorityVisitor extends Visitor
13{
14 public function enter(NodePath $path): void {}
15}
16
17$enclave = new Enclave;
18$enclave->use(LowPriorityVisitor::class, 0);
19$enclave->use(HighPriorityVisitor::class, 10);
20
21$ordered = $enclave->getRewritersInPriorityOrder();
22
23$ordered[0]; // "HighPriorityVisitor" (priority 10 runs first)
24$ordered[1]; // "LowPriorityVisitor" (priority 0 runs second)

Callback and AstRewriter transformers always run after visitor rewriters, in the order they were registered.

#Managing Rewriters

You can inspect and modify an enclave's registered rewriters at any time:

1<?php
2
3use Forte\Enclaves\Enclave;
4use Forte\Rewriting\NodePath;
5use Forte\Rewriting\Visitor;
6
7class ManagedVisitor extends Visitor
8{
9 public function enter(NodePath $path): void {}
10}
11
12$enclave = new Enclave;
13
14$enclave->hasRewriters(); // false
15$enclave->use(ManagedVisitor::class);
16$enclave->hasRewriter(ManagedVisitor::class); // true
17$enclave->rewriterCount(); // 1
18
19$enclave->removeRewriter(ManagedVisitor::class);
20$enclave->hasRewriter(ManagedVisitor::class); // false
21
22$enclave->use(ManagedVisitor::class);
23$enclave->clearRewriters();
24$enclave->rewriterCount(); // 0

#Path Patterns

Each Enclave uses glob-like include and exclude patterns to determine which files it applies to. The include method adds paths that should be processed, and exclude removes paths from consideration:

1<?php
2
3use Forte\Enclaves\Enclave;
4
5$enclave = new Enclave;
6$enclave->include('/app/views/**');
7$enclave->exclude('/app/views/emails/**');
8
9$enclave->matches('/app/views/home.blade.php'); // true
10$enclave->matches('/app/views/emails/welcome.blade.php'); // false
11$enclave->matches('/other/path/file.blade.php'); // false

Patterns support * (matches any characters within a single directory segment) and ** (matches zero or more directory segments). When both include and exclude patterns match a path, the more specific pattern wins based on a heuristic scoring system. You can pass multiple patterns at once:

1<?php
2
3use Forte\Enclaves\Enclave;
4
5$enclave = new Enclave;
6$enclave->include(['/views/**', '/components/**']);
7$enclave->exclude('/views/vendor/**');
8
9$enclave->matches('/views/home.blade.php'); // true
10$enclave->matches('/components/alert.blade.php'); // true
11$enclave->matches('/views/vendor/pkg/view.blade.php'); // false

#Custom Enclaves

By default, Forte creates a single application enclave that covers resource_path('views/**') while excluding views/vendor/** and views/mail/**. You access it with Forte::app().

To create additional enclaves for different parts of your application, use Forte::create:

1<?php
2
3namespace App\Providers;
4
5use Forte\Facades\Forte;
6use Forte\Rewriting\Passes\Elements\AddClass;
7use Illuminate\Support\ServiceProvider;
8
9class AppServiceProvider extends ServiceProvider
10{
11 public function boot(): void
12 {
13 // Create an enclave for admin views
14 Forte::create('admin')
15 ->include(resource_path('views/admin/**'))
16 ->apply(new AddClass('table', 'admin-table'));
17
18 // The default app enclave for everything else
19 Forte::app()
20 ->elementConditionalAttributes()
21 ->elementForeachAttributes();
22 }
23}

Enclave names must not start with { (reserved for internal enclaves like {app}). When a file path matches multiple enclaves, all matching enclaves contribute their rewriters.

You can check for registered enclaves using the manager methods:

1<?php
2
3use Forte\Enclaves\EnclavesManager;
4
5$manager = app(EnclavesManager::class);
6
7$manager->has('{app}'); // true (always present)
8$manager->count(); // 1
9
10$manager->create('admin')
11 ->include('/views/admin/**');
12
13$manager->has('admin'); // true
14$manager->count(); // 2
15$manager
16 ->get('admin')
17 ->matches('/views/admin/dashboard.blade.php'); // true

The names method returns all registered enclave names, and all returns the full map keyed by name:

1<?php
2
3use Forte\Enclaves\EnclavesManager;
4
5$manager = app(EnclavesManager::class);
6$manager->create('admin');
7$manager->create('emails');
8
9$manager->names(); // ["{app}", "admin", "emails"]
10$manager->count(); // 3

#Vendor Package Views

By default, the app enclave excludes views/vendor/**. Forte provides several methods to control which vendor views receive transformations.

#Including Vendor Packages

To include specific vendor package views in the default app enclave, use includeVendorPackages on the facade:

1<?php
2
3namespace App\Providers;
4
5use Forte\Facades\Forte;
6use Illuminate\Support\ServiceProvider;
7
8class AppServiceProvider extends ServiceProvider
9{
10 public function boot(): void
11 {
12 Forte::includeVendorPackages('notifications', 'pagination');
13 }
14}

This adds resource_path('views/vendor/{package}/**') as an include pattern. Because include patterns are more specific than the broad views/vendor/** exclude, the vendor views are processed.

To include all vendor views at once, call includeVendor:

1<?php
2
3namespace App\Providers;
4
5use Forte\Facades\Forte;
6use Illuminate\Support\ServiceProvider;
7
8class AppServiceProvider extends ServiceProvider
9{
10 public function boot(): void
11 {
12 Forte::includeVendor();
13 }
14}

#Excluding Vendor Packages

If you have included all vendor views but want to exclude specific packages, use excludeVendorPackages:

1<?php
2
3namespace App\Providers;
4
5use Forte\Facades\Forte;
6use Illuminate\Support\ServiceProvider;
7
8class AppServiceProvider extends ServiceProvider
9{
10 public function boot(): void
11 {
12 Forte::includeVendor();
13 Forte::excludeVendorPackages('debugbar');
14 }
15}

#Package-Specific Enclaves

For package authors who need their own isolated transformation rules, createForPackage creates an enclave named vendor:{package} that automatically includes the package's published views and, optionally, its source views:

1<?php
2
3namespace App\Providers;
4
5use Forte\Facades\Forte;
6use Illuminate\Support\ServiceProvider;
7
8class AppServiceProvider extends ServiceProvider
9{
10 public function boot(): void
11 {
12 Forte::createForPackage('acme/widgets')
13 ->elementForeachAttributes()
14 ->elementConditionalAttributes();
15 }
16}

This creates an enclave that targets resource_path('views/vendor/acme/widgets/**'). You can also provide the package's root path to include the source views directly:

1<?php
2
3namespace Acme\Widgets;
4
5use Forte\Facades\Forte;
6use Illuminate\Support\ServiceProvider;
7
8class WidgetsServiceProvider extends ServiceProvider
9{
10 public function boot(): void
11 {
12 Forte::createForPackage('acme/widgets', __DIR__.'/..')
13 ->elementForeachAttributes();
14 }
15}

The second argument adds {packagePath}/resources/views/** as an additional include pattern, so both published and source views are covered.

#See also

  • Rewriters: Write custom rewriters for advanced transformations
  • Rewrite Passes: Pre-built transformations for common tasks
  • Instrumentation: Inject tracking markers for debugging and profiling
  • Documents: Full API for working with parsed documents