pixielity/laravel-discovery

Powerful class discovery system for Laravel with attribute-based scanning, directory traversal, and monorepo support

Installs: 0

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Forks: 0

pkg:composer/pixielity/laravel-discovery

v1.0.3 2026-02-09 13:49 UTC

This package is auto-updated.

Last update: 2026-02-09 10:07:20 UTC


README

Configuration File

config/discovery.php:

<?php

return [
    /*
     * Cache Configuration
     */
    'cache' => [
        'enabled' => env('DISCOVERY_CACHE_ENABLED', env('APP_ENV') !== 'local'),
        'path' => base_path('bootstrap/cache/discovery'),
        'ttl' => null,  // Forever in production
    ],

    /*
     * Monorepo Patterns
     */
    'monorepo' => [
        'packages' => [
            'path' => 'packages/*',
            'namespace' => 'Pixielity\{package}',
        ],
        'modules' => [
            'path' => 'modules/*',
            'namespace' => 'Modules\{module}',
        ],
    ],

    /*
     * Pre-configured Paths
     */
    'paths' => [
        'settings' => [
            'packages/*/src/Settings',
            'modules/*/src/Settings',
            'app/Settings',
        ],
        'health' => [
            'packages/*/src/Health',
            'modules/*/src/Health',
        ],
        'commands' => [
            'packages/*/src/Console/Commands',
            'modules/*/src/Console/Commands',
        ],
    ],
];

Using Pre-configured Paths

$paths = config('discovery.paths.settings');
$settings = Discovery::directories($paths)->get();

Monorepo Configuration

The package automatically resolves namespaces for monorepo structures:

packages/
├── Auth/src/Settings/AuthSettings.php      → Pixielity\Auth\Settings\AuthSettings
├── Billing/src/Settings/BillingSettings.php → Pixielity\Billing\Settings\BillingSettings
└── Users/src/Settings/UserSettings.php     → Pixielity\Users\Settings\UserSettings

⬆ Back to Top

🎯 Real-World Examples

Example 1: Auto-Register Settings Classes

Automatically discover and register all settings classes in your application.

// In your service provider
public function boot(): void
{
    $settings = Discovery::attribute(AsSetting::class)
        ->directories(config('discovery.paths.settings'))
        ->extending(Settings::class)
        ->instantiable()
        ->cached('settings')
        ->get();

    foreach ($settings as $class => $metadata) {
        $this->app->singleton($class);
    }
}

Attribute Definition:

#[Attribute(Attribute::TARGET_CLASS)]
class AsSetting
{
    public function __construct(
        public string $key,
        public string $group = 'general',
        public bool $encrypted = false,
    ) {}
}

Settings Class:

#[AsSetting(key: 'app.settings', group: 'application')]
class AppSettings extends Settings
{
    public string $siteName;
    public string $siteUrl;
    public bool $maintenanceMode = false;
}

Example 2: Dynamic Route Registration

Automatically register routes from controller methods with attributes.

// In your RouteServiceProvider
public function boot(): void
{
    $routes = Discovery::methods(Route::class)
        ->directories('app/Http/Controllers')
        ->cached('routes')
        ->get();

    foreach ($routes as $identifier => $metadata) {
        [$class, $method] = explode('::', $identifier);
        $route = $metadata['attribute'];

        Route::{strtolower($route->method)}($route->path, [$class, $method])
            ->name($route->name)
            ->middleware($route->middleware);
    }
}

Route Attribute:

#[Attribute(Attribute::TARGET_METHOD)]
class Route
{
    public function __construct(
        public string $method,
        public string $path,
        public ?string $name = null,
        public array $middleware = [],
    ) {}
}

Controller:

class UserController extends Controller
{
    #[Route(method: 'GET', path: '/users', name: 'users.index', middleware: ['auth'])]
    public function index()
    {
        return User::paginate();
    }

    #[Route(method: 'POST', path: '/users', name: 'users.store', middleware: ['auth', 'admin'])]
    public function store(Request $request)
    {
        return User::create($request->validated());
    }

    #[Route(method: 'GET', path: '/users/{user}', name: 'users.show')]
    public function show(User $user)
    {
        return $user;
    }
}

Example 3: Plugin System

Build a flexible plugin system with automatic plugin discovery.

// Plugin Interface
interface Plugin{
    public function register(): void;
    public function boot(): void;
    public function getName(): string;
    public function getVersion(): string;
}

