Laravel 13 访问者模式深度解析
访问者模式是一种行为型设计模式,它将算法与对象结构分离,使得可以在不修改对象结构的情况下定义新的操作。本文将深入探讨 Laravel 13 中访问者模式的高级用法。
访问者模式基础
什么是访问者模式
访问者模式允许在不修改对象结构的情况下,定义作用于这些对象的新操作。
1 2 3 4 5 6 7 8
| <?php
namespace App\Contracts;
interface VisitorInterface { public function visit(ElementInterface $element): mixed; }
|
1 2 3 4 5 6 7 8
| <?php
namespace App\Contracts;
interface ElementInterface { public function accept(VisitorInterface $visitor): mixed; }
|
文档元素访问者
文档元素接口
1 2 3 4 5 6 7 8 9 10
| <?php
namespace App\Contracts\Document;
interface DocumentElementInterface { public function accept(DocumentVisitorInterface $visitor): mixed;
public function getContent(): string; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php
namespace App\Contracts\Document;
interface DocumentVisitorInterface { public function visitParagraph(ParagraphElement $paragraph): mixed;
public function visitHeading(HeadingElement $heading): mixed;
public function visitImage(ImageElement $image): mixed;
public function visitTable(TableElement $table): mixed;
public function visitCodeBlock(CodeBlockElement $codeBlock): mixed; }
|
具体元素实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <?php
namespace App\Elements\Document;
use App\Contracts\Document\DocumentElementInterface; use App\Contracts\Document\DocumentVisitorInterface;
class ParagraphElement implements DocumentElementInterface { protected string $content;
public function __construct(string $content) { $this->content = $content; }
public function accept(DocumentVisitorInterface $visitor): mixed { return $visitor->visitParagraph($this); }
public function getContent(): string { return $this->content; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| <?php
namespace App\Elements\Document;
use App\Contracts\Document\DocumentElementInterface; use App\Contracts\Document\DocumentVisitorInterface;
class HeadingElement implements DocumentElementInterface { protected string $content; protected int $level;
public function __construct(string $content, int $level = 1) { $this->content = $content; $this->level = $level; }
public function accept(DocumentVisitorInterface $visitor): mixed { return $visitor->visitHeading($this); }
public function getContent(): string { return $this->content; }
public function getLevel(): int { return $this->level; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| <?php
namespace App\Elements\Document;
use App\Contracts\Document\DocumentElementInterface; use App\Contracts\Document\DocumentVisitorInterface;
class ImageElement implements DocumentElementInterface { protected string $src; protected string $alt; protected int $width; protected int $height;
public function __construct(string $src, string $alt = '', int $width = 0, int $height = 0) { $this->src = $src; $this->alt = $alt; $this->width = $width; $this->height = $height; }
public function accept(DocumentVisitorInterface $visitor): mixed { return $visitor->visitImage($this); }
public function getContent(): string { return $this->alt; }
public function getSrc(): string { return $this->src; }
public function getAlt(): string { return $this->alt; }
public function getWidth(): int { return $this->width; }
public function getHeight(): int { return $this->height; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <?php
namespace App\Elements\Document;
use App\Contracts\Document\DocumentElementInterface; use App\Contracts\Document\DocumentVisitorInterface;
class TableElement implements DocumentElementInterface { protected array $headers; protected array $rows;
public function __construct(array $headers, array $rows) { $this->headers = $headers; $this->rows = $rows; }
public function accept(DocumentVisitorInterface $visitor): mixed { return $visitor->visitTable($this); }
public function getContent(): string { return ''; }
public function getHeaders(): array { return $this->headers; }
public function getRows(): array { return $this->rows; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <?php
namespace App\Elements\Document;
use App\Contracts\Document\DocumentElementInterface; use App\Contracts\Document\DocumentVisitorInterface;
class CodeBlockElement implements DocumentElementInterface { protected string $code; protected string $language;
public function __construct(string $code, string $language = 'php') { $this->code = $code; $this->language = $language; }
public function accept(DocumentVisitorInterface $visitor): mixed { return $visitor->visitCodeBlock($this); }
public function getContent(): string { return $this->code; }
public function getCode(): string { return $this->code; }
public function getLanguage(): string { return $this->language; } }
|
HTML 渲染访问者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| <?php
namespace App\Visitors\Document;
use App\Contracts\Document\DocumentVisitorInterface; use App\Elements\Document\{ParagraphElement, HeadingElement, ImageElement, TableElement, CodeBlockElement};
class HtmlRenderVisitor implements DocumentVisitorInterface { public function visitParagraph(ParagraphElement $paragraph): string { return "<p>{$paragraph->getContent()}</p>"; }
public function visitHeading(HeadingElement $heading): string { $level = $heading->getLevel(); $content = htmlspecialchars($heading->getContent()); return "<h{$level}>{$content}</h{$level}>"; }
public function visitImage(ImageElement $image): string { $src = htmlspecialchars($image->getSrc()); $alt = htmlspecialchars($image->getAlt()); $width = $image->getWidth(); $height = $image->getHeight();
$attrs = "src=\"{$src}\" alt=\"{$alt}\"";
if ($width) { $attrs .= " width=\"{$width}\""; }
if ($height) { $attrs .= " height=\"{$height}\""; }
return "<img {$attrs}>"; }
public function visitTable(TableElement $table): string { $html = '<table>';
$html .= '<thead><tr>'; foreach ($table->getHeaders() as $header) { $html .= '<th>' . htmlspecialchars($header) . '</th>'; } $html .= '</tr></thead>';
$html .= '<tbody>'; foreach ($table->getRows() as $row) { $html .= '<tr>'; foreach ($row as $cell) { $html .= '<td>' . htmlspecialchars($cell) . '</td>'; } $html .= '</tr>'; } $html .= '</tbody></table>';
return $html; }
public function visitCodeBlock(CodeBlockElement $codeBlock): string { $code = htmlspecialchars($codeBlock->getCode()); $language = htmlspecialchars($codeBlock->getLanguage());
return "<pre><code class=\"language-{$language}\">{$code}</code></pre>"; } }
|
Markdown 渲染访问者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| <?php
namespace App\Visitors\Document;
use App\Contracts\Document\DocumentVisitorInterface; use App\Elements\Document\{ParagraphElement, HeadingElement, ImageElement, TableElement, CodeBlockElement};
class MarkdownRenderVisitor implements DocumentVisitorInterface { public function visitParagraph(ParagraphElement $paragraph): string { return $paragraph->getContent() . "\n\n"; }
public function visitHeading(HeadingElement $heading): string { $prefix = str_repeat('#', $heading->getLevel()); return "{$prefix} {$heading->getContent()}\n\n"; }
public function visitImage(ImageElement $image): string { return "})\n\n"; }
public function visitTable(TableElement $table): string { $headers = $table->getHeaders(); $rows = $table->getRows();
$md = '| ' . implode(' | ', $headers) . ' |' . "\n"; $md .= '| ' . implode(' | ', array_fill(0, count($headers), '---')) . ' |' . "\n";
foreach ($rows as $row) { $md .= '| ' . implode(' | ', $row) . ' |' . "\n"; }
return $md . "\n"; }
public function visitCodeBlock(CodeBlockElement $codeBlock): string { return "```{$codeBlock->getLanguage()}\n{$codeBlock->getCode()}\n```\n\n"; } }
|
统计访问者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| <?php
namespace App\Visitors\Document;
use App\Contracts\Document\DocumentVisitorInterface; use App\Elements\Document\{ParagraphElement, HeadingElement, ImageElement, TableElement, CodeBlockElement};
class StatisticsVisitor implements DocumentVisitorInterface { protected array $stats = [ 'paragraphs' => 0, 'headings' => 0, 'images' => 0, 'tables' => 0, 'code_blocks' => 0, 'total_words' => 0, 'total_characters' => 0, ];
public function visitParagraph(ParagraphElement $paragraph): array { $content = $paragraph->getContent(); $this->stats['paragraphs']++; $this->stats['total_words'] += str_word_count($content); $this->stats['total_characters'] += strlen($content);
return $this->stats; }
public function visitHeading(HeadingElement $heading): array { $content = $heading->getContent(); $this->stats['headings']++; $this->stats['total_words'] += str_word_count($content); $this->stats['total_characters'] += strlen($content);
return $this->stats; }
public function visitImage(ImageElement $image): array { $this->stats['images']++;
return $this->stats; }
public function visitTable(TableElement $table): array { $this->stats['tables']++;
foreach ($table->getRows() as $row) { foreach ($row as $cell) { $this->stats['total_words'] += str_word_count($cell); $this->stats['total_characters'] += strlen($cell); } }
return $this->stats; }
public function visitCodeBlock(CodeBlockElement $codeBlock): array { $this->stats['code_blocks']++; $this->stats['total_characters'] += strlen($codeBlock->getCode());
return $this->stats; }
public function getStats(): array { return $this->stats; } }
|
产品访问者
产品元素接口
1 2 3 4 5 6 7 8 9 10 11 12
| <?php
namespace App\Contracts\Product;
interface ProductElementInterface { public function accept(ProductVisitorInterface $visitor): mixed;
public function getName(): string;
public function getPrice(): float; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php
namespace App\Contracts\Product;
interface ProductVisitorInterface { public function visitSimpleProduct(SimpleProduct $product): mixed;
public function visitBundleProduct(BundleProduct $product): mixed;
public function visitConfigurableProduct(ConfigurableProduct $product): mixed;
public function visitDigitalProduct(DigitalProduct $product): mixed; }
|
具体产品元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| <?php
namespace App\Elements\Product;
use App\Contracts\Product\ProductElementInterface; use App\Contracts\Product\ProductVisitorInterface;
class SimpleProduct implements ProductElementInterface { protected string $name; protected float $price; protected int $stock;
public function __construct(string $name, float $price, int $stock) { $this->name = $name; $this->price = $price; $this->stock = $stock; }
public function accept(ProductVisitorInterface $visitor): mixed { return $visitor->visitSimpleProduct($this); }
public function getName(): string { return $this->name; }
public function getPrice(): float { return $this->price; }
public function getStock(): int { return $this->stock; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| <?php
namespace App\Elements\Product;
use App\Contracts\Product\ProductElementInterface; use App\Contracts\Product\ProductVisitorInterface; use Illuminate\Support\Collection;
class BundleProduct implements ProductElementInterface { protected string $name; protected Collection $items; protected float $discount;
public function __construct(string $name, array $items, float $discount = 0) { $this->name = $name; $this->items = collect($items); $this->discount = $discount; }
public function accept(ProductVisitorInterface $visitor): mixed { return $visitor->visitBundleProduct($this); }
public function getName(): string { return $this->name; }
public function getPrice(): float { $total = $this->items->sum(fn($item) => $item->getPrice()); return $total * (1 - $this->discount); }
public function getItems(): Collection { return $this->items; }
public function getDiscount(): float { return $this->discount; } }
|
价格计算访问者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| <?php
namespace App\Visitors\Product;
use App\Contracts\Product\ProductVisitorInterface; use App\Elements\Product\{SimpleProduct, BundleProduct, ConfigurableProduct, DigitalProduct};
class PriceCalculationVisitor implements ProductVisitorInterface { protected float $taxRate; protected array $prices = [];
public function __construct(float $taxRate = 0.1) { $this->taxRate = $taxRate; }
public function visitSimpleProduct(SimpleProduct $product): array { $basePrice = $product->getPrice(); $tax = $basePrice * $this->taxRate;
return $this->prices[$product->getName()] = [ 'base_price' => $basePrice, 'tax' => $tax, 'total' => $basePrice + $tax, ]; }
public function visitBundleProduct(BundleProduct $product): array { $basePrice = $product->getPrice(); $tax = $basePrice * $this->taxRate; $savings = $product->getDiscount() > 0 ? $product->getItems()->sum(fn($item) => $item->getPrice()) - $basePrice : 0;
return $this->prices[$product->getName()] = [ 'base_price' => $basePrice, 'tax' => $tax, 'total' => $basePrice + $tax, 'savings' => $savings, ]; }
public function visitConfigurableProduct(ConfigurableProduct $product): array { $basePrice = $product->getPrice(); $tax = $basePrice * $this->taxRate;
return $this->prices[$product->getName()] = [ 'base_price' => $basePrice, 'tax' => $tax, 'total' => $basePrice + $tax, ]; }
public function visitDigitalProduct(DigitalProduct $product): array { $basePrice = $product->getPrice(); $tax = $basePrice * $this->taxRate;
return $this->prices[$product->getName()] = [ 'base_price' => $basePrice, 'tax' => $tax, 'total' => $basePrice + $tax, ]; }
public function getPrices(): array { return $this->prices; } }
|
AST 访问者
AST 节点接口
1 2 3 4 5 6 7 8
| <?php
namespace App\Contracts\AST;
interface AstNodeInterface { public function accept(AstVisitorInterface $visitor): mixed; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php
namespace App\Contracts\AST;
interface AstVisitorInterface { public function visitNumberNode(NumberNode $node): mixed;
public function visitStringNode(StringNode $node): mixed;
public function visitBinaryOpNode(BinaryOpNode $node): mixed;
public function visitFunctionCallNode(FunctionCallNode $node): mixed;
public function visitVariableNode(VariableNode $node): mixed; }
|
表达式求值访问者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| <?php
namespace App\Visitors\AST;
use App\Contracts\AST\AstVisitorInterface; use App\Nodes\AST\{NumberNode, StringNode, BinaryOpNode, FunctionCallNode, VariableNode};
class EvaluatorVisitor implements AstVisitorInterface { protected array $variables = []; protected array $functions = [];
public function __construct(array $variables = [], array $functions = []) { $this->variables = $variables; $this->functions = $functions; }
public function visitNumberNode(NumberNode $node): float { return $node->getValue(); }
public function visitStringNode(StringNode $node): string { return $node->getValue(); }
public function visitBinaryOpNode(BinaryOpNode $node): mixed { $left = $node->getLeft()->accept($this); $right = $node->getRight()->accept($this);
return match ($node->getOperator()) { '+' => $left + $right, '-' => $left - $right, '*' => $left * $right, '/' => $left / $right, '==' => $left == $right, '!=' => $left != $right, '<' => $left < $right, '>' => $left > $right, default => throw new \RuntimeException("Unknown operator: {$node->getOperator()}"), }; }
public function visitFunctionCallNode(FunctionCallNode $node): mixed { $name = $node->getName(); $args = array_map(fn($arg) => $arg->accept($this), $node->getArguments());
if (isset($this->functions[$name])) { return ($this->functions[$name])(...$args); }
throw new \RuntimeException("Unknown function: {$name}"); }
public function visitVariableNode(VariableNode $node): mixed { $name = $node->getName();
if (array_key_exists($name, $this->variables)) { return $this->variables[$name]; }
throw new \RuntimeException("Undefined variable: {$name}"); } }
|
测试访问者模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| <?php
namespace Tests\Unit\Visitors;
use Tests\TestCase; use App\Elements\Document\{ParagraphElement, HeadingElement, ImageElement}; use App\Visitors\Document\{HtmlRenderVisitor, MarkdownRenderVisitor, StatisticsVisitor};
class VisitorTest extends TestCase { public function test_html_render_visitor(): void { $visitor = new HtmlRenderVisitor();
$paragraph = new ParagraphElement('Hello World'); $this->assertEquals('<p>Hello World</p>', $paragraph->accept($visitor));
$heading = new HeadingElement('Title', 1); $this->assertEquals('<h1>Title</h1>', $heading->accept($visitor)); }
public function test_markdown_render_visitor(): void { $visitor = new MarkdownRenderVisitor();
$paragraph = new ParagraphElement('Hello World'); $this->assertEquals("Hello World\n\n", $paragraph->accept($visitor));
$heading = new HeadingElement('Title', 2); $this->assertEquals("## Title\n\n", $heading->accept($visitor)); }
public function test_statistics_visitor(): void { $visitor = new StatisticsVisitor();
$paragraph = new ParagraphElement('Hello World'); $paragraph->accept($visitor);
$heading = new HeadingElement('Title', 1); $heading->accept($visitor);
$image = new ImageElement('image.jpg', 'Alt text'); $image->accept($visitor);
$stats = $visitor->getStats();
$this->assertEquals(1, $stats['paragraphs']); $this->assertEquals(1, $stats['headings']); $this->assertEquals(1, $stats['images']); } }
|
最佳实践
1. 访问者应该无状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php
class StatelessVisitor implements VisitorInterface { public function visit(ElementInterface $element): mixed { return $this->process($element); }
protected function process(ElementInterface $element): mixed { return null; } }
|
2. 使用双分派
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php
interface ElementInterface { public function accept(VisitorInterface $visitor): mixed; }
class ConcreteElement implements ElementInterface { public function accept(VisitorInterface $visitor): mixed { return $visitor->visitConcreteElement($this); } }
|
3. 提供默认实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?php
abstract class BaseVisitor implements VisitorInterface { public function visitElementA(ElementA $element): mixed { return $this->defaultVisit($element); }
public function visitElementB(ElementB $element): mixed { return $this->defaultVisit($element); }
protected function defaultVisit($element): mixed { return null; } }
|
总结
Laravel 13 的访问者模式提供了一种优雅的方式来分离算法和对象结构。通过合理使用访问者模式,可以在不修改对象结构的情况下添加新的操作,提高代码的可扩展性和可维护性。