Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Visitor Laravel Package

hoa/visitor

Hoa\Visitor is a lightweight PHP library for implementing the Visitor design pattern. It provides visitor and visitable interfaces plus base classes, making it easy to add operations to object structures without modifying their classes.

View on GitHub
Deep Wiki
Context7

Getting Started

hoa/visitor is a ~200-line library that formalizes the Visitor pattern: a node exposes accept(Visitor $v, &$data = null), and a visitor exposes visit(Element $e, &$data = null). That is essentially the entire surface area — the value is the shared contract, not the implementation.

composer require hoa/visitor

Two things to check before you write any code:

  1. Read the source first. vendor/hoa/visitor/ contains a handful of interfaces (Visit, Visitor, Element) plus an exception class. They are short enough to read in five minutes, and there is no magic dispatcher — you implement the dispatch yourself.
  2. Check PHP/Laravel compatibility. The package was archived in 2017 with no PHP 8 / Laravel 10-12 support guarantee. It usually works (the code is trivial), but it pulls in hoa/exception and friends, and Composer may warn on composer install. If it fights you, copy the two interfaces into app/Support/Visitor/ and drop the dependency — you lose nothing functionally.

First use case in a Laravel app: a heterogeneous tree you already model with polymorphic relations — a CMS block tree, a menu tree, a rules engine, or an AST for a mini DSL. Today you probably have a giant switch (get_class($node)) scattered across three services; the visitor pattern replaces each scattering with one cohesive class.

<?php
namespace App\Documents;

use Hoa\Visitor;

abstract class Node implements Visitor\Visit
{
    public function accept(Visitor\Visitor $visitor, &$data = null)
    {
        return $visitor->visit($this, $data);
    }
}

final class Document extends Node
{
    /** @param Node[] $children */
    public function __construct(public array $children = []) {}
}
final class Heading extends Node
{
    public function __construct(public string $text, public int $level = 1) {}
}
final class Paragraph extends Node
{
    public function __construct(public string $text) {}
}

Implementation Patterns

1. One dispatcher, many visitors. The library doesn't dispatch, so build a single abstract base that maps element classes to visit{ShortName} methods — that's the "extension point" everyone rebuilds anyway:

namespace App\Documents\Visitors;

use Hoa\Visitor;

abstract class Dispatcher implements Visitor\Visitor
{
    public function visit(Visitor\Element $element, &$data = null)
    {
        $method = 'visit' . class_basename($element);

        if (! method_exists($this, $method)) {
            throw new \LogicException(
                sprintf('No handler [%s] for %s', $method, get_class($element))
            );
        }

        return $this->{$method}($element, $data);
    }
}

2. Keep each visitor a single responsibility. An HtmlRenderer, a WordCounter, an OutlineExtractor, and a PlainTextExporter all share the node model, and you can add a new operation without touching a single node class. This is the whole point — it beats instanceof chains the moment you have more than two operations.

class HtmlRenderer extends Dispatcher
{
    protected function visitDocument(Document $d, &$data): string
    {
        return collect($d->children)
            ->map(fn (Node $c) => $c->accept($this, $data))
            ->implode("\n");
    }
    protected function visitHeading(Heading $h, &$data): string
    {
        return "<h{$h->level}>" . e($h->text) . "</h{$h->level}>";
    }
    protected function visitParagraph(Paragraph $p, &$data): string
    {
        return '<p>' . e($p->text) . '</p>';
    }
}

3. Resolve visitors through the container so they can have real dependencies (config, repositories, formatters). Bind the concrete visitor in a service provider and inject it into controllers, jobs, or event listeners — the visitor is just a service:

// Controller
public function show(Document $doc, HtmlRenderer $renderer)
{
    return view('documents.show', ['html' => $doc->accept($renderer)]);
}

4. Use &$data as a shared traversal context, not as the return value. Return values are for the "computed" result of one node; $data is the accumulator/context shared across the walk (indentation depth, parent path, counters, a SplStack of open tags). It's a reference, so it survives recursion into children cleanly:

$data = ['depth' => 0];
$rendered = $doc->accept($renderer, $data); // $data mutated in place

5. Wire it into Laravel jobs/listeners for expensive walks — e.g. a queued BuildOutline job that hydrates the tree, calls $root->accept(new OutlineVisitor, $data) and persists the result. Visitors are stateless by design, so they're safe to serialize into a job and inject.

6. Test visitors, not nodes. Because dispatch is deterministic, each visitor is a pure unit under test: construct a tiny tree in the test, call accept, assert the string/array. You rarely need a full Laravel test case — plain PHPUnit/Pest is enough.

Gotchas and Tips

  • The package is archived. Pin the version, don't expect fixes, and treat hoa/visitor as vendored infrastructure. If a future PHP minor breaks the old &$data = null signature, forking two interfaces into app/Support/ is a fifteen-minute fix and eliminates the risk entirely.
  • Don't pass expressions by reference. $node->accept(new HtmlRenderer) will throw a fatal error because the second parameter is &$data. Assign to a variable first ($v = new HtmlRenderer; or app(HtmlRenderer::class)).
  • Dispatch is by class name — subclasses silently break it. If Paragraph is extended by Quote, class_basename() yields Quote and your visitParagraph is never called. Either keep node hierarchies shallow, or check instanceof / walk parents in the dispatcher. Anonymous classes (new class extends Node) are effectively unusable for this reason.
  • method_exists is your debugger. The most common failure is a typo'd handler name — dump get_class($element) inside visit() the moment something unexpectedly throws LogicException.
  • Don't retrofit accept() onto Eloquent models. Making an Article model implement the visitor interface pollutes the persistence layer and couples your schema to presentation logic. Wrap models in small DTO nodes (ArticleNode, CategoryNode) inside the same tree you're already building — the visitor then depends on your domain, not on Eloquent.
  • Circular structures will recurse forever. The library will happily loop on a self-referencing node. Track visited spl-object-ids in $data when the tree can be a graph rather than a tree.
  • The Hoa\Exception dependency is dead weight. The library's exception class extends Hoa's own base exception; if you fork the interfaces, you can drop the whole hoa/exception subtree from composer.lock as a bonus.
  • Skip it for trivial cases. If you have one operation and three node types, a match on get_class($node) is clearer, dependency-free, and PHP 8-friendly. Reach for hoa/visitor (or its forked interfaces) only when the number of operations will grow independently of the node set.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource