gcego/registry
gcego/registry is a lightweight Laravel package that provides a simple registry for registering, resolving, and managing shared values, services, and configuration through a clean, fluent API, helping centralize reusable objects and app bindings.
The package is a definition-discovery layer: you point it at directories ("roots"), each root is tagged with a "kind" and bound to a contract, and the package finds PHP definition files and resolves them into classes you can consume at runtime.
Minimal setup:
composer require <vendor>/<package>
php artisan vendor:publish --tag=<package>-config
config/<package>.php, declare at least one root — the directory plus the kind and the contract its definitions must satisfy:
'roots' => [
'actions' => [
'path' => app_path('Actions'),
'kind' => 'action',
'contract'=> App\Contracts\Action::class,
],
],
DefinitionProvider:
<?php
use App\Actions\SendInvoice;
return SendInvoice::class; // v1.1.0: may be omitted if the file declares exactly one DefinitionProvider
app(\Vendor\Package\Registry::class)->refresh();
(or run the package's cache-rebuild artisan command in a deploy step).Where to look first:
config/<package>.php — roots, kinds, contracts, cache path, verification flags.Registry — refresh(), plus the accessor your app uses to resolve definitions.DiscoveredFile — the value object you get back (path, resolved class, alternatives, roots). This is the primary debugging surface.First real use case: auto-registering a set of handler/action classes (queueable actions, policies, webhook handlers, importers) so you never hand-edit a Provider array again. Put a folder in a root, drop in a class, refresh — it is available.
One root per kind. The "kind" is the unit of intent, not just a directory. Keep folders and kinds 1:1 (Actions/, Policies/, Importers/). This makes contract-based resolution predictable and lets you later split or merge folders without touching consumer code.
Lean on class entrypoints (v1.1.0). For the common case — one class per file that implements DefinitionProvider — you can now drop the return Foo::class; line. Definition files become plain class files, which means your IDE refactors and static analysis treat them normally. Keep the explicit return when a file is intentionally a map of several definitions or declares helper classes alongside the provider.
Overlapping roots and contract routing. When two roots of different kinds match the same file (for example, a Domain/ folder scanned both as actions and as policies), v1.1.0 resolves it to the registered kind whose contract accepts the loaded definition. Use this deliberately: put shared domain folders in multiple roots and let the contract decide. Verify with:
$file = $registry->find(SendInvoice::class);
dump($file->roots()); // which roots matched
dump($file->alternatives); // other candidate classes considered
Custom discovery with withAlternative(). When a file exposes more than one plausible class, or you are layering a new class over an old one for BC, attach candidates explicitly rather than relying on inference:
$file = $file->withAlternative(LegacyHandler::class);
The resolver still chooses by contract, so alternatives are a safe way to stage a migration.
Cache discipline in deploys. The registry is expensive to build and cheap to read once cached. Treat cache rebuild as a build artifact:
composer install --no-dev and code deploy.refresh() on every request.Testing pattern. Reset the registry per test so class-declaring files aren't carried across tests:
protected function setUp(): void
{
parent::setUp();
app(Registry::class)->refresh();
}
Then assert behaviour, not discovery internals. Use roots() only when asserting routing.
Long-running consumers. Queue workers, Octane and php artisan serve keep classes in memory. Resolve through the registry rather than storing concrete class strings in config, so a restart-and-refresh is the only operational action required after a deploy.
Restart workers after deploying class changes. A class-declaring entrypoint that changed after it was loaded now throws DefinitionLoadException asking for a restart. This is intentional and much better than silently serving the old class — but it means your deploy runbook must restart Horizon/queue workers and reload Octane. Add it to the deploy checklist.
Duplicate class declarations are now an exception, not a fatal. As of v1.1.0, if two entrypoints declare the same class you get a DefinitionLoadException. Inspect the message for both paths, then consolidate (one definition file owning the class) or register one path as an alternative on the other. Silent duplicates across roots are almost always a copy-paste error.
Registry::refresh() is safe now, but not free. Definition files that declare classes are included at most once per PHP process, so refresh, cache rebuilds, verification and cache reads no longer fatal with "cannot declare class". Refresh in dev and tests; avoid it on the hot path — it re-scans roots.
Ambiguous roots with no accepting contract. Contract-based routing only picks a kind if a contract accepts the loaded definition. If a file matches multiple kinds and none of the contracts match, you get a load exception. Symptom: a definition exists and is valid but is "invisible". Check roots() and confirm the class actually implements the contract for the kind you expect.
Files that declare no classes are unchanged. This is the compatibility guarantee for v1.1.0 — config-array-style definition files, closures, and plain return arrays behave exactly as before. Upgrade is non-breaking for that style; the class-entrypoint behaviour is purely additive.
Debugging checklist when a definition vanishes:
Registry::refresh() and confirm the file is scanned (DiscoveredFile exists for the path).roots() — did the root you expect match?alternatives — was a different class inferred?DefinitionLoadException, not silent wrongness.Extension points. DiscoveredFile is a value object (withAlternative() is immutable-style, returning a new instance), so custom discovery or test fixtures can compose candidates without mutating shared state. roots() is the stable hook for diagnostics and for routing decisions in your own code.
Config quirks. Cache path and verification are environment-sensitive — point them at a writable path per environment and keep the cache out of VCS. If you add roots at runtime (in a service provider), rebuild the cache after doing so or the manifest won't know about them.
How can I help you explore Laravel packages today?