// Plugin Attribute
#[Attribute(Attribute::TARGET_CLASS)]
class Plugin
{
    public function __construct(
        public string $name,
        public string $version,
        public bool $enabled = true,
        public array $dependencies = [],
    ) {}
}

// Example Plugin
#[Plugin(name: 'Analytics', version: '1.0.0', enabled: true)]
class AnalyticsPlugin implements Plugin{
    public function register(): void
    {
        // Register plugin services
    }

    public function boot(): void
    {
        // Boot plugin
    }

    public function getName(): string
    {
        return 'Analytics';
    }

    public function getVersion(): string
    {
        return '1.0.0';
    }
}

// Plugin Manager
class PluginManager
{
    public function loadPlugins(): void
    {
        $plugins = Discovery::attribute(Plugin::class)
            ->implementing(PluginInterface::class)
            ->where('enabled', true)
            ->instantiable()
            ->cached('plugins')
            ->get();

        foreach ($plugins as $class => $metadata) {
            $plugin = app($class);

            // Check dependencies
            $this->checkDependencies($metadata['attribute']->dependencies);

            // Register and boot plugin
            $plugin->register();
            $plugin->boot();

            Log::info("Plugin loaded: {$plugin->getName()} v{$plugin->getVersion()}");
        }
    }
}

Example 4: Health Check System

Automatically discover and run health checks.

// Health Check Interface
interface HealthCheck{
    public function check(): HealthCheckResult;
    public function getName(): string;
}

// Health Check Attribute
#[Attribute(Attribute::TARGET_CLASS)]
class HealthCheck
{
    public function __construct(
        public string $name,
        public int $priority = 0,
        public bool $critical = false,
    ) {}
}

// Example Health Check
#[HealthCheck(name: 'Database', priority: 10, critical: true)]
class DatabaseHealthCheck implements HealthCheck{
    public function check(): HealthCheckResult
    {
        try {
            DB::connection()->getPdo();
            return HealthCheckResult::healthy('Database connection successful');
        } catch (\Exception $e) {
            return HealthCheckResult::unhealthy('Database connection failed: ' . $e->getMessage());
        }
    }

    public function getName(): string
    {
        return 'Database';
    }
}

// Health Check Controller
class HealthController extends Controller
{
    public function check()
    {
        $checks = Discovery::attribute(HealthCheck::class)
            ->implementing(HealthCheckInterface::class)
            ->instantiable()
            ->cached('health-checks')
            ->get();

        $results = [];
        $allHealthy = true;

        foreach ($checks as $class => $metadata) {
            $check = app($class);
            $result = $check->check();

            $results[] = [
                'name' => $check->getName(),
                'status' => $result->status,
                'message' => $result->message,
                'critical' => $metadata['attribute']->critical,
            ];

            if (!$result->isHealthy() && $metadata['attribute']->critical) {
                $allHealthy = false;
            }
        }

        return response()->json([
            'status' => $allHealthy ? 'healthy' : 'unhealthy',
            'checks' => $results,
        ], $allHealthy ? 200 : 503);
    }
}

Example 5: Event Listener Discovery

Automatically register event listeners.

// Listener Attribute
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class Listens
{
    public function __construct(
        public string $event,
        public int $priority = 0,
    ) {}
}

// Example Listener
#[Listens(event: UserCreated::class, priority: 10)]
#[Listens(event: UserUpdated::class, priority: 5)]
class SendWelcomeEmail
{
    public function handle(UserCreated|UserUpdated $event): void
    {
        Mail::to($event->user)->send(new WelcomeEmail($event->user));
    }
}

// In EventServiceProvider
public function boot(): void
{
    $listeners = Discovery::attribute(Listens::class)
        ->directories('app/Listeners')
        ->cached('event-listeners')
        ->get();

    foreach ($listeners as $class => $metadata) {
        // Note: Listens is repeatable, so we get an array of attributes
        $attributes = is_array($metadata['attribute'])
            ? $metadata['attribute']
            : [$metadata['attribute']];

        foreach ($attributes as $attribute) {
            Event::listen($attribute->event, $class);
        }
    }
}

Example 6: Validation Rules Discovery

Discover custom validation rules.

// Validation Rule Attribute
#[Attribute(Attribute::TARGET_CLASS)]
class ValidationRule
{
    public function __construct(
        public string $name,
        public ?string $message = null,
    ) {}
}

