Mod is pre-1.0. Minor releases may change the API until 1.0.
Skip to content

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:

bash
php artisan event:list --event=DocumentUploaded
text
  App\Modules\Knowledge\Events\DocumentUploaded ..
  ⇂ App\Modules\Knowledge\Listeners\GenerateEmbeddings@handle

A 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 typeRegistered as
providera service provider
commandan Artisan command
listeneran event listener, for the events its handle() method accepts
subscriberan event subscriber, through Event::subscribe()
migrationa 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:

bash
php artisan migrate
text
   INFO  Running migrations.

  2026_10_08_120000_create_documents_table .......... 1.91ms DONE

Laravel'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:

php
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\DocumentPolicy

mod: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:

app/Providers/AppServiceProvider.phpboot()
php
use Tey\Mod\Facades\Mod;

Mod::layout('modules')->generates('subscriber', in: 'Modules/{module}/Subscribers', suffix: 'Subscriber');
bash
php artisan mod:subscriber Knowledge:Document
# -> app/Modules/Knowledge/Subscribers/DocumentSubscriber.php

A 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:

app/Providers/AppServiceProvider.phpboot()
php
use Tey\Mod\Facades\Mod;

Mod::layout('modules')->generates('handler', in: 'Modules/{module}/Handlers');
config/mod.php
php
'discovery' => [
    'file_types' => ['handler' => 'listener'],
],

A handler generated with mod:handler Knowledge:IndexUploadedDocument, whose handle() method accepts DocumentUploaded, is then registered as a listener:

bash
php artisan event:list --event=DocumentUploaded
text
  App\Modules\Knowledge\Events\DocumentUploaded ..
  ⇂ App\Modules\Knowledge\Handlers\IndexUploadedDocument@handle

A 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:

config/mod.php
php
'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:

app/Providers/AppServiceProvider.phpboot()
php
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:

bash
php artisan mod:cache   # also run by php artisan optimize
php artisan mod:clear   # also run by php artisan optimize:clear
text
   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:

ReasonWhat to do
placed by no file typenothing: 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 subscribercheck it: for example, a listener whose handle() has no typed event parameter
placed by more than one file typegive one file type a priority:
placed by a callback, so its file type cannot be toldnothing, 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:

bash
php artisan mod:list -v

Templates 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:

config/mod.php
php
'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.

Released under the MIT License. Created by Jasper Tey.