sanmai/hoa-protocol
PHP library from the Hoa project offering a unified abstraction for resources and paths. It implements a hoa:// protocol with mount points and routers, letting applications resolve virtual paths across filesystems and custom backends in a consistent, extensible way. Maintained fork for legacy projec...
This package is a stripped fork of hoa/protocol: it keeps the Protocol/Node tree and the hoa:// path composition, but removes resolve(), so it no longer translates a hoa://… node path into a real filesystem path. Treat it as a namespaced path registry, not a filesystem abstraction. It is archived, has no dependents, and no auto-discovery — plan to pin it (or vendor it) rather than assume maintenance.
Minimal steps:
composer require sanmai/hoa-protocol and confirm the PSR-4 mapping (Hoa\Protocol\ → src/) in the installed composer.json.src/Protocol.php — the singleton root and get/set accessors.src/Node/Node.php and src/Node/Reach.php — how child nodes and aliases are composed.hoa/protocol — this is the only reliable spec for what was removed.(string) $node returns in your installed version. On the fork it should yield a hoa://… URI, not a path. If you need a path, you must supply the resolution yourself.First realistic use case: a module/theme path registry. Register each module's root once during boot, then compose stable, namespaced URIs (hoa://Modules/Blog/Resources/views) that other code can consume — and resolve them through your own service layer.
// app/Providers/ProtocolServiceProvider.php
use Hoa\Protocol\Protocol;
public function register(): void
{
$protocol = Protocol::getInstance();
foreach (config('modules.paths', []) as $name => $path) {
$protocol->set("Modules/{$name}", new \Hoa\Protocol\Node\Node());
// Store the real path yourself — the package no longer does this.
$this->app->make('modules.resolver')->map("hoa://Modules/{$name}", $path);
}
}
Wrap it in a Laravel service. Don't inject Protocol::getInstance() into controllers. Bind the singleton once in a service provider and expose a small PathRegistry object with register(), get(), and toPath(). This gives you a single seam to swap out if the package dies (which, being archived, it will).
final class PathRegistry
{
public function __construct(private Protocol $protocol, private array $roots = []) {}
public function register(string $name, string $base): void
{
$this->protocol->set($name, new Node\Node());
$this->roots[$name] = rtrim($base, DIRECTORY_SEPARATOR);
}
public function path(string $uri): string
{
// Re-implement the removed resolve() against your own root map.
[$root, $rest] = explode('/', substr($uri, 6), 2);
return $this->roots[$root] . DIRECTORY_SEPARATOR . $rest;
}
}
Config-driven registration. Keep the module list in config/modules.php and register in boot(). This keeps the singleton mutation in one place and avoids ordering surprises when other packages register roots too.
Use Node\Reach for shared resources. A Reach node aliases another node by reference, which is useful for a Public/Assets root shared across modules without duplicating path strings. Verify the fork did not gut the Reach traversal — check src/Node/Reach.php before relying on it.
Compose, then resolve at the boundary. Keep hoa://… URIs inside your domain (Blade, config, manifests) and convert to real paths only at the edge (controllers, file reads, Storage calls). This makes the eventual removal of the package a matter of dropping one resolver class.
Blade/helper integration. Add a @hoa('Modules/Blog/Resources') directive or a hoa_path() helper that delegates to PathRegistry::path(), so templates never touch the singleton directly.
Asset manifests. If you already emit logical asset identifiers in a manifest, this is a clean place to store them — register a Assets root, emit hoa://Assets/css/app.css, and resolve in a Vite-style helper. It's a thin win, but it is a real one.
Testing. Rebinding the singleton per test is essential — see gotchas.
resolve() is gone; __toString() is a URI. Any code ported from upstream hoa/protocol that does file_exists($protocol['…']) or realpath((string) $node) will silently receive a hoa:// string and fail. Audit for resolve calls on upgrade.hoa:// stream wrapper may be dead. Upstream registered a PHP stream wrapper around resolve(). If the fork left the wrapper but removed the resolver, file_get_contents('hoa://…') will not work. Don't register the wrapper; keep resolution in your own service.Protocol::getInstance() persists within the process. In tearDown() (or a RefreshApplication-aware base test case), reset roots or rebind your PathRegistry so module registrations from one test don't bleed into the next.extra.laravel.providers; register your service provider manually in config/app.php (or bootstrap/providers.php on Laravel 11+).hoa/protocol, both packages declare Hoa\Protocol\Protocol — the autoloader will pick one arbitrarily. Run composer why hoa/protocol and add a replace/conflict rule if needed.set('Modules/Blog', …) will silently create intermediate nodes; a typo like Modules/Blog/ creates a distinct trailing-slash node. Normalize names in your registry wrapper.composer audit step and a calendar reminder to revisit. Because the package is tiny, vendoring the two or three classes you actually use into app/Support/ is a defensible exit plan.Node\Node (to attach metadata such as a base path) and wrapping Protocol behind your own interface. Prefer the latter — it keeps the archived dependency at arm's length.app_path(), base_path(), and config() already cover it. This package earns its place only when you have a genuinely dynamic, plugin-style set of named roots that must be composed as URIs.How can I help you explore Laravel packages today?