puli/discovery
Puli Discovery is a PHP library for locating and managing resources across packages. It provides a discovery API to register resource bindings, find files by type or path, and integrate resource lookup into Puli-based applications.
puli/discovery is the read/write API for Puli’s artifact registry: you publish bindings (a resource + a binding type + parameters) and later ask the DiscoveryManager for them. It sits underneath the puli CLI, so the first thing to check is whether you actually need the CLI or just the library.
Minimal install:
composer require puli/discovery:^1.0
Note the package is abandoned (last release Jan 2016). Pull it into a sandbox or a legacy project only, and pin the version explicitly.
Where to look first, in this order:
Puli\Discovery\Api\Discovery — the interface you should type-hint against. Methods: getTypes(), getType(), hasType(), getBindings(), findBindings(), findBindingsByType(), hasBindings(), hasBinding().Puli\Discovery\DiscoveryManager — the concrete implementation. Constructor takes a DiscoveryStore, an optional TypeRegistry, and an optional EventDispatcherInterface.Puli\Discovery\Api\Type\BindingType + Puli\Discovery\Api\Binding\ResourceBinding — what you’re actually storing.Puli\Discovery\Store\JsonDiscovery (or NullDiscovery / KeyValueStoreDiscovery) — where bindings are persisted.First use case: a lightweight plugin registry. Tag classes with a custom type, then resolve them in a bootstrap step:
$discovery = new DiscoveryManager(new JsonDiscovery(__DIR__ . '/discovery.json'));
$discovery->defineType(new BindingType('app/plugin'));
$discovery->addBinding(new ResourceBinding($resource, 'app/plugin'));
foreach ($discovery->findBindingsByType('app/plugin') as $binding) {
$class = $binding->getResource()->getPath();
// register $class somewhere
}
If you’re in Laravel, the practical equivalent is a service provider that constructs the DiscoveryManager once and binds Discovery into the container.
Service provider + singleton. Puli has no Laravel integration, so wire it yourself. Register the manager as a singleton and resolve it by interface:
$this->app->singleton(Discovery::class, function ($app) {
$store = new JsonDiscovery($app->storagePath('framework/puli.json'));
$manager = new DiscoveryManager($store);
foreach (config('puli.types', []) as $typeName) {
$manager->defineType(new BindingType($typeName));
}
foreach ($app['events']->makeListeners ?? [] as $l) { /* attach */ }
return $manager;
});
Keep this in a dedicated PuliServiceProvider so the package is removable in one file.
Programmatic publication over config. Puli’s native workflow is puli.json in each package plus the CLI to publish. In Laravel you rarely control installed package manifests, so prefer publishing bindings programmatically during boot (or in a cached bootstrap step) and read via findBindingsByType(). Treat the JSON store as a build artifact, not source.
Custom binding types. Subclass BindingType (or implement the interface) when a plain type name isn’t enough — e.g. a type that carries a serializer/validator for parameters. Register once with defineType(); look it up with getType($name). This is the main extension point beyond listeners.
Event-driven side effects. DiscoveryManager dispatches BindingTypeRegisteredEvent, BindingTypeRemovedEvent, ResourceBindingAddedEvent, and ResourceBindingRemovedEvent. Implement Puli\Discovery\Api\DiscoveryListener and addListener() to keep an in-memory index in sync, or to invalidate a cache whenever bindings change. This mirrors Laravel’s event system closely enough that you can forward Puli events onto the Laravel dispatcher in the provider.
Two-phase boot. In a Laravel app, discovery is naturally two-phase: (1) publish bindings during register()/a cached bootstrap, (2) consume them in boot() or in console commands. Avoid querying bindings inside hot request paths; resolve once and cache the result, because JsonDiscovery decodes its file per call in the reference implementation.
Query filtering. findBindings($typeName, $query) accepts a query string understood by the underlying store; findBindingsByType() returns all bindings of a type. For most registries the latter is all you need, and it’s the more predictable of the two.
Testing. Swap JsonDiscovery for NullDiscovery in tests, or point JsonDiscovery at a temp file per test and rebuild the manager. Because everything is behind the Discovery interface, faking it is trivial.
each()-era idioms in transitive deps). Isolate it behind your own interface so removal is a one-file change.extra section plus PackageManifest / installed.json. For new Laravel work, prefer a Composer-extra-based plugin registry; use puli/discovery only if you’re maintaining a Puli-era codebase.defineType() twice with the same name raises an exception. Puli’s bootstrapping typically re-registers built-in types (puli/binding-type/class, etc.) — guard with hasType() before defining.ResourceBinding wraps a puli/repository Resource, not a raw string. getResource()->getPath() is usually what you want, but it may be a repository-relative path; getRepositoryPath() or the puli:// scheme is what you need if you’re feeding it to an autoloader.puli.json alongside composer.json. If your package manager also writes there (or if you deploy without the file), a stale discovery.json silently yields zero bindings. Assert hasBindings() in a health check.$discovery->getTypes() and $discovery->getBindings() before blaming find logic — most “discovery doesn’t work” reports are empty registries, not broken queries. Attach a listener that logs every ResourceBindingAddedEvent during development.ResourceBindingAddedEvent/ResourceBindingRemovedEvent listeners rather than by hand.DiscoveryListener (react to changes), custom BindingType (validate/serialize params), and the DiscoveryStore implementations (swap JSON for a DB-backed store without touching call sites). Everything else in the package is implementation detail.How can I help you explore Laravel packages today?