Understanding Visitors

The Visitor Pattern is a common design pattern that allows you to add new operations to objects without changing their structure. In PHPRegex, visitors process the AST to perform analyses and transformations.

Simple explanation

Imagine you have an AST (tree structure) and you want to do different things with it:

  • Explain the pattern in plain English
  • Validate the pattern for errors
  • Highlight the pattern with colors
  • Analyze for potential ReDoS risk

Instead of putting all this logic in the AST nodes themselves, we use visitors that “walk” the tree and perform specific tasks.

How visitors work

Basic Visitor Structure

use PHPRegex\Parser\AbstractTraversingVisitor;
use PHPRegex\Parser\Node;

class MyCustomVisitor extends AbstractTraversingVisitor
{
    public function visitLiteral(Node\LiteralNode $node)
    {
        echo "Found literal: " . $node->value . "\n";

        return parent::visitLiteral($node); // Continue traversal
    }

    public function visitQuantifier(Node\QuantifierNode $node)
    {
        echo "Found quantifier: " . $node->quantifier . "\n";

        return parent::visitQuantifier($node); // Continue traversal
    }
}

Every node you do not override has its children visited, so this visitor sees the whole tree. The parent::visit*() call is what keeps the descent going below a node you override - forget it and nothing below that node is reached.

Using a Visitor

use PHPRegex\Toolkit\Regex;

$regex = Regex::create();
$ast = $regex->parse('/hello\d+/');

$visitor = new MyCustomVisitor();
$ast->accept($visitor); // Start the traversal

Running the two blocks above prints:

Found literal: h
Found literal: e
Found literal: l
Found literal: l
Found literal: o
Found quantifier: +

One line per node - remember the parser emits one LiteralNode per character.

Built-in visitors

PHPRegex includes several useful visitors:

1. PatternPrinter

use PHPRegex\Parser\Printer\PatternPrinter;

$compiler = new PatternPrinter();
$pattern = $ast->accept($compiler); // Regenerate the pattern: '/hello\d+/'

2. TextExplainer

use PHPRegex\Explain\TextExplainer;

$explainer = new TextExplainer();
$explanation = $ast->accept($explainer); // Get plain English explanation

3. Highlighting Visitors

use PHPRegex\Explain\Highlighter\ConsoleHighlighter;
use PHPRegex\Explain\Highlighter\HtmlHighlighter;

$consoleHighlighter = new ConsoleHighlighter();
$htmlHighlighter = new HtmlHighlighter();

$consoleOutput = $ast->accept($consoleHighlighter);
$htmlOutput = $ast->accept($htmlHighlighter);

The full catalogue - validation, linting, rewriting, metrics, generation, rendering - lives in the Visitors Reference.

Creating custom visitors

Step 1: Extend a base class

PHPRegex ships two bases: AbstractTraversingVisitor, which walks the whole tree, and AbstractNodeVisitor, where each method returns the node’s value and you choose which children to visit. The Visitors Reference tabulates which one fits which goal; a collector that must see every node of its kind takes the traversing base:

use PHPRegex\Parser\AbstractTraversingVisitor;
use PHPRegex\Parser\Node;

class QuantifierCounter extends AbstractTraversingVisitor
{
    private int $count = 0;

    public function getCount(): int
    {
        return $this->count;
    }

    public function visitQuantifier(Node\QuantifierNode $node)
    {
        $this->count++;

        return parent::visitQuantifier($node); // Continue traversal
    }
}

Step 2: Use Your Visitor

use PHPRegex\Toolkit\Regex;

$regex = Regex::create();
$ast = $regex->parse('/a+b*c?/');

$counter = new QuantifierCounter();
$ast->accept($counter);

echo "Quantifiers found: " . $counter->getCount(); // "3"

Real-world examples

Example 1: Pattern complexity analyzer

use PHPRegex\Parser\AbstractTraversingVisitor;
use PHPRegex\Parser\Node;
use PHPRegex\Toolkit\Regex;

class ComplexityAnalyzer extends AbstractTraversingVisitor
{
    private int $complexityScore = 0;

    public function visitQuantifier(Node\QuantifierNode $node)
    {
        $this->complexityScore += 2; // Each quantifier adds complexity

        return parent::visitQuantifier($node);
    }

    public function visitGroup(Node\GroupNode $node)
    {
        $this->complexityScore += 3; // Groups are more complex

        return parent::visitGroup($node);
    }

    public function getComplexityScore(): int
    {
        return $this->complexityScore;
    }
}

$ast = Regex::create()->parse('/(?:(a+)|b){2,4}/');
$analyzer = new ComplexityAnalyzer();
$ast->accept($analyzer);

echo $analyzer->getComplexityScore(); // 10: two groups (3 + 3), two quantifiers ({2,4} and +, 2 + 2)

Example 2: Named group collector

use PHPRegex\Parser\AbstractTraversingVisitor;
use PHPRegex\Parser\Node;
use PHPRegex\Toolkit\Regex;

class GroupNameCollector extends AbstractTraversingVisitor
{
    private array $names = [];

    public function visitGroup(Node\GroupNode $node)
    {
        if ($node->name !== null) {
            $this->names[] = $node->name;
        }

        return parent::visitGroup($node); // Reach groups nested inside this one
    }

    public function getNames(): array
    {
        return $this->names;
    }
}

$ast = Regex::create()->parse('/(?<year>\d{4})-(?<month>\d{2})/');
$collector = new GroupNameCollector();
$ast->accept($collector);

print_r($collector->getNames());

The output lists both names, in pattern order:

Array
(
    [0] => year
    [1] => month
)

On /(?<date>(?<year>\d{4}))-(?<month>\d{2})/, the same collector returns date, year, month: the parent::visitGroup($node) call descends into nested groups instead of stopping at the outermost one.

Edit on GitHub