BehavioralPHPverifiedVerified
Visitor Pattern in PHP
Lets you add new operations to an object structure without modifying the objects themselves, by separating the algorithm from the object structure it operates on.
How to Implement the Visitor Pattern in PHP
1Step 1: Define the visitor interface
interface Visitor
{
public function visitCircle(Circle $circle): string;
public function visitRectangle(Rectangle $rectangle): string;
}2Step 2: Define elements that accept visitors
interface Shape
{
public function accept(Visitor $visitor): string;
}
class Circle implements Shape
{
public function __construct(public readonly float $radius) {}
public function accept(Visitor $visitor): string
{
return $visitor->visitCircle($this);
}
}
class Rectangle implements Shape
{
public function __construct(
public readonly float $width,
public readonly float $height,
) {}
public function accept(Visitor $visitor): string
{
return $visitor->visitRectangle($this);
}
}3Step 3: Implement concrete visitors
class AreaCalculator implements Visitor
{
public function visitCircle(Circle $circle): string
{
$area = M_PI * $circle->radius ** 2;
return "Circle area: {$area}";
}
public function visitRectangle(Rectangle $rectangle): string
{
$area = $rectangle->width * $rectangle->height;
return "Rectangle area: {$area}";
}
}
class SvgExporter implements Visitor
{
public function visitCircle(Circle $circle): string
{
return "<circle r=\"{$circle->radius}\"/>";
}
public function visitRectangle(Rectangle $rectangle): string
{
return "<rect width=\"{$rectangle->width}\" height=\"{$rectangle->height}\"/>";
}
}
// Usage
$shapes = [new Circle(5), new Rectangle(4, 6)];
$calc = new AreaCalculator();
foreach ($shapes as $shape) {
echo $shape->accept($calc) . "\n";
}<?php
declare(strict_types=1);
// [step] Define a type-safe AST visitor for expression evaluation
interface ExpressionVisitorInterface
{
public function visitNumber(NumberExpression $expr): mixed;
public function visitBinary(BinaryExpression $expr): mixed;
public function visitUnary(UnaryExpression $expr): mixed;
public function visitVariable(VariableExpression $expr): mixed;
}
interface ExpressionInterface
{
public function accept(ExpressionVisitorInterface $visitor): mixed;
}
// [step] Define expression nodes (elements)
final readonly class NumberExpression implements ExpressionInterface
{
public function __construct(public float $value) {}
public function accept(ExpressionVisitorInterface $visitor): mixed
{
return $visitor->visitNumber($this);
}
}
enum BinaryOperator: string
{
case Add = '+';
case Subtract = '-';
case Multiply = '*';
case Divide = '/';
}
final readonly class BinaryExpression implements ExpressionInterface
{
public function __construct(
public ExpressionInterface $left,
public BinaryOperator $operator,
public ExpressionInterface $right,
) {}
public function accept(ExpressionVisitorInterface $visitor): mixed
{
return $visitor->visitBinary($this);
}
}
enum UnaryOperator: string
{
case Negate = '-';
}
final readonly class UnaryExpression implements ExpressionInterface
{
public function __construct(
public UnaryOperator $operator,
public ExpressionInterface $operand,
) {}
public function accept(ExpressionVisitorInterface $visitor): mixed
{
return $visitor->visitUnary($this);
}
}
final readonly class VariableExpression implements ExpressionInterface
{
public function __construct(public string $name) {}
public function accept(ExpressionVisitorInterface $visitor): mixed
{
return $visitor->visitVariable($this);
}
}
// [step] Implement an evaluator visitor
final class EvaluatorVisitor implements ExpressionVisitorInterface
{
/** @param array<string, float> $variables */
public function __construct(
private readonly array $variables = [],
) {}
public function visitNumber(NumberExpression $expr): float
{
return $expr->value;
}
public function visitBinary(BinaryExpression $expr): float
{
$left = (float) $expr->left->accept($this);
$right = (float) $expr->right->accept($this);
return match ($expr->operator) {
BinaryOperator::Add => $left + $right,
BinaryOperator::Subtract => $left - $right,
BinaryOperator::Multiply => $left * $right,
BinaryOperator::Divide => $right !== 0.0
? $left / $right
: throw new \DivisionByZeroError('Division by zero'),
};
}
public function visitUnary(UnaryExpression $expr): float
{
$operand = (float) $expr->operand->accept($this);
return match ($expr->operator) {
UnaryOperator::Negate => -$operand,
};
}
public function visitVariable(VariableExpression $expr): float
{
return $this->variables[$expr->name]
?? throw new \RuntimeException("Undefined variable: {$expr->name}");
}
}
// [step] Implement a printer visitor
final class PrinterVisitor implements ExpressionVisitorInterface
{
public function visitNumber(NumberExpression $expr): string
{
return (string) $expr->value;
}
public function visitBinary(BinaryExpression $expr): string
{
$left = (string) $expr->left->accept($this);
$right = (string) $expr->right->accept($this);
return "({$left} {$expr->operator->value} {$right})";
}
public function visitUnary(UnaryExpression $expr): string
{
$operand = (string) $expr->operand->accept($this);
return "({$expr->operator->value}{$operand})";
}
public function visitVariable(VariableExpression $expr): string
{
return $expr->name;
}
}Visitor Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Visitor Pattern in the Real World
“Think of a tax auditor visiting different types of businesses—a restaurant, a law firm, a retail shop. The auditor (visitor) knows exactly what to examine at each type of business and applies the appropriate inspection procedure. The businesses (elements) simply let the auditor in; they don't change their own operations to accommodate the audit.”