Modules
New functionality belongs in a module, not the root app/. A module is an in-repo Composer package
under app-modules/, wired through a path repository in the root composer.json
(app-modules/*, symlinked), and auto-loaded by
internachi/modular.
Anatomy
Section titled “Anatomy”app-modules/automations/ composer.json package name, deps, autoload, service provider ARCHITECTURE.md working notes, kept current config/ optional: <module>.php, merged in the provider database/ factories/ migrations/ seeders/ resources/views/ routes/ optional src/ AutomationsFilamentPlugin.php Console/Commands/ Filament/Resources/ Filament/Pages/ Filament/Clusters/ Filament/Widgets/ Jobs/ Listeners/ Models/ Policies/ Providers/AutomationsServiceProvider.php tests/Feature/ tests/Unit/src/ maps to WebMoves\<Module>\: app-modules/catalog/src/Models/Product.php is
WebMoves\Catalog\Models\Product. Tests, factories and seeders have their own namespaces
(WebMoves\Automations\Tests\, ...\Database\Factories\, ...\Database\Seeders\), declared in the
module’s composer.json.
A module’s composer.json must:
- declare
"version": "1.0". Without it Composer takes the version from the git branch (dev-<branch>) and lock-file resolution breaks on branch switches - list its service provider under
extra.laravel.providers, so Laravel discovers it.contactsandsimple-filament-layouthave none, since they only provide classes requireeverything it uses, other modules and packages alike.tests/Unit/ModuleDependencyTest.phpfails otherwise, and also on a dependency cycle
The current graph is on the architecture
page. When you add a package for a
module, update only what you need: ddev composer update web-moves/<module> <package>, without
-W, which would upgrade the whole stack.
What modular wires up
Section titled “What modular wires up”These come from vendor/internachi/modular/src/Plugins/. Don’t also register them by hand: a listener
registered twice fires twice.
| You add | Found at | What happens |
|---|---|---|
| Views | resources/views |
View namespace named after the module folder: view('assistant::topbar-button') |
| Blade components | src/View/Components |
Components prefixed with the module name |
| Migrations | database/migrations |
Run by migrate |
| Factories | database/factories |
Found for the module’s models |
| Commands | src/Console/Commands |
Registered with Artisan |
| Listeners | src/Listeners |
Event discovery, on by default here (config/app-modules.php leaves should_discover_events null) |
| Policies | src/Policies/<Model>Policy.php for src/Models/<Model>.php |
Registered on the Gate by name |
| Routes | routes/*.php |
Required as they are, with no middleware group applied |
| Translations | resources/lang |
Namespaced by the module name |
Things that don’t fit the convention need wiring in the service provider:
-
Config is not merged. Keep
mergeConfigFrominregister():$this->mergeConfigFrom(__DIR__.'/../../config/catalog.php', 'catalog'); -
Commands elsewhere than
src/Console/Commands, as in content-management, whose commands live by feature (src/Sites/Console/Commands/) and are listed in$this->commands([...]). -
Policies for models that aren’t in this module’s
src/Models, such as a vendor model:Gate::policy(Role::class, RolePolicy::class). -
The schedule. A module adds its entries itself:
$this->callAfterResolving(Schedule::class, function (Schedule $schedule): void {$schedule->command('automations:run-due')->everyMinute()->withoutOverlapping();});
Create a module’s files with the framework’s generators and --module, for example
ddev artisan make:model Foo --module=catalog.
Filament plugins
Section titled “Filament plugins”Most modules have a plugin class directly under src/, extending
WebMoves\Utils\Filament\ModuleFilamentPlugin, and it is usually empty:
class AutomationsFilamentPlugin extends ModuleFilamentPlugin {}The base class discovers src/Filament/Resources, Clusters, Pages and Widgets by folder, so a
new resource in the right folder appears without being listed. A subclass can turn one off
($discoverWidgets = false) or add to register(): panel routes, as in SalesFilamentPlugin, or
render hooks, as in AssistantFilamentPlugin.
The plugin itself is registered once, in AdminPanelProvider::panel():
->plugins([ CustomFieldsFilamentPlugin::make(), ContentManagementFilamentPlugin::make(), // ...The customer panel is different: CustomerPanelProvider lists its pages, resources and widgets
explicitly, and is itself a discovered provider of the customer-panel module.
A new resource needs a policy
Section titled “A new resource needs a policy”Filament only checks a policy if there is one, so a resource without a policy is open to every staff role. Every resource needs one in its module:
namespace WebMoves\Automations\Policies;
class AutomationRulePolicy extends CapabilityPolicy{ protected string $capability = 'automations.manage';}A custom page that isn’t a resource is open to everyone on the panel unless it overrides
canAccess(). See Authorization.
Adding a module
Section titled “Adding a module”ddev artisan make:module <name>scaffolds the folder, including a service provider and"version": "1.0", under theWebMovesnamespace thatconfig/app-modules.phpsets.- Make sure
web-moves/<name>is in the rootcomposer.json’srequire. ddev composer update web-moves/<name>.- If it has admin screens, add a plugin class and register it in
AdminPanelProvider. - Add an
ARCHITECTURE.md. Add a line to the dependency table.
ddev artisan modules:sync only updates phpunit.xml (the Modules test suite glob) and PhpStorm
config files. It does not touch Composer state or Laravel’s package cache.
Removing a module
Section titled “Removing a module”- Remove it from the root
composer.json’srequire. - Delete the module’s directory.
ddev composer update web-moves/<name> --no-scripts, which drops it fromcomposer.lockandvendor/composer/installed.json.ddev composer dump-autoload.
A bare dump-autoload alone won’t refresh installed.json. That leaves
bootstrap/cache/packages.php pointing at the deleted provider, and the next artisan boot fails with
“Class not found”.