doyo/modular-bundle
Modular Bundle for Laravel: organize your app into self-contained modules with their own routes, views, migrations, and service providers. A lightweight way to split large projects into maintainable, reusable pieces with minimal configuration and no boilerplate overhead.
First, the honest reality check. doyo/modular-bundle is a Symfony bundle (HttpKernel + DependencyInjection based), not a Laravel package. There is no service provider, no extra.laravel block, and no facade. If you composer require doyo/modular-bundle in a Laravel app you will pull Symfony components you don't need and get nothing bootable — Laravel has no config/bundles.php equivalent, so there is no registration hook for it to attach to. With zero stars and zero dependents, it is effectively unmaintained; treat it as reference architecture, not a dependency.
So "getting started" means reading it, not installing it.
composer.json (what Symfony versions it targets — this tells you how old it is), then README.md (likely thin), then src/ looking for the module manager/registry class and DependencyInjection/ for the compiler pass that wires modules in. That compiler pass is the whole idea of the package in one file.ModuleManager, ModuleInterface, CompilerPass, Resources/config — verify the actual names against src/, the README is not reliable here.// app/Modules/ModuleServiceProvider.php
abstract class ModuleServiceProvider extends ServiceProvider
{
abstract protected function modulePath(): string;
public function register(): void
{
$path = $this->modulePath();
$this->mergeConfigFrom("$path/Config/config.php", $this->moduleName());
$this->app->singleton("module.{$this->moduleName()}", fn () => new ModuleDescriptor(
name: $this->moduleName(), path: $path,
));
}
public function boot(): void
{
$path = $this->modulePath();
$this->loadRoutesFrom("$path/Routes/web.php");
$this->loadMigrationsFrom("$path/Database/Migrations");
$this->loadViewsFrom("$path/Resources/views", $this->moduleName());
}
}
Then a ModuleRegistry singleton that scans app/Modules/*/ and register()s each provider from AppServiceProvider::register(). That is the entire feature set you'd otherwise be importing.
Directory-per-module, self-contained. The bundle's core assumption is that a module owns its own routes, config, DI definitions, and resources, and the host app only knows about a manifest. Mirror that:
app/Modules/Billing/
├── BillingServiceProvider.php
├── Config/config.php
├── Routes/web.php
├── Database/Migrations/
├── Http/Controllers/
├── Models/
└── Contracts/ # public surface — other modules may only use these
Registry + manifest instead of filesystem scanning at runtime. Scanning directories on every request is a footgun once you enable config:cache and route:cache. Cache the discovered module list:
final class ModuleRegistry
{
public function __construct(private array $modules) {}
public function enabled(): array
{
return array_filter($this->modules, fn ($m) => $m->enabled);
}
}
Populate it in a service provider from a cached array (Cache::rememberForever('modules.manifest', ...)) and add an artisan command (php artisan modules:clear) that busts it. This is the single most important lesson from any modular bundle: discovery must be cacheable.
Compiler-pass analog = tagged bindings. The Symfony side registers modules via a container compiler pass; in Laravel do it with container tags or an explicit provider list in config/modules.php. Prefer the explicit list — it's greppable and it survives config:cache.
Config namespacing. Always mergeConfigFrom($path, 'billing') and read via config('billing.key'). Never let a module call config('services.stripe') directly; give it a namespaced key so two modules can't collide.
Boundaries via contracts and events, not model reach-through. Modules should expose Contracts/* interfaces resolved from the container, and broadcast domain events (Billing\Events\InvoicePaid) that other modules listen to. This is what actually keeps a modular app modular — the DI plumbing is the easy part.
Composer integration tips. If a module is meant to be optional, ship it as a separate package with extra.laravel.providers in its composer.json so auto-discovery handles registration; during development use a path repository:
"repositories": [{ "type": "path", "url": "../modules/billing" }]
Scaffolding. Wrap module creation in php artisan make:module Billing — a stub-publishing command. Without it, every new module drifts in structure and the registry stops being trustworthy.
composer require this into a Laravel app. Beyond the missing registration hook, you inherit symfony/http-kernel and symfony/dependency-injection constraints that can conflict with Laravel's own Symfony component versions. Vendor the idea, not the code.config:cache and route:cache are where modular designs break. Any module registration that touches the filesystem or runs glob() at boot will behave differently in production. Test with php artisan config:cache && php artisan route:cache in CI, not just locally./admin will silently shadow the app's. Prefix every module route group with the module name.composer dump-autoload. If CI uses --classmap-authoritative, a module added without a dump will 500 in production only.settings table will explode. Enforce prefixed table names (billing_invoices) as a module convention, and be aware migrate:rollback across modules is order-dependent and rarely clean — prefer forward-only migrations per module.register() vs boot(). Don't resolve ModuleRegistry in another provider's register(); ordering isn't guaranteed. Use boot() or defer with $this->app->booted(...).ModuleDescriptor value object, and Event::dispatch(new ModuleBooted($name)).php artisan about (config/cache state), php artisan route:list --path=billing, php artisan tinker → app(ModuleRegistry::class)->enabled(), and php artisan config:clear as the first move whenever a module "disappears."nwidart/laravel-modules (mature, Laravel-native, handles the provider/route/config/migration loading you just hand-rolled) or plain Composer packages with auto-discovery. A dead Symfony bundle is a teaching aid, not a foundation.How can I help you explore Laravel packages today?