kodicms/module-loader
A Laravel module loader for KodiCMS that discovers, registers, and boots modular packages, wiring service providers, routes, and resources into the application lifecycle. Organizes feature modules and simplifies extending the CMS.
Install the package with Composer and let Laravel discover the service provider:
composer require kodicms/module-loader
Publish the configuration so you can control where modules live:
php artisan vendor:publish --provider="KodiCMS\ModuleLoader\Providers\ModuleLoaderServiceProvider"
Look first at config/module-loader.php (or config/modules.php depending on the release you install). That file defines the modules directory path, the enabled/disabled module list, and the namespace used for autoloading module code. This is the single source of truth for how the package finds you.
Then scaffold your first module with the artisan command the package registers (typically php artisan module:make Blog). The command generates a folder containing a service provider, routes file, and language/view directories. Your first real use case is almost always "I want a Blog section with its own routes, controllers, and views that doesn't pollute app/Http." Register the generated provider (or rely on the package's loader to auto-register it) and hit a route defined in the module's routes.php to prove the wiring works end-to-end.
Module-per-feature. Treat each top-level module as a bounded context: Blog, Shop, Admin. Inside, mirror the standard Laravel layout (Http/Controllers, Models, Resources/views, routes.php, config/, lang/). The loader's job is just to make those paths discoverable — keep everything else idiomatic Laravel so you're not relearning a framework.
Provider-driven bootstrapping. Each module ships a service provider. Use loadViewsFrom(), loadTranslationsFrom(), loadMigrationsFrom(), and loadRoutesFrom() in the provider's boot() method so the module's assets are namespaced (e.g. view('blog::post.show')). This keeps modules portable and prevents collisions between modules that both have a home view.
Registration order and dependencies. If module B's service provider needs module A's bindings, don't rely on directory alphabetization. Explicitly order providers in the config, or have B's provider require the contract A binds to and let Laravel's container resolve it. The loader is a discovery mechanism, not a dependency manager.
Config as a contract. Publish or ship a config/<module>.php and merge it via mergeConfigFrom() in the provider. Consumers then override with config('blog.per_page'). This is the cheapest way to make a module usable by two applications without forking it.
Console workflow. Use the module-aware generator commands (module:make, and any module:* commands the package exposes) as your daily driver. Wire module:migrate/module:seed variants into your deploy scripts so each module's migrations run under its own namespace rather than dumping every migration into the root database/migrations folder.
Integration tip — routes per environment. Keep routes.php thin and split into routes/web.php and routes/api.php, loading each with its own middleware group from the provider. This is the single biggest source of "why is my module route hitting the web middleware in my API test" confusion.
Laravel version pin. This package's last release is 2016, which places it in the Laravel 5.x era. Before adopting it, pin your Laravel major and verify composer why-not laravel/framework <your-version> — expect to either freeze Laravel or maintain a fork. Do not assume loadRoutesFrom exists on older 5.x branches; check the actual method signatures available to you.
Composer autoloading. Modules usually live outside app/, which means PSR-4 autoloading must be registered per module. If a class "exists" but Composer can't find it, run composer dump-autoload after adding a module — there is no hot-reload of the autoloader.
Config and route caching. Once you run php artisan config:cache or route:cache, the dynamic module discovery can silently break because the loader's runtime registration isn't cache-aware. Clear both caches after any module add/remove, and avoid caching in local development while iterating on modules.
Naming collisions. Two modules with a Post model or a home route will overwrite each other without warning. Namespace everything — models under Modules\Blog\Models, views under blog::, translations under blog::. The loader won't save you here.
Debugging discovery. If a module isn't loading, check in this order: (1) is it listed in the config's enabled array, (2) does its provider's namespace match the folder, (3) does composer dump-autoload pick up the class, (4) is the provider actually registered with the container. Nine times out of ten it's step 2 or 3.
Extension point. The cleanest way to bend the loader to your needs is a custom provider that calls the loader facade in register() to pull in additional module paths (e.g. a packages/ directory for vendored modules). Avoid editing the package's source directly — you'll lose the change on composer update, and with no active maintenance you'll be doing the surgery repeatedly.
Licensing. GPL-2.0 is copyleft and can be a real constraint for closed-source products that link against the package. Confirm with legal before shipping this in a commercial application; MIT/Apache alternatives may be a cheaper path than compliance review.
How can I help you explore Laravel packages today?