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

Modular Bundle Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.

  1. Clone the repo and read in this order: 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.
  2. Identify the moving parts. Bundles of this shape almost always provide: a module definition (name, path, enabled flag), a manager/registry service that enumerates modules, and loader logic that pulls in each module's routing, config, and services. Grep for ModuleManager, ModuleInterface, CompilerPass, Resources/config — verify the actual names against src/, the README is not reliable here.
  3. First use case: build the equivalent yourself in Laravel in ~60 lines. The value is the pattern, and Laravel's provider system already does 90% of the work.
// 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.

Implementation Patterns

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.

Gotchas and Tips

  • Do not 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.
  • Zero dependents means zero answers. No Stack Overflow, no issue tracker history, no security patches. If you do copy code, copy the small stuff (registry, manifest cache) and rewrite the wiring.
  • 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.
  • Route registration order matters. Later providers win on duplicate URIs; a module that registers /admin will silently shadow the app's. Prefix every module route group with the module name.
  • Autoload drift. New module namespace → composer dump-autoload. If CI uses --classmap-authoritative, a module added without a dump will 500 in production only.
  • Migration collisions. Two modules creating a 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.
  • Migration ordering across modules. Laravel loads migrations by filename timestamp only. If module B's migration has an FK to module A's table, A must sort earlier — document the dependency, or batch module migrations through a single command with an explicit order.
  • Container resolution in register() vs boot(). Don't resolve ModuleRegistry in another provider's register(); ordering isn't guaranteed. Use boot() or defer with $this->app->booted(...).
  • Extension points to look for in the bundle source: the compiler pass (how modules get discovered), the module interface (what a module must declare), and any event dispatched during module boot. Those three map 1:1 to Laravel's provider list, a ModuleDescriptor value object, and Event::dispatch(new ModuleBooted($name)).
  • Debugging checklist: 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."
  • When you genuinely need this shape at scale, evaluate maintained alternatives first: 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.
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