// Example Rule
#[ValidationRule(name: 'phone', message: 'The :attribute must be a valid phone number.')]
class PhoneRule implements ValidationRule
{
    public function passes($attribute, $value): bool
    {
        return preg_match('/^[0-9]{10}$/', $value);
    }

    public function message(): string
    {
        return 'The :attribute must be a valid phone number.';
    }
}

// In ValidationServiceProvider
public function boot(): void
{
    $rules = Discovery::attribute(ValidationRule::class)
        ->implementing(ValidationRule::class)
        ->instantiable()
        ->cached('validation-rules')
        ->get();

    foreach ($rules as $class => $metadata) {
        $ruleName = $metadata['attribute']->name;

        Validator::extend($ruleName, function ($attribute, $value, $parameters, $validator) use ($class) {
            $rule = app($class);
            return $rule->passes($attribute, $value);
        });

        if ($message = $metadata['attribute']->message) {
            Validator::replacer($ruleName, function ($message, $attribute, $rule, $parameters) {
                return str_replace(':attribute', $attribute, $message);
            });
        }
    }
}

Example 7: API Resource Discovery

Automatically register API resources.

// API Resource Attribute
#[Attribute(Attribute::TARGET_CLASS)]
class ApiResource
{
    public function __construct(
        public string $model,
        public ?string $collection = null,
    ) {}
}

// Example Resource
#[ApiResource(model: User::class, collection: UserCollection::class)]
class UserResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'created_at' => $this->created_at,
        ];
    }
}

// Resource Registry
class ResourceRegistry
{
    protected array $resources = [];

    public function discover(): void
    {
        $resources = Discovery::attribute(ApiResource::class)
            ->extending(JsonResource::class)
            ->cached('api-resources')
            ->get();

        foreach ($resources as $class => $metadata) {
            $modelClass = $metadata['attribute']->model;
            $this->resources[$modelClass] = $class;
        }
    }

    public function getResourceForModel(string $modelClass): ?string
    {
        return $this->resources[$modelClass] ?? null;
    }
}

Example 8: Middleware Discovery

Discover and register middleware automatically.

// Middleware Attribute
#[Attribute(Attribute::TARGET_CLASS)]
class Middleware
{
    public function __construct(
        public string $alias,
        public array $groups = [],
        public int $priority = 0,
    ) {}
}

// Example Middleware
#[Middleware(alias: 'check.subscription', groups: ['web', 'api'], priority: 100)]
class CheckSubscription
{
    public function handle(Request $request, Closure $next)
    {
        if (!auth()->user()?->hasActiveSubscription()) {
            return response()->json(['error' => 'Subscription required'], 403);
        }

        return $next($request);
    }
}

// In Kernel or ServiceProvider
public function registerMiddleware(): void
{
    $middlewares = Discovery::attribute(Middleware::class)
        ->directories('app/Http/Middleware')
        ->cached('middlewares')
        ->get();

    foreach ($middlewares as $class => $metadata) {
        $attribute = $metadata['attribute'];

        // Register alias
        $this->app['router']->aliasMiddleware($attribute->alias, $class);

        // Register in groups
        foreach ($attribute->groups as $group) {
            $this->app['router']->pushMiddlewareToGroup($group, $class);
        }
    }
}

⬆ Back to Top

📚 API Reference

DiscoveryManager

Main service class for discovery operations.

Methods

attribute(string $attributeClass): DiscoveryBuilder

Discover classes by attribute.

Discovery::attribute(AsCard::class)->get();
directories(string|array $directories): DiscoveryBuilder

Discover classes in directories.

Discovery::directories('packages/*/src/Settings')->get();
implementing(string $interface): DiscoveryBuilder

Discover classes implementing an interface.

Discovery::implementing(HealthCheckInterface::class)->get();
extending(string $parentClass): DiscoveryBuilder

Discover classes extending a parent class.

Discovery::extending(Command::class)->get();
methods(string $attributeClass): DiscoveryBuilder

Discover methods with an attribute.

Discovery::methods(Route::class)->get();
properties(string $attributeClass): DiscoveryBuilder

Discover properties with an attribute.

Discovery::properties(Validate::class)->get();
clearCache(?string $key = null): void

Clear discovery caches.

Discovery::clearCache('settings');
finder(string|array $directories): Finder

