Skip to content

How it works

The whole design rests on one idea: a module is a real Composer package from day one. Everything else — discovery, enable/disable, zero-churn promotion — follows from that.

A module is a Composer package

Each module folder ships its own composer.json with PSR-4 autoload and Laravel provider discovery:

jsonc
// Modules/Blog/composer.json
{
  "name": "acme/blog-module",
  "type": "laravel-module",
  "autoload": { "psr-4": { "Modules\\Blog\\": "src/" } },
  "extra": { "laravel": { "providers": [
    "Modules\\Blog\\Infrastructure\\Providers\\BlogServiceProvider"
  ]}}
}

The app declares the modules directory as a path repository:

jsonc
// app composer.json
"repositories": [{ "type": "path", "url": "Modules/*" }],
"require": { "acme/blog-module": "*" }

Composer symlinks Modules/Blog into vendor/, so it behaves exactly like any installed package — no custom autoloader.

The boot chain

composer.json (path repo)
        │  composer install

vendor/acme/blog-module  ──►  symlink to Modules/Blog
        │  PSR-4 from the module's own composer.json

Modules\Blog\…  classes autoload
        │  Laravel package auto-discovery reads extra.laravel.providers

BlogServiceProvider registers itself
        │  modules_statuses.json + ModuleManager

the module is gated on/off

Two layers, clear jobs

LayerJob
Composer + LaravelAutoload classes and register module providers (standard mechanisms).
This packageDiscover modules (ModuleManager), gate them via modules_statuses.json, expose the Modules facade + module_path(), provide a convention-loading base provider with attribute wiring, and compile it all for production (module:cache).

Why promotion is zero-churn

The only "local" detail is the path repository entry in the app's composer.json. The module's namespace, structure, provider and autoload all live in the module's own composer.json and never change.

develop:  repositories: [{ type: path,  url: "Modules/*" }]
promote:  repositories: [{ type: vcs,   url: "git@…/blog-module.git" }]

The class Modules\Blog\… is identical whether symlinked from Modules/ or installed from a registry. The module was always a package — promotion only changes where Composer fetches it from.

See Promote to a package and Private distribution.