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:
@php
$people = [
'Alice',
'Bob',
'Charlie',
];
@endphp
<ul>
<li #foreach="$users as $user">{{ $user }}</li>
</ul>
The #foreach syntax is not valid Blade by default. However, we can add this functionality to our application like so:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Forte\Facades\Forte;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Forte::app()
->elementForeachAttributes();
}
}
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:
<div #if="count($people) > 0">
...
</div>
<p #else>
The count of people was not greater than zero.
</p>
The above template is equivalent to the following Blade:
@if (count($people) > 0)
<div>...</div>
@else
<p>The count of people was not greater than zero.</p>
@endif
You can also use #else-if to create conditional chains across sibling elements:
<div #if="$status === 'active'">
Active user
</div>
<div #else-if="$status === 'pending'">
Pending approval
</div>
<p #else>
Unknown status
</p>
This is equivalent to the following Blade:
@if ($status === 'active')
<div>Active user</div>
@elseif ($status === 'pending')
<div>Pending approval</div>
@else
<p>Unknown status</p>
@endif
To enable conditional elements, call the elementConditionalAttributes method on the target Enclave:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Forte\Facades\Forte;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Forte::app()
->elementConditionalAttributes();
}
}
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:
<ul>
<li #foreach="$users as $user">{{ $user }}</li>
</ul>
The above template is equivalent to the following Blade:
<ul>
@foreach ($users as $user)
<li>{{ $user }}</li>
@endforeach
</ul>
To enable foreach attributes:
<?php
Forte::app()
->elementForeachAttributes();
#Element Forelse Attributes
The forelse rewriter allows you to write Blade's forelse and empty directives using HTML attributes:
<li #forelse="$users as $user">{{ $user->name }}</li>
<p #empty>No users</p>
The above template is equivalent to the following Blade:
@forelse ($users as $user)
<li>{{ $user->name }}</li>
@empty
<p>No users</p>
@endforelse
To enable forelse attributes:
<?php
Forte::app()
->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:
@php ($name = '...')
@php
$greeting = 'Hello, ' . $name;
@endphp
To enable mixed PHP directives:
<?php
Forte::app()
->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:
@json([
'hello',
'names' => [
'one',
],
])
would typically throw a ParseError with a message similar to the following:
Unclosed '[' on line 3 does not match ')'
However, with Forte's directive argument hoisting, the output would become the expected JSON structure:
{"0":"hello","names":["one"]}
To enable directive argument hoisting:
<?php
Forte::app()
->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:
<ul #if="$show" #foreach="$items as $item">
<li>{{ $item }}</li>
</ul>
Because #if appears before #foreach, the conditional wraps the loop. This is equivalent to:
@if ($show)
@foreach ($items as $item)
<ul>
<li>{{ $item }}</li>
</ul>
@endforeach
@endif
Reversing the attribute order reverses the nesting:
<ul #foreach="$items as $item" #if="$item->visible">
<li>{{ $item }}</li>
</ul>
Here #foreach is outermost and #if filters inside the loop. This is equivalent to:
@foreach ($items as $item)
@if ($item->visible)
<ul>
<li>{{ $item }}</li>
</ul>
@endif
@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:
<div #if="$isAdmin">
Admin controls
</div>
<div #else-if="$isModerator">
Moderator controls
</div>
<p #else>
Standard view
</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:
<div #if="$a">A</div>
<p>separator</p>
<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:
<ul #if="$hasPermission" #foreach="$items as $item" #if="$item->visible">
<li>{{ $item }}</li>
</ul>
<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:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Forte\Facades\Forte;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Forte::app()
->elementConditionalAttributes()
->elementForeachAttributes()
->elementForelseAttributes()
->allowMixedPhpDirectives()
->hoistDirectiveArguments();
}
}
#Changing the Default Attribute Prefix
The default attribute-based rewriters all use # as a prefix:
<div #if="$total > 0">
...
</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:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Forte\Facades\Forte;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Forte::app()
->elementConditionalAttributes('my-prefix')
->elementForeachAttributes('my-prefix')
->elementForelseAttributes('my-prefix');
}
}
#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:
<?php
use Forte\Enclaves\Enclave;
use Forte\Rewriting\NodePath;
use Forte\Rewriting\Visitor;
class HighlightVisitor extends Visitor
{
public function enter(NodePath $path): void
{
// Visitor logic
}
}
$enclave = new Enclave;
$enclave->use(HighlightVisitor::class);
$enclave->hasRewriters(); // true
$enclave->rewriterCount(); // 1
When registering multiple rewriters at once, pass an array to use or call useMany:
<?php
use Forte\Enclaves\Enclave;
use Forte\Rewriting\NodePath;
use Forte\Rewriting\Visitor;
class VisitorA extends Visitor
{
public function enter(NodePath $path): void {}
}
class VisitorB extends Visitor
{
public function enter(NodePath $path): void {}
}
$enclave = new Enclave;
$enclave->use([VisitorA::class, VisitorB::class]);
$enclave->rewriterCount(); // 2
#AST Rewriters
The apply method registers AstRewriter instances such as the built-in rewrite passes:
<?php
use Forte\Enclaves\Enclave;
use Forte\Facades\Forte;
use Forte\Rewriting\Passes\Elements\AddClass;
use Forte\Rewriting\Passes\Elements\RenameTag;
$enclave = new Enclave;
$enclave->apply(
new AddClass('div', 'mt-4'),
new RenameTag('b', 'strong')
);
$doc = Forte::parse('<div><b>Hello</b></div>');
$result = $enclave->transformDocument($doc);
$result->render(); // '<div class="mt-4"><strong>Hello</strong></div>'
#Callback Transformers
The transform method registers a closure that receives each NodePath during traversal:
<?php
use Forte\Ast\Elements\ElementNode;
use Forte\Enclaves\Enclave;
use Forte\Facades\Forte;
use Forte\Rewriting\NodePath;
$enclave = new Enclave;
$enclave->transform(function (NodePath $path): void {
$node = $path->node();
if ($node instanceof ElementNode && $node->tagNameText() === 'b') {
$path->replaceWith('<strong>'.$node->innerContent().'</strong>');
}
});
$doc = Forte::parse('<b>Hello</b>');
$result = $enclave->transformDocument($doc);
$result->render(); // '<strong>Hello</strong>'
#Rewriter Priority
The use method accepts an optional second argument to control execution order. Higher priority rewriters run first:
<?php
use Forte\Enclaves\Enclave;
use Forte\Rewriting\NodePath;
use Forte\Rewriting\Visitor;
class LowPriorityVisitor extends Visitor
{
public function enter(NodePath $path): void {}
}
class HighPriorityVisitor extends Visitor
{
public function enter(NodePath $path): void {}
}
$enclave = new Enclave;
$enclave->use(LowPriorityVisitor::class, 0);
$enclave->use(HighPriorityVisitor::class, 10);
$ordered = $enclave->getRewritersInPriorityOrder();
$ordered[0]; // "HighPriorityVisitor" (priority 10 runs first)
$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:
<?php
use Forte\Enclaves\Enclave;
use Forte\Rewriting\NodePath;
use Forte\Rewriting\Visitor;
class ManagedVisitor extends Visitor
{
public function enter(NodePath $path): void {}
}
$enclave = new Enclave;
$enclave->hasRewriters(); // false
$enclave->use(ManagedVisitor::class);
$enclave->hasRewriter(ManagedVisitor::class); // true
$enclave->rewriterCount(); // 1
$enclave->removeRewriter(ManagedVisitor::class);
$enclave->hasRewriter(ManagedVisitor::class); // false
$enclave->use(ManagedVisitor::class);
$enclave->clearRewriters();
$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:
<?php
use Forte\Enclaves\Enclave;
$enclave = new Enclave;
$enclave->include('/app/views/**');
$enclave->exclude('/app/views/emails/**');
$enclave->matches('/app/views/home.blade.php'); // true
$enclave->matches('/app/views/emails/welcome.blade.php'); // false
$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:
<?php
use Forte\Enclaves\Enclave;
$enclave = new Enclave;
$enclave->include(['/views/**', '/components/**']);
$enclave->exclude('/views/vendor/**');
$enclave->matches('/views/home.blade.php'); // true
$enclave->matches('/components/alert.blade.php'); // true
$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:
<?php
namespace App\Providers;
use Forte\Facades\Forte;
use Forte\Rewriting\Passes\Elements\AddClass;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Create an enclave for admin views
Forte::create('admin')
->include(resource_path('views/admin/**'))
->apply(new AddClass('table', 'admin-table'));
// The default app enclave for everything else
Forte::app()
->elementConditionalAttributes()
->elementForeachAttributes();
}
}
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:
<?php
use Forte\Enclaves\EnclavesManager;
$manager = app(EnclavesManager::class);
$manager->has('{app}'); // true (always present)
$manager->count(); // 1
$manager->create('admin')
->include('/views/admin/**');
$manager->has('admin'); // true
$manager->count(); // 2
$manager
->get('admin')
->matches('/views/admin/dashboard.blade.php'); // true
The names method returns all registered enclave names, and all returns the full map keyed by name:
<?php
use Forte\Enclaves\EnclavesManager;
$manager = app(EnclavesManager::class);
$manager->create('admin');
$manager->create('emails');
$manager->names(); // ["{app}", "admin", "emails"]
$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:
<?php
namespace App\Providers;
use Forte\Facades\Forte;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Forte::includeVendorPackages('notifications', 'pagination');
}
}
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:
<?php
namespace App\Providers;
use Forte\Facades\Forte;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Forte::includeVendor();
}
}
#Excluding Vendor Packages
If you have included all vendor views but want to exclude specific packages, use excludeVendorPackages:
<?php
namespace App\Providers;
use Forte\Facades\Forte;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Forte::includeVendor();
Forte::excludeVendorPackages('debugbar');
}
}
#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:
<?php
namespace App\Providers;
use Forte\Facades\Forte;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Forte::createForPackage('acme/widgets')
->elementForeachAttributes()
->elementConditionalAttributes();
}
}
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:
<?php
namespace Acme\Widgets;
use Forte\Facades\Forte;
use Illuminate\Support\ServiceProvider;
class WidgetsServiceProvider extends ServiceProvider
{
public function boot(): void
{
Forte::createForPackage('acme/widgets', __DIR__.'/..')
->elementForeachAttributes();
}
}
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