Get Symfony Finder for advanced discovery.

$finder = Discovery::finder('packages/*/src')
    ->name('*.php')
    ->date('since yesterday');

DiscoveryBuilder

Fluent builder for configuring discovery.

Methods

where(string $property, mixed $operator, mixed $value = null): self

Filter by attribute property.

->where('enabled', true)
->where('priority', '>', 5)
filter(callable $callback): self

Add custom filter.

->filter(fn($class, $meta) => $meta['attribute']->priority > 5)
instantiable(): self

Filter to instantiable classes only.

->instantiable()
extends(string $parentClass): self

Validate parent class.

->extends(Command::class)
implements(string $interface): self

Validate interface implementation.

->implements(ServiceInterface::class)
cached(string $key): self

Enable caching.

->cached('settings')
get(): array

Execute discovery and return results.

->get()

⬆ Back to Top

⚡ Performance

Benchmarks

Performance tests on a monorepo with 500 classes:

OperationWithout CacheWith CacheImprovement
Attribute Discovery50ms2ms25x faster
Directory Scan (100 files)120ms3ms40x faster
Interface Discovery80ms2ms40x faster
Method Discovery60ms2ms30x faster

Optimization Tips

  1. Use Attribute Discovery - Fastest method, leverages composer cache
  2. Enable Caching - Essential for production
  3. Scope Directory Scans - Use specific paths instead of scanning everything
  4. Combine Filters - Apply filters early to reduce processing
// ✅ Optimized
Discovery::attribute(AsCard::class)
    ->where('enabled', true)  // Filter early
    ->instantiable()          // Validate early
    ->cached('cards')         // Cache results
    ->get();

// ❌ Not optimized
Discovery::directories('.')   // Scans everything
    ->get();                  // No caching

⬆ Back to Top

🧪 Testing

Running Tests

# Run all tests
composer test

# Run with coverage
composer test-coverage

# Run static analysis
composer analyse

Writing Tests

use Pixielity\Discovery\Tests\TestCase;

class MyDiscoveryTest extends TestCase
{
    public function test_discovers_classes_with_attribute(): void
    {
        $classes = Discovery::attribute(MyAttribute::class)->get();

        $this->assertNotEmpty($classes);
        $this->assertArrayHasKey(MyClass::class, $classes);
    }
}

⬆ Back to Top

🔒 Security

Security Considerations

  1. File System Access - Discovery scans the file system. Ensure paths are properly configured.
  2. User Input - Never use user input directly in directory paths.
  3. Class Loading - Always validate classes before instantiation.
  4. Cache Security - Protect cache directory from web access.

Best Practices

// ✅ GOOD - Validate before use
$classes = Discovery::attribute(MyAttribute::class)
    ->instantiable()
    ->implementing(MyInterface::class)
    ->get();

// ❌ BAD - No validation
$classes = Discovery::directories($_GET['path'])->get();

Reporting Vulnerabilities

Please report security vulnerabilities to: security@pixielity.com

See SECURITY.md for our security policy.

⬆ Back to Top

🤝 Contributing

We welcome contributions! Please see CONTRIBUTING.md for details.

Development Setup

# Clone repository
git clone https://github.com/pixielity-co/laravel-discovery.git
cd discovery

# Install dependencies
composer install

# Run tests
composer test

# Run static analysis
composer analyse

Coding Standards

  • Follow PSR-12
  • Use strict types
  • Add PHPDoc blocks
  • Write tests for new features
  • Update documentation

⬆ Back to Top

💖 Credits

This package stands on the shoulders of giants. Special thanks to:

Core Dependencies

Inspiration

  • Laravel's Package Auto-Discovery - Inspired the automatic registration pattern
  • Spatie's Laravel Packages - Set the standard for Laravel package quality
  • PHP 8 Attributes - Made modern, declarative PHP possible

Community

  • All our contributors
  • The Laravel community for feedback and support
  • Everyone who reported issues and suggested improvements

Tools & Services

⬆ Back to Top

📄 License

The MIT License (MIT). Please see LICENSE for more information.

MIT License

Copyright (c) 2026 Pixielity Team

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

⬆ Back to Top

🆘 Support & Resources

Documentation

Community

Stay Updated

**[⬆ Back to Top](#top)** Made with ❤️ by the [Pixielity Team](https://github.com/pixielity-co)