Laravel Modules
Laravel package that auto-discovers and registers module assets (configs, routes, views, migrations, commands, Livewire, Filament, Nova, and more) from a Modules/ directory at the project root.
composer require
mozex/laravel-modules
Laravel Modules gives your Laravel application a modular directory structure with zero configuration. Create a Modules/ directory at your project root, drop in your module folders, and the package handles the rest: it discovers and registers configs, service providers, helpers, Artisan commands, migrations, seeders, translations, views, Blade components, routes, events, listeners, Livewire components, Filament resources, and Nova resources.
No boilerplate. No manual registration arrays. Just follow the conventions and your modules work.
How it works
The package scans your Modules/ directory for assets that follow Laravel's standard conventions. A Blog module with a Routes/web.php file gets its routes loaded with the web middleware group. A View/Components/Card.php class becomes <x-blog::card />. A Lang/ directory registers translations under the blog namespace.
Every asset type uses the same pattern: the module's directory name becomes a kebab-case namespace prefix. Blog becomes blog, UserAdmin becomes user-admin, PWA becomes pwa. You don't configure this; it's derived from the directory name.
The discovery runs once per request (or once at cache time in production). Behind the scenes, the package uses Spatie's StructureDiscoverer for class scanning and glob patterns for file and directory matching. The results can be cached with a single Artisan command, making production boot times fast.
Installation
Install the package via Composer:
composer require mozex/laravel-modules
Then register the Modules namespace in your project's composer.json so PHP can autoload module classes:
{
"autoload": {
"psr-4": {
"App\\": "app/",
"Modules\\": "Modules/"
}
}
}
Regenerate the autoloader:
composer dump-autoload
That's it. The package auto-registers its service provider through Laravel's package discovery. All features are enabled by default.
If you want to change any defaults (disable features, adjust discovery patterns, set module load order), publish the config file:
php artisan vendor:publish --tag=laravel-modules-config
This creates config/modules.php with every option documented inline.
Module structure
Modules live under Modules/ at your project root. Each module is a PascalCase directory that mirrors Laravel's own application structure:
project-root/
├── app/
├── Modules/
│ ├── Blog/
│ │ ├── Config/
│ │ │ └── blog.php
│ │ ├── Console/
│ │ │ └── Commands/
│ │ │ └── PublishPosts.php
│ │ ├── Database/
│ │ │ ├── Factories/
│ │ │ │ └── PostFactory.php
│ │ │ ├── Migrations/
│ │ │ │ └── 2024_01_10_create_posts_table.php
│ │ │ └── Seeders/
│ │ │ └── BlogDatabaseSeeder.php
│ │ ├── Events/
│ │ │ └── PostPublished.php
│ │ ├── Filament/
│ │ │ └── Admin/
│ │ │ └── Resources/
│ │ │ └── Posts/
│ │ │ ├── PostResource.php
│ │ │ ├── Pages/
│ │ │ ├── Schemas/
│ │ │ └── Tables/
│ │ ├── Helpers/
│ │ │ └── formatting.php
│ │ ├── Lang/
│ │ │ ├── en/
│ │ │ │ └── messages.php
│ │ │ └── en.json
│ │ ├── Listeners/
│ │ │ └── NotifyFollowers.php
│ │ ├── Livewire/
│ │ │ └── PostEditor.php
│ │ ├── Models/
│ │ │ └── Post.php
│ │ ├── Nova/
│ │ │ └── Post.php
│ │ ├── Policies/
│ │ │ └── PostPolicy.php
│ │ ├── Providers/
│ │ │ └── BlogServiceProvider.php
│ │ ├── Resources/
│ │ │ └── views/
│ │ │ ├── home.blade.php
│ │ │ ├── components/
│ │ │ │ └── alert.blade.php
│ │ │ └── livewire/
│ │ │ └── post-editor.blade.php
│ │ ├── Routes/
│ │ │ ├── web.php
│ │ │ ├── api.php
│ │ │ └── console.php
│ │ └── View/
│ │ └── Components/
│ │ └── Card.php
│ └── Shop/
│ └── ...
└── vendor/
You don't need all of these directories. Only create what your module needs. A module with just a Routes/web.php and a Resources/views/ directory is perfectly valid.
Mature modules also tend to grow directories the package doesn't touch but that have become common Laravel conventions: Actions/, Concerns/ (for traits), Data/ or DataTransferObjects/, Enums/, Exceptions/, Http/Controllers/, Http/Middleware/, Http/Requests/, Jobs/, Mail/, Notifications/, Settings/, Support/, and so on. None of these are auto-discovered. They're just file locations that follow Laravel naming conventions, so the classes inside them autoload through the module's PSR-4 mapping like any other class. Use them the same way you'd use their app/ counterparts.
Quick start
Once installed, create your first module:
Modules/
└── Blog/
├── Models/
│ └── Post.php
├── Resources/
│ └── views/
│ └── index.blade.php
└── Routes/
└── web.php
Your model uses the Modules\Blog namespace:
namespace Modules\Blog\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $fillable = ['title', 'body'];
}
Your route file works like any Laravel route file:
use Illuminate\Support\Facades\Route;
use Modules\Blog\Models\Post;
Route::get('/blog', function () {
return view('blog::index', ['posts' => Post::all()]);
});
And your view is accessible through the blog:: namespace:
{{-- Modules/Blog/Resources/views/index.blade.php --}}
@foreach($posts as $post)
<h2>{{ $post->title }}</h2>
<p>{{ $post->body }}</p>
@endforeach
That's a working module. The route file gets loaded with the web middleware group (because it's named web.php), the view is registered under the blog namespace, and the model is autoloaded through the PSR-4 mapping you set up during installation.
Naming conventions
The package converts your module's directory name to a kebab-case prefix for all namespaced assets:
| Module directory | Prefix | Example usage |
|---|---|---|
Blog |
blog |
view('blog::home') |
UserAdmin |
user-admin |
<x-user-admin::nav /> |
PWA |
pwa |
<livewire:pwa::icons /> |
CRM |
crm |
__('crm::messages.welcome') |
MyAPI |
my-api |
view('my-api::dashboard') |
This prefix applies to views, Blade components, anonymous components, Livewire components, and translations. Routes use the filename for grouping instead (see the Routes docs).
Configuration
All settings live in config/modules.php. The file has three layers:
Global settings
'modules_directory' => 'Modules', // where modules live (relative to project root)
'modules_namespace' => 'Modules\\', // PSR-4 namespace prefix
If you rename your modules directory (say, to src/Domains/), update both of these and your composer.json autoload mapping to match.
Per-module settings
Control which modules are active and the order they load:
'modules' => [
'Shared' => [
'active' => true,
'order' => 1, // lower numbers load first
],
'Blog' => [
'active' => true,
'order' => 2,
],
'Legacy' => [
'active' => false, // completely ignored during discovery
],
],
Modules not listed here default to active: true with an order of 9999. Setting active to false skips the module entirely: no routes, no views, no migrations, nothing.
The order value controls the sequence in which discovered assets are registered. If your Shared module defines base configs that other modules depend on, give it a low order number so it loads first.
Per-feature settings
Each feature has its own config section with an active toggle and discovery patterns:
'views' => [
'active' => true,
'patterns' => [
'*/Resources/views',
],
],
Setting active to false disables that feature for all modules. The patterns array controls which directories or files get scanned, using glob syntax relative to the modules directory.
Some features have extra options. Configs have a priority flag that controls merge direction. Routes have commands_filenames and channels_filenames arrays for special route files. Livewire has a view_path setting for single-file and multi-file component locations. Check the feature-specific documentation pages for details.
Modules facade
The Mozex\Modules\Facades\Modules facade provides utility methods you can use in your application code:
Path helpers
use Mozex\Modules\Facades\Modules;
// Absolute path to the modules directory
Modules::modulesPath();
// e.g., /var/www/app/Modules
// Path to a specific location within modules
Modules::modulesPath('Blog/Config');
// e.g., /var/www/app/Modules/Blog/Config
// Project base path with optional suffix
Modules::basePath('storage');
// e.g., /var/www/app/storage
Module name extraction
// From a fully-qualified class name
Modules::moduleNameFromNamespace('Modules\\Blog\\Models\\Post');
// returns 'Blog'
// From a file path
Modules::moduleNameFromPath('/var/www/app/Modules/Blog/Models/Post.php');
// returns 'Blog'
Seeders
// Get all discovered module seeder classes
Modules::seeders();
// returns ['Modules\Blog\Database\Seeders\BlogDatabaseSeeder', ...]
Route customization
The package already discovers web.php, api.php, console.php, and channels.php files inside each module's Routes/ directory with sensible defaults. web.php gets the web middleware group, api.php gets the api prefix and middleware, and so on.
If you want to discover a different route file type, say admin.php or localized.php, you define a new route group using Modules::routeGroup(). The group name has to match the filename. Once defined, any module that drops a Routes/admin.php file will have those routes loaded with the attributes you configured:
// Now every module's Routes/admin.php file loads with these attributes
Modules::routeGroup('admin',
prefix: 'admin',
middleware: ['web', 'auth', 'is-admin'],
as: 'admin.',
);
Attribute values can be closures, which are useful when you need to pull values from config or other sources that aren't available at service provider registration time:
Modules::routeGroup('api',
prefix: fn () => config('app.api_prefix', 'api'),
middleware: ['api'],
);
For groups that need more than a simple Route::group() wrapper, use registerRoutesUsing() to take over the registration entirely. The closure receives the group's attributes and the route file and can wrap them in whatever logic the group needs:
// Every module's Routes/localized.php file will be wrapped in Route::localized()
Modules::registerRoutesUsing('localized', function (array $attributes, $routes) {
Route::localized(fn () => Route::group($attributes, $routes));
});
Call both routeGroup() and registerRoutesUsing() from a service provider's register() method so they're defined before route discovery runs.
Features at a glance
Here's everything the package discovers and registers:
| Feature | What it does | Config key |
|---|---|---|
| Configs | Merges module config files into Laravel's config | configs |
| Service Providers | Registers module service providers | service-providers |
| Helpers | Loads helper function files via require_once |
helpers |
| Commands | Registers Artisan commands | commands |
| Migrations | Adds migration paths to Laravel's migrator | migrations |
| Seeders | Discovers module database seeders | seeders |
| Translations | Registers namespaced and JSON translations | translations |
| Views | Registers view namespaces and anonymous components | views |
| Blade Components | Registers class-based Blade components | blade-components |
| Routes | Loads route files with group-based middleware | routes |
| Models & Factories | Wires model-factory name guessing | models, factories |
| Policies | Wires model-policy name guessing | policies |
| Events & Listeners | Enables event auto-discovery in modules | listeners |
| Livewire | Registers Livewire components (class, SFC, MFC) | livewire-components |
| Filament | Registers resources, pages, widgets, clusters per panel | filament-* |
| Nova | Registers Nova resources | nova-resources |
Livewire, Filament, and Nova features are only active when their respective packages are installed. You don't need to configure anything to skip them.
Each feature has its own documentation page with detailed configuration, directory layout, usage examples, and troubleshooting tips. Browse them in the sidebar.
Integrations
Beyond the features the package discovers, there are integration guides for common tools you'll pair with a modular Laravel project:
- Inertia: module-scoped Inertia pages for Vue or React, Vite alias setup, TypeScript configuration, and typed props generated from PHP via Spatie TypeScript Transformer.
- PHPStan: configuring PHPStan to analyse every module directory, plus composer scripts for the common commands.
- PHPUnit: wiring module tests into your PHPUnit test suite.
- Pest: applying Pest's TestCase and traits to module tests.