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:
1<?php
2
3use Forte\Ast\Elements\ElementNode;
4use Forte\Rewriting\NodePath;
5use Forte\Rewriting\Passes\Elements\ElementPass;
6
7abstract readonly class ElementPass implements AstRewriter
8{
9 public function __construct(protected string $pattern) {}
10
11 abstract protected function applyToElement(NodePath $path, ElementNode $element): void;
12}
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:
1<?php
2
3use Forte\Ast\Elements\ElementNode;
4use Forte\Facades\Forte;
5use Forte\Rewriting\NodePath;
6use Forte\Rewriting\Passes\Elements\ElementPass;
7
8readonly class AddLazyLoading extends ElementPass
9{
10 protected function applyToElement(NodePath $path, ElementNode $element): void
11 {
12 if (! $element->getAttribute('loading')) {
13 $path->setAttribute('loading', 'lazy');
14 }
15 }
16}
17
18$doc = Forte::parse('<img src="hero.jpg"><img src="thumb.jpg" loading="eager">');
19$result = $doc->apply(new AddLazyLoading('img'));
20
21// '<img src="hero.jpg" loading="lazy"><img src="thumb.jpg" loading="eager">'
22$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:
1<?php
2
3use Forte\Ast\Document\Document;
4
5interface AstRewriter
6{
7 public function rewrite(Document $doc): Document;
8}
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:
1<?php
2
3use Forte\Ast\Document\Document;
4use Forte\Facades\Forte;
5use Forte\Rewriting\AstRewriter;
6use Forte\Rewriting\Builders\Builder;
7use Forte\Rewriting\CallbackVisitor;
8use Forte\Rewriting\NodePath;
9use Forte\Rewriting\Rewriter;
10
11readonly class RemoveDebugDirectives implements AstRewriter
12{
13 public function __construct(private string $pattern = 'dd') {}
14
15 public function rewrite(Document $doc): Document
16 {
17 return (new Rewriter)
18 ->addVisitor(new CallbackVisitor(
19 enter: function (NodePath $path): void {
20 if ($directive = $path->asDirective()) {
21 if ($directive->is($this->pattern)) {
22 $path->replaceWith(Builder::comment('debug removed'));
23 }
24
25 return;
26 }
27
28 if ($block = $path->asDirectiveBlock()) {
29 if ($block->is($this->pattern)) {
30 $path->replaceWith(Builder::comment('debug removed'));
31 }
32 }
33 }
34 ))
35 ->rewrite($doc);
36 }
37}
38
39$doc = Forte::parse('<div>@dd($user)</div>');
40$result = $doc->apply(new RemoveDebugDirectives);
41
42// '<div><!-- debug removed --></div>'
43$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:
1<?php
2
3use Forte\Ast\Elements\ElementNode;
4use Forte\Rewriting\NodePath;
5use Forte\Rewriting\Passes\Elements\ElementPass;
6
7readonly class AddDataAttribute extends ElementPass
8{
9 public function __construct(
10 string $pattern,
11 private string $attributeName,
12 private string $attributeValue,
13 ) {
14 parent::__construct($pattern);
15 }
16
17 protected function applyToElement(NodePath $path, ElementNode $element): void
18 {
19 $path->setAttribute($this->attributeName, $this->attributeValue);
20 }
21}
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:
1<?php
2
3use Forte\Facades\Forte;
4use Forte\Rewriting\Passes\Elements\AddClass;
5use Forte\Rewriting\Passes\Elements\SetAttribute;
6use Forte\Rewriting\RewritePipeline;
7
8$pipeline = new RewritePipeline(
9 new AddLazyLoading('img'),
10 new AddClass('img', 'responsive'),
11 new SetAttribute('img', 'decoding', 'async'),
12);
13
14$doc = Forte::parse('<img src="photo.jpg">');
15$result = $pipeline->rewrite($doc);
16
17// '<img src="photo.jpg" loading="lazy" class="responsive" decoding="async">'
18$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