drivkraft / interlock
Lightweight architecture manager for structuring Laravel apps with modules.
Requires
- php: ^8.4|^8.5
- laravel/framework: ^13.0
- laravel/prompts: ^0.3.25
- symfony/finder: ^7.4||^8.0
- wikimedia/composer-merge-plugin: ^2.1
Requires (Dev)
- larastan/larastan: ^3.9
- laravel/pao: ^1.0
- laravel/pint: ^1.29
- livewire/livewire: ^4.1
- lorisleiva/laravel-actions: ^2.13
- orchestra/testbench: ^11.0
- pestphp/pest: ^5.0
- pestphp/pest-plugin-laravel: ^5.0
- pestphp/pest-plugin-type-coverage: ^5.0
- phpstan/extension-installer: ^1.4
- spatie/laravel-data: ^4.23
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Laravel Interlock
Lightweight architecture manager for structuring Laravel apps with modules.
A module is a folder in modules/ with its own composer.json, classes, routes, views, translations and migrations. The package discovers and registers everything a module contains, and adds module:* generators that write straight into a module.
Requirements: PHP 8.5+, Laravel 13.
Installation
composer require drivkraft/interlock
Module autoloading uses wikimedia/composer-merge-plugin, which the package requires. Composer only reads the plugin's settings from your application, so add them to its composer.json and run composer dump-autoload:
"extra": { "merge-plugin": { "include": ["modules/*/composer.json"] } }, "config": { "allow-plugins": { "wikimedia/composer-merge-plugin": true } }
Without allow-plugins, non-interactive installs (CI) fail.
Configuration
php artisan vendor:publish --tag="interlock-config"
// config/interlock.php return [ 'path' => base_path('modules'), // keep the merge plugin's include in sync 'source' => 'src', // PHP folder inside each module 'namespace' => 'Module', // root namespace for new modules 'tests_base' => 'Tests\TestCase', // exposed to custom stubs 'stubs' => null, // custom make:module stubs, null = package stubs // Folder per generator, relative to `source` 'directories' => [ 'model' => 'Models', 'enum' => 'Enums', 'interface' => 'Contracts', 'trait' => 'Concerns', 'scope' => 'Models/Scopes', 'data' => 'Data', 'action' => 'Actions', ], // Group attributes per route file name; unlisted files load ungrouped 'routes' => [ 'web' => ['middleware' => ['web']], 'api' => ['middleware' => ['api'], 'prefix' => 'api'], ], // Used only when Livewire is installed 'livewire' => [ 'components' => 'resources/views/components', 'namespaces' => [ 'pages' => 'resources/views/pages', 'layouts' => 'resources/views/layouts', ], ], // Each discovery type can be turned off 'discover' => [ 'providers' => true, 'routes' => true, 'views' => true, 'translations' => true, 'migrations' => true, 'components' => true, 'livewire' => true, 'commands' => true, 'events' => true, 'factories' => true, ], 'cache' => base_path('bootstrap/cache/interlock.php'), ];
Quick Start
php artisan make:module billing php artisan module:model Invoice --module=billing -mf php artisan module:controller InvoiceController --module=billing php artisan migrate
modules/billing/
├── composer.json Module\Billing\ => src/
├── src/
│ ├── Http/Controllers/InvoiceController.php
│ ├── Models/Invoice.php
│ └── Providers/BillingServiceProvider.php
├── routes/web.php loaded in the web group
├── resources/views/ view('billing::...')
├── lang/ __('billing::...')
├── database/{migrations,factories,seeders}/
└── tests/
No registration needed: the provider, routes, views, translations and migrations are discovered. make:module runs composer dump-autoload (skip with --no-dump) and asks which parts to create; without interaction it creates all except --api.
How-to Guides
Generate classes inside a module
Every make:* command has a module:* counterpart with the same options plus --module (prompted when omitted, required with --no-interaction). Module-aware differences:
module:commandprefixes the signature with the module name (billing:send-reminders).module:componentwrites the view to the module and rendersbilling::components.invoice-card.module:listener --event=InvoicePaidresolvesModule\Billing\Events\InvoicePaid.module:modelasks for related files unless you pass an option.module:providerdoes not touchbootstrap/providers.php(module providers are discovered) and warns when the provider won't be discovered: only top-levelProviders/*ServiceProviderclasses are.
make:* is unchanged and still writes to app/.
Actions and data objects
php artisan module:action SendInvoice --module=billing # src/Actions php artisan module:data InvoiceData --module=billing # src/Data
module:action adds AsAction when lorisleiva/laravel-actions is installed; module:data extends Spatie\LaravelData\Data when spatie/laravel-data is installed, otherwise a final readonly class. Names are used as given; no suffix is added.
Change generator folders
Set directories.{generator} to a folder relative to source, for example 'interface' => 'Interfaces' or 'class' => 'Support'. Unlisted generators use Laravel's defaults. Nested names (module:enum Billing/Status) go below the configured folder.
Add route groups
'routes' => [ 'web' => ['middleware' => ['web']], 'api' => ['middleware' => ['api'], 'prefix' => 'api'], 'tenant' => ['middleware' => ['web', 'auth', 'tenant'], 'prefix' => 'app'], ],
Every module's routes/tenant.php now loads in that group. Route files not in the map load without a group.
Translations, components and Livewire
lang/en/messages.php→__('billing::messages.paid'). JSON translations are merged unprefixed.src/View/Componentsandresources/views/components→<x-billing::invoice-card />.- With Livewire installed, each module is a namespace:
<livewire:billing::invoice-list />. Eachlivewire.namespacesentry adds a sub-namespace (billing.pages::invoices), whose folder is also a view and anonymous component namespace (<x-billing.layouts::app>). Generate withmodule:livewire {name} --module=billing [--kind=pages]; allmake:livewireoptions work.
Seed a module
php artisan module:seed --module=billing # Module\Billing\Database\Seeders\DatabaseSeeder
php artisan module:seed --module=billing --class=InvoiceSeeder
make:module creates the module's DatabaseSeeder with the seeders part.
Custom module stubs
'stubs' => [ 'composer.json' => base_path('stubs/modules/composer.json.stub'), 'StubSourceDirectory/Providers/StubClassNamePrefixServiceProvider.php' => base_path('stubs/modules/provider.stub'), 'README.md' => base_path('stubs/modules/readme.stub'), ],
With custom stubs, make:module writes the whole map and skips the parts prompt. Paths and contents support the placeholders. Include a composer.json with a PSR-4 entry for source.
Cache discovery in production
php artisan optimize runs module:cache, and optimize:clear runs module:clear. While cached, new modules and discovered classes are ignored until you run module:clear (make:module does this for you).
Find modules from code
use Drivkraft\Interlock\Facades\Interlock; Interlock::modules(); // Collection<string, Module> Interlock::module('billing')?->namespace; // 'Module\Billing\' Interlock::moduleForClass(Invoice::class)?->name; // 'billing' Interlock::moduleForPath(__FILE__);
module() matches the folder name or its kebab-case form.
Explanation
Discovery. Any folder in path with a composer.json is a module, named after the folder. Its namespace comes from the PSR-4 entry pointing at source, falling back to {namespace}\{Name}\. On boot the package builds a manifest of everything each module contains; providers are registered during Laravel's register phase, the rest at boot. module:cache snapshots the manifest, like Laravel's config and route caches.
Autoloading. Modules are plain Composer autoload entries merged by the merge plugin rather than registered at runtime. Module classes therefore work with optimised and authoritative classmaps, exist before Laravel boots, and are visible to IDEs and static analysis. The cost is a dump-autoload per new module.
Generators. Each module:* command extends its make:* counterpart and only changes the target namespace and path, so all options, and future ones, keep working. References to your application in Laravel's stubs stay in App\, so a module controller still extends App\Http\Controllers\Controller.
Optional integrations. Livewire, laravel-actions and laravel-data are detected at runtime and never required. They only change what the generators produce.
Reference
Commands
| Command | Description |
|---|---|
make:module {name} |
Create a module. --web, --api, --views, --lang, --migrations, --factories, --seeders, --tests, --all, --no-dump. Existing files are skipped. |
module:list |
List modules with namespace and path. |
module:cache / module:clear |
Write or delete the discovery cache. Hooked into optimize / optimize:clear. |
module:seed |
db:seed for a module. |
module:action, module:data |
See Actions and data objects. |
Generators mirroring make:*: cast, channel, class, command, component, controller, enum, event, exception, factory, interface, job, job-middleware, listener, livewire, mail, middleware, migration, model, notification, observer, policy, provider, request, resource, rule, scope, seeder, test, trait.
What gets discovered
| Module path | Registered as |
|---|---|
src/Providers/*ServiceProvider.php |
Service providers |
routes/{name}.php |
Routes in the routes.{name} group (web; api with /api prefix), or ungrouped |
resources/views |
billing:: view namespace |
lang |
billing:: translations, plus JSON translations |
src/View/Components, resources/views/components |
<x-billing::...> components |
src/Livewire, resources/views/components |
billing:: Livewire namespace (Livewire only) |
src/Console/Commands |
Artisan commands |
src/Listeners |
Event listeners |
database/migrations |
Migration path |
database/factories |
Factories for src/Models/* |
Stub placeholders
| Placeholder | make:module billing |
|---|---|
StubModuleName / StubModuleNameSingular / StubModuleNamePlural |
billing / billing / billings |
StubClassNamePrefix |
Billing |
StubModuleNamespace |
Module |
StubSourceDirectory |
src |
StubBasePath |
Absolute module path |
StubMigrationPrefix |
2026_10_23_120000 |
StubTestCaseBase / StubFullyQualifiedTestCaseBase |
TestCase / Tests\TestCase |
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Thank you for considering contributing to Interlock! Please review our contributing guide to get started.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
Interlock is open-sourced software licensed under the MIT license.