Auto-Discovery
Mod registers the providers, Artisan commands and event listeners your layout places, adds its migration folders to Laravel's migrator, and finds the factory and policy of each model. A class generated into a module works without a line of registration.
Discovering Listeners
The GenerateEmbeddings listener from Generating Files, in the Knowledge module, is registered as soon as it exists:
php artisan event:list --event=DocumentUploaded App\Modules\Knowledge\Events\DocumentUploaded ..
⇂ App\Modules\Knowledge\Listeners\GenerateEmbeddings@handleA listener is registered for the events its handle() method accepts. It is never registered twice: whatever Laravel's own event discovery covers (app/Listeners, or the paths given to withEvents()), its event cache, or a manual Event::listen() already holds is left alone.
What Is Discovered
| File type | Registered as |
|---|---|
provider | a service provider |
command | an Artisan command |
listener | an event listener, for the events its handle() method accepts |
subscriber | an event subscriber, through Event::subscribe() |
migration | a migration folder, added to the migrator |
Discovery looks in each file type's own folder, such as app/Modules/Knowledge/Listeners, and runs after every provider has booted. Only classes that really are providers, commands, listeners or subscribers are registered; anything else in those folders is skipped.
Route files, views and translations aren't discovered. A module loads its routes from a provider of its own, which is discovered; Adding Routes shows how.
Running Module Migrations
The folders a migration is written to, such as app/Modules/Knowledge/Database/Migrations, are added to Laravel's migrator. migrate, migrate:rollback and migrate:status include them:
php artisan migrate INFO Running migrations.
2026_10_08_120000_create_documents_table .......... 1.91ms DONELaravel's own database/migrations is left to Laravel.
Finding Factories and Policies
A model the layout places finds its factory and policy through the layout. Discovery doesn't need a newFactory() method on the model or a Gate::policy() call:
use App\Modules\Knowledge\Models\Document;
use Illuminate\Support\Facades\Gate;
Document::factory(); // App\Modules\Knowledge\Database\Factories\DocumentFactory
Gate::getPolicyFor(Document::class); // App\Modules\Knowledge\Policies\DocumentPolicymod:model -f still writes newFactory(), so the model keeps working without mod.
A policy your app registers with Gate::policy() is kept. A factory resolver your app sets after mod (Factory::guessFactoryNamesUsing()) replaces mod's, as it would replace any earlier one. Factory and policy lookup are part of discovery, so 'discovery.enabled' => false turns them off too.
Discovering Event Subscribers
A subscriber in a Listeners folder is treated the way Laravel's own event discovery treats it: its typed handle*() methods are registered as listeners, and subscribe() isn't called.
Subscribers are discovered for a file type named subscriber. The built-in layouts don't have one, so add it to yours:
use Tey\Mod\Facades\Mod;
Mod::layout('modules')->generates('subscriber', in: 'Modules/{module}/Subscribers', suffix: 'Subscriber');php artisan mod:subscriber Knowledge:Document
# -> app/Modules/Knowledge/Subscribers/DocumentSubscriber.phpA class in that folder with a public subscribe() method taking one parameter is registered through Event::subscribe().
Discovering Other File Types
discovery.file_types maps a file type id to what it is discovered as. The built-in layouts call their Artisan commands command, so those are discovered already. To discover a file type of your own, declare it, then map its id:
use Tey\Mod\Facades\Mod;
Mod::layout('modules')->generates('handler', in: 'Modules/{module}/Handlers');'discovery' => [
'file_types' => ['handler' => 'listener'],
],A handler generated with mod:handler Knowledge:IndexUploadedDocument, whose handle() method accepts DocumentUploaded, is then registered as a listener:
php artisan event:list --event=DocumentUploaded App\Modules\Knowledge\Events\DocumentUploaded ..
⇂ App\Modules\Knowledge\Handlers\IndexUploadedDocument@handleA key that isn't a file type of the active layout stops the app with an error listing the layout's file types. The value is provider, command, listener, subscriber, directory (for file types like migrations), or false to stop discovering one. For example, to manage migration folders yourself:
'discovery' => [
'file_types' => ['migration' => false],
],Discovering Anywhere in a Domain
To discover a file type in every PHP file below its group folder, not only its own folder, pass discover: 'anywhere'. discoverExcept: skips folders below the group folder, such as src/Domain/Knowledge/Tests:
use Tey\Mod\Facades\Mod;
Mod::layout('ddd')->generates('listener', in: '{domain+}/Listeners', discover: 'anywhere', discoverExcept: ['Tests']);Caching Discovery in Production
php artisan optimize caches discovery, and php artisan optimize:clear clears it. Both run mod's own commands:
php artisan mod:cache # also run by php artisan optimize
php artisan mod:clear # also run by php artisan optimize:clear INFO Discovery cached in [bootstrap/cache/mod-discovery.php]: 1 providers, 0 commands, 1 listeners, 0 subscribers, 2 directories, 5 rejected.
Rejected files were found but not registered: 5 placed by no file type (helpers and plain classes; nothing to do). Run with -v to list them.mod:cache prints what it registered, and a second line for any files it found but didn't register, grouped by reason:
| Reason | What to do |
|---|---|
| placed by no file type | nothing: files no file type of the layout owns, such as app/Models/User.php, the base classes in app/Support and a module's routes/web.php |
| in a discovered folder but not a provider, command, listener or subscriber | check it: for example, a listener whose handle() has no typed event parameter |
| placed by more than one file type | give one file type a priority: |
| placed by a callback, so its file type cannot be told | nothing, unless it should be discovered |
php artisan mod:cache -v lists each rejected file with its reason.
With a cache present, mod registers from the cache without scanning. Like Laravel's own caches, it doesn't pick up new classes: after adding a provider, command or listener while the cache exists, run php artisan optimize:clear.
When the layout, templates or discovery settings change after the cache was written, the cache is ignored: mod scans instead and logs a warning naming both commands. Set 'discovery.on_stale_cache' => 'fail' to stop the app booting instead.
See What's Discovered
Inspect the active layout and its discovered classes:
php artisan mod:list -vTemplates are scanned when console commands are registered; cached inventory can be replayed without rescanning. The cache fingerprints templates as well as the layout and discovery settings. After editing templates with a cache present, clear or rebuild it.
Turning Discovery Off
To keep the rest of discovery and stop finding factories or policies through the layout, turn that part off:
'discovery' => [
'factories' => false,
'policies' => false,
],'enabled' => false turns all of discovery off: providers, commands, listeners, subscribers, migration folders, factories and policies.
Configuration lists every discovery key.