idel/laravel-modular
idel/laravel-modular brings a structured module system to Laravel, letting you split large applications into self-contained packages with their own routes, views, migrations, config, and service providers, so features stay organized, reusable, and easy to enable or disable.
Install with a pinned constraint — the last tagged release is from December 2023, so treat the version you test as the version you ship:
composer require idel/laravel-modular:^1.0
Before writing code, check two things in vendor/idel/laravel-modular/:
composer.json — the illuminate/* constraints tell you whether the package will actually run on your Laravel version (especially Laravel 11+, where the skeleton and service-provider bootstrapping changed). If it lags, either stay on the supported version or plan to fork.bootstrap/providers.php / config/app.php.Then publish the scaffolding:
php artisan vendor:publish --provider="Idel\Modular\ModularServiceProvider"
That gives you config/modular.php, which is the real entry point. Read it top to bottom before anything else — it typically controls the module base path (modules/ or app-modules/), the PHP namespace for modules, stub locations, and (critically) whether module providers are auto-discovered or must be listed explicitly.
First use case: pick one cohesive slice of the app — billing, notifications, a reporting subsystem — and move it wholesale. Do not start by modularizing the whole codebase. A good first module has its own routes, a handful of migrations, and few inbound dependencies from the rest of the app. That way you validate the package's route/migration/view registration against something real, with a small blast radius.
If the package ships make commands, the flow is usually:
php artisan modular:make Billing # or module:make, check `artisan list`
composer dump-autoload # almost always required after scaffolding
Treat the module as a mini-Laravel app. The layout that works:
modules/Billing/
├── Providers/BillingServiceProvider.php
├── Http/Controllers/
├── Routes/web.php
├── Routes/api.php
├── Database/Migrations/
├── Resources/views/
├── Config/billing.php
└── Tests/
Inside the module provider's boot(), register the pieces explicitly rather than hoping discovery handles it:
$this->loadMigrationsFrom(__DIR__.'/../Database/Migrations');
$this->loadViewsFrom(__DIR__.'/../Resources/views', 'billing');
$this->mergeConfigFrom(__DIR__.'/../Config/billing.php', 'billing');
Route::middleware('web')
->prefix(config('modular.route_prefix'))
->name('billing.')
->group(__DIR__.'/../Routes/web.php');
Guard your route names and prefixes. Modules sharing a flat namespace collide fast. Always name routes (billing.invoices.index) and namespace config/views (config('billing.gateway'), view('billing::index')).
Cross-module communication: events and contracts, never direct class imports. If Billing needs data from Orders, define an interface in a small Core/Shared module and bind the implementation in the owning module's provider. Otherwise you get a module graph that is just a directory graph.
// modules/Core/Contracts/OrderLookup.php
interface OrderLookup { public function recentFor(int $userId): Collection; }
Register module console commands and schedules in the module provider, not in routes/console.php. Same for queued jobs — a module that works in HTTP but breaks under queue:work is usually a missing commands() or a class not in the autoload map.
Testing: add a testsuite per module in phpunit.xml pointing at modules/*/Tests, and make sure your base TestCase migrates all module migration paths. A module whose tests only pass when run in isolation is not done.
Deployment: modules must be enumerated deterministically. If auto-discovery scans the filesystem, confirm the order is stable and that config:cache + route:cache still produce the same route table as local dev — diff route:list output between environments once.
Class not found → run composer dump-autoload. If modules aren't covered by a psr-4 entry in the root composer.json, check whether the package registers it at runtime; runtime spl_autoload_register works in HTTP but silently fails in optimized/--classmap-authoritative deployments.config:cache hides new module config. Adding modules/Billing/Config/billing.php won't appear until php artisan config:clear. Same class of bug for view caches after moving Blade files.route:cache can bake an incomplete table. Verify with php artisan route:list --path=billing after caching.php artisan about, php artisan route:list --path=<module>, php artisan event:list, and dump the package's module registry (often app('modular')->all() or a Modular facade) to confirm what it thinks is installed. Nine times out of ten the registry disagrees with the filesystem.composer.json autoload and re-dumping.ServiceProvider that registers routes, migrations, views, and config, you can delete the package in an afternoon and replace it with a 20-line ModuleServiceProvider base class. That escape hatch is worth designing for on day one — it also makes the package's value proposition (convention and make commands) the only thing you're actually depending on.How can I help you explore Laravel packages today?