Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Interlock Laravel Package

drivkraft/interlock

drivkraft/interlock is a Laravel package that adds lightweight interlocking primitives to help coordinate concurrent work, prevent overlapping jobs, and guard shared resources against race conditions with simple, reliable locks.

View on GitHub
Deep Wiki
Context7

Getting Started

Check the floor before anything else. v1.0.0 requires PHP 8.5+ and Laravel 13. Both are bleeding-edge; if your app or any dependency pins an older constraint, this package is not adoptable yet. Run composer why-not php 8.5 / inspect your composer.json platform config first.

  1. Install and publish the config:
    composer require <vendor>/interlock
    php artisan vendor:publish   # publish config/interlock.php
    
  2. Wire module autoloading into the root composer.json via the merge plugin (this is the one step you cannot skip):
    "extra": {
        "merge-plugin": {
            "include": ["modules/*/composer.json"]
        }
    }
    
    Then composer dump-autoload.
  3. Scaffold and inspect:
    php artisan module:make billing --all
    php artisan module:list
    

Where to look first: config/interlock.php. Almost every behaviour is driven from three keys — interlock.routes, interlock.discover, and interlock.directories. Read those before writing code; they explain why files "just work" (or silently don't).

First real use case: don't start greenfield. Pick one cohesive slice of an existing app — a billing domain is the canonical example — move its models, migrations, routes and service provider into modules/billing/, add a composer.json with a PSR-4 entry, then composer dump-autoload && php artisan module:list. If the module shows up and the routes still resolve, your wiring is correct. Confirm with Interlock::modules() in Tinker.

Implementation Patterns

Module anatomy. A module is just a folder in modules/ containing a composer.json. Folder name = module name; namespace comes from the PSR-4 mapping:

modules/billing/
├── composer.json          # {"autoload":{"psr-4":{"Modules\\Billing\\":"src/"}}}
├── src/
│   ├── BillingServiceProvider.php
│   ├── Models/Invoice.php
│   └── Livewire/
├── routes/web.php
├── routes/api.php
├── resources/views/
├── lang/
├── database/migrations/
├── database/factories/
└── tests/

Never register what discovery already registers. Providers, routes, views, translations, Blade components, commands, listeners, migrations and factories inside a module are picked up automatically. If you also list the provider in bootstrap/providers.php, you get double registration (duplicated event listeners are the usual symptom).

Route groups are keyed by filename. routes/web.php gets web middleware, routes/api.php gets api. Adding an admin surface is a config edit, not a provider edit:

// config/interlock.php
'routes' => [
    'web'   => ['middleware' => ['web']],
    'api'   => ['middleware' => ['api'], 'prefix' => 'api'],
    'admin' => ['middleware' => ['web', 'auth'], 'prefix' => 'admin'],
],

Use Interlock when code must stay module-agnostic. This is the escape hatch that keeps shared/base code from hardcoding namespaces:

use Interlock;

Interlock::modules();                    // all discovered modules
Interlock::module('billing');            // one module
Interlock::moduleForClass(Invoice::class);   // resolve owner of a class
Interlock::moduleForPath(__FILE__);          // resolve owner of a file

Typical pattern: a base Controller or policy that needs to know which module a model belongs to in order to pick a view namespace, a config file, or a translation prefix.

Stay inside the generators. With 30 module:* counterparts of the framework generators, there is rarely a reason to hand-create a file in the wrong folder:

php artisan module:make billing --web --api --views --lang --migrations --factories --seeders --tests
php artisan module:model Invoice --module=billing
php artisan module:controller InvoiceController --module=billing --resource
php artisan module:livewire InvoiceForm --module=billing

They accept the same options as their make:* equivalents plus --module, and honour interlock.directories for output paths — so one config change redirects every generator consistently.

Optional integrations are detected, not declared. module:action switches to lorisleiva/laravel-actions and module:data to spatie/laravel-data if those packages are installed. In a monorepo where some apps have them and some don't, keep a shared stub set so generated classes look the same either way.

Livewire sub-namespaces. Each module gets a Livewire namespace, and you can nest configurable ones such as billing.pages:: — useful for splitting page-level components from shared widgets without inventing folder conventions.

Deployment. module:cache and module:clear are hooked into optimize / optimize:clear, so the correct deploy sequence is just:

composer install --no-dev --optimize-autoloader
php artisan optimize        # config:cache + route:cache + module:cache

Do not hand-roll module:cache after optimize in a deploy script; it's already inside it.

Seeding and tests per module. module:seed billing seeds a single module in isolation, which makes per-module test fixtures cheap. Add each module's tests/ directory to phpunit.xml test suites if you want module-scoped test runs.

Gotchas and Tips

The merge plugin is a hard dependency, and its failures are quiet. If modules/*/composer.json isn't in the root's merge-plugin.include, modules are discovered (folders are scanned) but their classes don't autoload — you get "class not found" from a module that module:list happily reports. When something is missing, check this before anything else. Also re-run composer dump-autoload after creating a module; adding classes to an existing module with a working PSR-4 map is fine without it.

Optimised and authoritative classmaps work — but only if you regenerate. -o -a installs are supported, which means a new module added after composer install -a needs a fresh dump-autoload and module:cache. In CI, always dump autoload before optimize.

Folder name and namespace must agree. The module name is the folder name; the namespace is the PSR-4 prefix. Rename the folder without editing composer.json and you get a module whose name and namespace disagree — Interlock::moduleForClass() starts returning null and generated files land in the old namespace. moduleForPath() is the fastest way to diagnose this: it tells you which module the framework thinks a file belongs to.

Discovery toggles fail silently by design. Turning off a key under interlock.discover doesn't warn — the routes/components/migrations simply don't exist. Symptom triage:

  • Routes 404 → php artisan route:list, then check discover and interlock.routes filenames.
  • php artisan doesn't show a module command → commands discovery off, or the class isn't in the module's autoloaded path.
  • Migrations missing → php artisan migrate:status; if absent, discovery is off or the file isn't under the configured migrations directory.
  • Listeners not firing → check you haven't also registered the event in EventServiceProvider.

A route file with an unlisted name is dead weight. routes/admin.php gets no middleware group at all unless admin exists in interlock.routes. Worse, an accidentally matched name inherits middleware you didn't intend. Keep route filenames aligned one-to-one with config keys.

Namespace collisions are the real cost of auto-discovery. Blade components and translations are registered globally. Two modules shipping components/button.blade.php or the same JSON translation key will silently shadow each other (last-registered wins, and order follows discovery). Convention: prefix component names and translation keys per module, and check the resolved view with php artisan view:list-style debugging when a component renders the wrong markup.

Migrations need module awareness. Discovery registers module migration paths, but ad-hoc commands (migrate --path=...) bypass it. Keep the default php artisan migrate in deploy scripts rather than per-module paths, and remember migrate:fresh ordering across modules follows discovery order — avoid cross-module foreign keys without a deliberate ordering strategy.

module:seed needs the module name, and seeder classes must match the module namespace. A DatabaseSeeder copied from the app root will reference app-level namespaces and fail; use module:make seeder --module=billing as the base.

Custom stubs are the extension point worth investing in. module:make supports custom stubs with placeholders. Publish/edit the stub set once so every generated class carries your standard traits, declare(strict_types=1), final classes, or team docblocks — otherwise 30 generators produce 30 slightly different house styles.

Directory remapping is global. interlock.directories redirects generator output, but discovery follows the same config. Change models to app/Models inside a module and both generation and discovery move — which is what you want, but it means a config change can orphan already-generated files without any error.

Ecosystem risk is the headline gotcha. PHP 8.5 / Laravel 13 support narrows the pool of packages you can combine this with (Livewire, laravel-actions, laravel-data must all be on 8.5-era releases). In composer.json, let module:action / module:data integrations stay optional: don't require-dev them in the root if any module is meant to work without them, or you'll pin the whole app to their constraints.

Debugging starting points, in order: php artisan module:list (is it discovered?) → Interlock::moduleForClass() (is the autoload right?) → php artisan route:list / migrate:status (is that discovery type on?) → module:clear && module:cache (is the manifest stale?).

Requirements

  • PHP 8.5+ and Laravel 13.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource