caue-santos / auto-class-discovery
Scan directories for classes, interfaces, traits and enums and inspect their parents, interfaces, traits and attributes without writing manual reflection code.
Requires
- php: ^8.1
- illuminate/console: ^10.0|^11.0|^12.0|^13.0
- illuminate/filesystem: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^10.5|^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-08-31 19:55:11 UTC
README
Scan a directory and get back every class, interface, trait and enum declared in it — along with its parents, implemented interfaces, used traits and PHP attributes — without writing any manual ReflectionClass code.
Typical uses: auto-binding repository implementations to their contracts, auto-registering event listeners/jobs tagged with an attribute, building a plugin system, or asserting architectural rules in your test suite ("every class in App\Models must extend Model").
Features
- Scans directories using PHP's own tokenizer (not regex), so it correctly finds every declaration in a file — multiple classes per file, abstract/final classes, traits, PHP 8.1 enums — and never gets confused by a
Foo::classconstant fetch, an anonymous class, or the word "class" sitting in a comment or a string. - Returns typed
ClassDefinitionobjects (parents, interfaces, traits, attributes, abstract/final/enum flags, source file) instead of raw arrays. - Fluent query helpers backed by
Illuminate\Support\Collection:implementing(),extending(),using(),withAttribute(). - Optional grouping: scan several directories in one call and keep their results separated (
discover(['repositories' => ..., 'policies' => ...])). - File-based caching, the same pattern Laravel uses for routes/config, plus two artisan commands (
auto-class-discovery:cache/:clear) so production doesn't pay the scanning cost on every request. - Manual registration (
manualDiscover()) for classes you already know about, e.g. from third-party packages.
Requirements
- PHP 8.1+
- Laravel 10, 11, 12 or 13
Installation
composer require caue-santos/auto-class-discovery
The service provider is auto-discovered by Laravel. To publish the config file:
php artisan vendor:publish --tag=auto-class-discovery-config
Quick start
use CaueSantos\AutoClassDiscovery\Facades\AutoClassDiscovery; $discovered = AutoClassDiscovery::discover(app_path('Domain')); // $discovered = ['class' => [...], 'interface' => [...], 'trait' => [...], 'enum' => [...]] foreach ($discovered['class'] as $fqcn => $definition) { // $definition is a CaueSantos\AutoClassDiscovery\Support\ClassDefinition }
Or work through the query helpers instead of the raw tree — this is the recommended way to consume the results:
AutoClassDiscovery::discover(app_path('Domain')); AutoClassDiscovery::classes(); // every discovered class AutoClassDiscovery::implementing(ShouldQueue::class); // classes implementing an interface AutoClassDiscovery::extending(Model::class); // classes extending a base class AutoClassDiscovery::using(HasFactory::class); // classes using a trait AutoClassDiscovery::withAttribute(AsCommand::class); // classes carrying a PHP attribute
Every one of the helpers above returns an Illuminate\Support\Collection<int, ClassDefinition>, so the usual collection methods (map, filter, sortBy, ...) are available.
The ClassDefinition object
Each discovered symbol is described by a CaueSantos\AutoClassDiscovery\Support\ClassDefinition:
$definition->class; // "App\Models\User" $definition->type; // "class" | "interface" | "trait" | "enum" $definition->interfaces; // ["Illuminate\Contracts\Auth\Authenticatable", ...] $definition->parents; // every ancestor class, most-derived first $definition->traits; // traits used directly by the class $definition->attributes; // fully qualified names of PHP attributes declared on it $definition->abstract; // bool $definition->final; // bool $definition->enum; // bool $definition->file; // absolute path it was found in, or null for manualDiscover()
Plus helper methods:
$definition->isClass(); $definition->isInterface(); $definition->isTrait(); $definition->isEnum(); $definition->isAbstract(); $definition->isFinal(); $definition->isInstantiable(); // a non-abstract class $definition->implements(Authenticatable::class); $definition->extends(Model::class); $definition->uses(HasFactory::class); $definition->hasAttribute(AsCommand::class); $definition->attributeInstances(AsCommand::class); // instantiated attribute objects, args and all $definition->shortName(); // "User" $definition->namespace(); // "App\Models" $definition->newInstance(...$args); // throws if not instantiable $definition->reflection(); // a fresh ReflectionClass $definition->toArray(); // plain array, also used by json_encode()
ClassDefinition also implements ArrayAccess, mirroring the shape PHP's own class_implements()/class_parents()/class_uses() return (a name => name map) under the interfaces, parents/parent and traits keys — so $definition['interfaces'] keeps working if you're used to that shape.
Grouping discovery paths
Pass an associative array to scan several directories while keeping their results apart:
AutoClassDiscovery::discover([ 'repositories' => app_path('Repositories'), 'policies' => app_path('Policies'), ]); AutoClassDiscovery::classes('repositories'); AutoClassDiscovery::implementing(RepositoryContract::class, 'policies'); // scoped to one group
Every query helper (classes(), interfaces(), traits(), enums(), ofType(), all(), implementing(), extending(), using(), withAttribute()) accepts an optional $group argument.
Manual registration
Register a class you already know about — say, one shipped by another package — without scanning the filesystem for it. Its kind (class/interface/trait/enum) is detected automatically:
AutoClassDiscovery::manualDiscover(SomeVendorRepository::class, group: 'repositories');
An InvalidDiscoveryTypeException is thrown if the class/interface/trait/enum doesn't actually exist (typo, missing use, etc).
Caching
Scanning the filesystem on every request is wasteful in production. Configure your discovery paths once in config/auto-class-discovery.php:
'paths' => [ 'repositories' => app_path('Repositories'), 'policies' => app_path('Policies'), ],
then either run the artisan command as part of your deploy step (alongside config:cache/route:cache):
php artisan auto-class-discovery:cache php artisan auto-class-discovery:clear
or let the package cache itself lazily on first use:
AutoClassDiscovery::discoverOrCache( config('auto-class-discovery.paths'), config('auto-class-discovery.cache_path'), );
discoverOrCache() reads the cache file when it exists and only touches the filesystem otherwise, writing the cache right after. Lower-level building blocks are also available: cache(string $path), loadFromCache(string $path): bool, clearCache(string $path).
Real-world recipes
Auto-bind repository implementations to their contracts
Given a convention where every class under App\Repositories implements exactly one repository contract:
// AppServiceProvider::register() AutoClassDiscovery::discover(app_path('Repositories')); foreach (AutoClassDiscovery::implementing(RepositoryContract::class) as $definition) { foreach ($definition->interfaces as $interface) { if ($interface !== RepositoryContract::class && is_subclass_of($interface, RepositoryContract::class)) { $this->app->bind($interface, $definition->class); } } }
Auto-register event listeners tagged with an attribute
#[Attribute(Attribute::TARGET_CLASS)] class ListensTo { public function __construct(public string $event) {} } #[ListensTo(OrderShipped::class)] class SendShipmentNotification { public function handle(OrderShipped $event): void { /* ... */ } } // AppServiceProvider::boot() AutoClassDiscovery::discover(app_path('Listeners')); foreach (AutoClassDiscovery::withAttribute(ListensTo::class) as $definition) { foreach ($definition->attributeInstances(ListensTo::class) as $attribute) { Event::listen($attribute->event, $definition->class); } }
Assert architectural conventions in a test
it('every model extends Eloquent', function () { $discovery = new AutoClassDiscovery(); $discovery->discover(app_path('Models')); expect($discovery->classes()->every(fn ($definition) => $definition->extends(Model::class)))->toBeTrue(); });
Testing
composer install
composer test
Upgrading to 1.0
1.0 is a rewrite of the pre-1.0 dev-master version and includes breaking changes — see CHANGELOG.md for the full list:
- The class is no longer fully static — use the
AutoClassDiscoveryfacade (or resolveCaueSantos\AutoClassDiscovery\AutoClassDiscoveryfrom the container) instead of calling the concrete class's methods statically. The old static state was also unsafe under Octane/long-running workers. - Discovered entries are now
ClassDefinitionobjects, not raw arrays. Array access ($entry['interfaces']) still works for backward compatibility, but theparentkey was corrected/aliased toparents(it always held every ancestor, not a single parent). manualDiscover()now takes a class name and detects its kind automatically instead of taking a$typestring and a pre-built$itemarray; it also defaults to keying by class name instead of appending numerically.discoverFromCache(array $cache)is kept as a deprecated alias for the newhydrate(array $cached).
Security
If you discover any security related issues, please email cauesantosre4@gmail.com instead of using the issue tracker.