Visitor — operations on object structures without modifying them
Concept
Visitor pattern lets you add new operations to an object structure without modifying the objects. You define a visitor class with a method for each type of element in the structure. Elements "accept" visitors and let them operate on them.
The double dispatch problem: PHP doesn't have built-in method overloading based on parameter types. $visitor->visit($element) — what visit() does depends on $element's type. You'd need instanceof checks. Visitor solves this with double dispatch: $element->accept($visitor) calls $visitor->visitCircle($this) inside Circle, and $visitor->visitSquare($this) inside Square.
Structure:
- Visitor Interface: One
visit*()method per concrete element type. - Concrete Visitor: Implements all
visit*()methods. Encapsulates an operation. - Element Interface:
accept(Visitor $visitor). - Concrete Elements: Call the appropriate
visit*($this)on the visitor.
Benefits:
- Add new operations (visitors) without modifying elements.
- Gather related operations in one place (visitor class) rather than scattered across element classes.
- Accumulate state across elements (e.g., total price across all items).
Drawbacks:
- Adding a new element type requires updating ALL visitor classes.
- Elements must expose their implementation for the visitor to do its job — can break encapsulation.
When to use: Object structure rarely changes (fixed element types), but you frequently add new operations (export formats, validations, reports).
Code Example
<?php
// Visitor Interface
interface DocumentVisitor
{
public function visitHeading(Heading $heading): void;
public function visitParagraph(Paragraph $paragraph): void;
public function visitImage(Image $image): void;
public function visitCodeBlock(CodeBlock $code): void;
}
// Element Interface
interface DocumentElement
{
public function accept(DocumentVisitor $visitor): void;
}
// Concrete Elements
class Heading implements DocumentElement
{
public function __construct(public readonly string $text, public readonly int $level = 1) {}
public function accept(DocumentVisitor $visitor): void { $visitor->visitHeading($this); }
}
class Paragraph implements DocumentElement
{
public function __construct(public readonly string $text) {}
public function accept(DocumentVisitor $visitor): void { $visitor->visitParagraph($this); }
}
class Image implements DocumentElement
{
public function __construct(public readonly string $src, public readonly string $alt) {}
public function accept(DocumentVisitor $visitor): void { $visitor->visitImage($this); }
}
class CodeBlock implements DocumentElement
{
public function __construct(public readonly string $code, public readonly string $language = 'php') {}
public function accept(DocumentVisitor $visitor): void { $visitor->visitCodeBlock($this); }
}
// Concrete Visitors — separate operations for same element structure
class HtmlExporter implements DocumentVisitor
{
private string $output = '';
public function visitHeading(Heading $heading): void
{
$this->output .= "<h{$heading->level}>{$heading->text}</h{$heading->level}>\n";
}
public function visitParagraph(Paragraph $paragraph): void
{
$this->output .= "<p>{$paragraph->text}</p>\n";
}
public function visitImage(Image $image): void
{
$this->output .= "<img src='{$image->src}' alt='{$image->alt}'>\n";
}
public function visitCodeBlock(CodeBlock $code): void
{
$this->output .= "<pre><code class='language-{$code->language}'>{$code->code}</code></pre>\n";
}
public function getOutput(): string { return $this->output; }
}
class WordCountVisitor implements DocumentVisitor
{
private int $count = 0;
public function visitHeading(Heading $heading): void { $this->count += str_word_count($heading->text); }
public function visitParagraph(Paragraph $para): void { $this->count += str_word_count($para->text); }
public function visitImage(Image $image): void { /* images have no words */ }
public function visitCodeBlock(CodeBlock $code): void { /* code not counted */ }
public function getCount(): int { return $this->count; }
}
// Object structure
$document = [
new Heading('PHP Design Patterns', 1),
new Paragraph('Design patterns are reusable solutions to common programming problems.'),
new Image('/images/patterns.png', 'Design Patterns Overview'),
new CodeBlock("class Visitor { ... }", 'php'),
];
// Apply different visitors to the same structure
$html = new HtmlExporter();
foreach ($document as $element) { $element->accept($html); }
echo $html->getOutput();
$wordCount = new WordCountVisitor();
foreach ($document as $element) { $element->accept($wordCount); }
echo "Word count: " . $wordCount->getCount();