pollora/discovery
| Install | |
|---|---|
composer require pollora/discovery |
|
| Latest Version: | v0.1.0 |
| PHP: | ^8.3 |
| Laravel: | 11 · 12 · 13 |
| License: | MIT |
| Last Updated: | Oct 12, 2026 |
| Links: | GitHub · Packagist |
Scans directories for PHP classes, reads each class once, and hands it to every discovery that wants it: one finds #[Action] methods, another #[PostType] classes, a third REST controllers. Each discovery collects what it found, then applies it in one pass. Scans are cached, a class is reflected once whatever the number of discoveries, and each item is applied once per request.
It is the engine behind Pollora's attributes, and it runs without Laravel: in a WordPress plugin, a Bedrock site or any PHP project.
Part of Pollora, the Laravel framework for WordPress. In a Pollora project it is already installed and wired: themes, plugins and modules are scanned for you, and your own discoveries are picked up from the container. See Auto-discovery.
Installation
composer require pollora/discovery
Requires PHP 8.3+. It builds on spatie/php-structure-discoverer, which brings illuminate/collections; nothing else from Laravel is needed.
Quick start
A discovery says what it looks for in discover() and what to do with it in apply():
use Pollora\Discovery\Domain\Contracts\DiscoveryInterface;
use Pollora\Discovery\Domain\Contracts\DiscoveryLocationInterface;
use Pollora\Discovery\Domain\Contracts\ReflectionCacheInterface;
use Pollora\Discovery\Domain\Services\IsDiscovery;
use Spatie\StructureDiscoverer\Data\DiscoveredClass;
use Spatie\StructureDiscoverer\Data\DiscoveredStructure;
final class ShortcodeDiscovery implements DiscoveryInterface
{
use IsDiscovery;
public function discover(DiscoveryLocationInterface $location, DiscoveredStructure $structure, ?ReflectionCacheInterface $reflectionCache = null): void
{
if (! $structure instanceof DiscoveredClass) {
return;
}
$class = $structure->namespace.'\\'.$structure->name;
foreach ($reflectionCache->getClassAttributes($class, Shortcode::class) as $attribute) {
$this->getItems()->add($location, [$attribute->newInstance()->tag, $class]);
}
}
public function apply(): void
{
foreach ($this->getItems() as [$tag, $class]) {
add_shortcode($tag, [new $class, 'render']);
}
}
}
Point the engine at a directory and run it:
use Pollora\Discovery\Domain\Models\DiscoveryLocation;
use Pollora\Discovery\Infrastructure\Services\DiscoveryEngine;
(new DiscoveryEngine)
->addLocation(new DiscoveryLocation('MyPlugin\\', __DIR__.'/src'))
->addDiscovery('shortcodes', new ShortcodeDiscovery)
->run();
run() is discover() then apply(). Running again applies only what is new: an item is never applied twice.
Fitting it to an environment
Every dependency of DiscoveryEngine is optional. Without them, it builds classes by reflection, keeps no persistent cache and runs only the discoveries added to it.
| Argument | Contract | Default |
|---|---|---|
resolver |
InstanceResolverInterface, builds discoveries given by class name and the classes they find |
ReflectionInstanceResolver (constructor parameters filled from defaults and class types); ContainerInstanceResolver wraps any PSR-11 container |
registrar |
DiscoveryRegistrarInterface, adds the environment's discoveries before each run |
none |
cacheManager |
DiscoveryCacheManager, with a Spatie DiscoverCacheDriver |
no persistent cache |
logger |
PSR-3 | none |
skipClasses, skipPaths |
classes, or path fragments, never handed to discoveries | none |
Caching scans
Scanning walks every file of a location, so cache it outside development. Psr16DiscoverCacheDriver stores scans in any PSR-16 cache, such as WordPress transients or the object cache; Spatie's own drivers work too.
use Pollora\Discovery\Infrastructure\Cache\Psr16DiscoverCacheDriver;
use Pollora\Discovery\Infrastructure\Services\DiscoveryCacheManager;
$engine = new DiscoveryEngine(
cacheManager: new DiscoveryCacheManager(
cacheDriver: new Psr16DiscoverCacheDriver($psr16Cache, ttl: DAY_IN_SECONDS),
logger: $logger,
),
);
With warnOnSlowScans: true (meant for development, where the cache is off), a location that takes more than 250 ms to scan is logged: it usually holds node_modules, vendor or a build output.
Keeping a class out
use Pollora\Attributes\SkipDiscovery;
#[SkipDiscovery] // no discovery sees it
#[SkipDiscovery(except: [HookDiscovery::class])] // only the ones listed do
final class InternalHelper {}
Public API
The Pollora\Discovery\Domain\Contracts\* and Pollora\Discovery\Domain\Services\* namespaces are the stable contracts for writing discoveries and follow semantic versioning. Infrastructure classes may change in a minor version.
Testing
composer test # unit tests, PHPStan, Pint
composer test:benchmark # time and memory bounds on generated classes
composer benchmark # full report: php tests/Benchmark/run.php [--quick|--stress|N…]
License
MIT. See LICENSE.
Related Packages
PHP attributes to declare WordPress post types, taxonomies, hooks, REST routes,...
A package for creating taxonomies and terms to categorise Eloquent models
A package that will add WordPress integration test suite with Pest framework
Version History
| Version | Released | PHP | Laravel | License |
|---|---|---|---|---|
| v0.1.0 | ^8.3 | ^11.0| | MIT |