Rewriting
Custom Rewrite Passes
Forte's built-in rewrite passes cover common transformations, but you can create your own for project-specific or domain-specific needs. Custom passes are reusable, composable, and integrate with RewritePipeline and Document::apply() just like the built-in ones.
#Extending ElementPass
The simplest way to create a custom pass is to extend ElementPass. The base class handles tag matching and visitor setup. You provide a tag name pattern in the constructor and implement the applyToElement method:
<?php
use Forte\Ast\Elements\ElementNode;
use Forte\Rewriting\NodePath;
use Forte\Rewriting\Passes\Elements\ElementPass;
abstract readonly class ElementPass implements AstRewriter
{
public function __construct(protected string $pattern) {}
abstract protected function applyToElement(NodePath $path, ElementNode $element): void;
}
The pattern supports * wildcards via Str::is(), so img matches only <img>, h* matches <h1> through <h6>, and * matches every element.
Here is a custom pass that adds loading="lazy" to images that do not already have a loading attribute:
<?php
use Forte\Ast\Elements\ElementNode;
use Forte\Facades\Forte;
use Forte\Rewriting\NodePath;
use Forte\Rewriting\Passes\Elements\ElementPass;
readonly class AddLazyLoading extends ElementPass
{
protected function applyToElement(NodePath $path, ElementNode $element): void
{
if (! $element->getAttribute('loading')) {
$path->setAttribute('loading', 'lazy');
}
}
}
$doc = Forte::parse('<img src="hero.jpg"><img src="thumb.jpg" loading="eager">');
$result = $doc->apply(new AddLazyLoading('img'));
// '<img src="hero.jpg" loading="lazy"><img src="thumb.jpg" loading="eager">'
$result->render();
The pass only modifies images that lack the attribute, leaving loading="eager" untouched.
#Implementing AstRewriter
For passes that target directives or need to handle multiple node types, implement the AstRewriter interface directly. It has a single method:
<?php
use Forte\Ast\Document\Document;
interface AstRewriter
{
public function rewrite(Document $doc): Document;
}
Inside rewrite, create a Rewriter with a CallbackVisitor that filters and transforms nodes. This is the same pattern Forte's built-in directive passes use internally.
The following pass removes @dd directives and replaces them with an HTML comment. It handles both standalone @dd($var) and block @dd...@enddd forms:
<?php
use Forte\Ast\Document\Document;
use Forte\Facades\Forte;
use Forte\Rewriting\AstRewriter;
use Forte\Rewriting\Builders\Builder;
use Forte\Rewriting\CallbackVisitor;
use Forte\Rewriting\NodePath;
use Forte\Rewriting\Rewriter;
readonly class RemoveDebugDirectives implements AstRewriter
{
public function __construct(private string $pattern = 'dd') {}
public function rewrite(Document $doc): Document
{
return (new Rewriter)
->addVisitor(new CallbackVisitor(
enter: function (NodePath $path): void {
if ($directive = $path->asDirective()) {
if ($directive->is($this->pattern)) {
$path->replaceWith(Builder::comment('debug removed'));
}
return;
}
if ($block = $path->asDirectiveBlock()) {
if ($block->is($this->pattern)) {
$path->replaceWith(Builder::comment('debug removed'));
}
}
}
))
->rewrite($doc);
}
}
$doc = Forte::parse('<div>@dd($user)</div>');
$result = $doc->apply(new RemoveDebugDirectives);
// '<div><!-- debug removed --></div>'
$result->render();
The key pattern is to use $path->asDirective() and $path->asDirectiveBlock() to safely narrow the node type before checking with is(). This covers both standalone and block directive forms.
#Constructor Patterns
Custom passes should use readonly class with promoted constructor properties. This keeps the pass immutable and its configuration explicit:
<?php
use Forte\Ast\Elements\ElementNode;
use Forte\Rewriting\NodePath;
use Forte\Rewriting\Passes\Elements\ElementPass;
readonly class AddDataAttribute extends ElementPass
{
public function __construct(
string $pattern,
private string $attributeName,
private string $attributeValue,
) {
parent::__construct($pattern);
}
protected function applyToElement(NodePath $path, ElementNode $element): void
{
$path->setAttribute($this->attributeName, $this->attributeValue);
}
}
When extending ElementPass, pass the pattern to the parent constructor and keep additional parameters as private properties. When implementing AstRewriter directly, all parameters are your own.
#Composing Custom Passes
Custom passes work identically to built-in passes in pipelines and apply calls. You can mix them freely:
<?php
use Forte\Facades\Forte;
use Forte\Rewriting\Passes\Elements\AddClass;
use Forte\Rewriting\Passes\Elements\SetAttribute;
use Forte\Rewriting\RewritePipeline;
$pipeline = new RewritePipeline(
new AddLazyLoading('img'),
new AddClass('img', 'responsive'),
new SetAttribute('img', 'decoding', 'async'),
);
$doc = Forte::parse('<img src="photo.jpg">');
$result = $pipeline->rewrite($doc);
// '<img src="photo.jpg" loading="lazy" class="responsive" decoding="async">'
$result->render();
Since RewritePipeline itself implements AstRewriter, you can nest pipelines inside other pipelines or pass them anywhere a single rewriter is accepted.
#See also
- Rewrite Passes: Built-in passes for common transformations
- Rewriters: The visitor pattern and NodePath API
- Node Builders: Factory methods for creating synthetic nodes
- Enclaves: Apply rewriters to isolated compilation environments