Search by

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.

Maintainers

Package info

github.com/ca-santos/auto-class-dicovery

pkg:composer/caue-santos/auto-class-discovery

Transparency log

Statistics

Installs: 221

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-08-31 19:45 UTC

This package is auto-updated.

Last update: 2026-08-31 19:55:11 UTC


README

GitHub Workflow Status Packagist Packagist Packagist

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::class constant fetch, an anonymous class, or the word "class" sitting in a comment or a string.
  • Returns typed ClassDefinition objects (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 AutoClassDiscovery facade (or resolve CaueSantos\AutoClassDiscovery\AutoClassDiscovery from 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 ClassDefinition objects, not raw arrays. Array access ($entry['interfaces']) still works for backward compatibility, but the parent key was corrected/aliased to parents (it always held every ancestor, not a single parent).
  • manualDiscover() now takes a class name and detects its kind automatically instead of taking a $type string and a pre-built $item array; it also defaults to keying by class name instead of appending numerically.
  • discoverFromCache(array $cache) is kept as a deprecated alias for the new hydrate(array $cached).

Security

If you discover any security related issues, please email cauesantosre4@gmail.com instead of using the issue tracker.

Credits