11. Laravel Modules. Stubs in Laravel projects. Laravel project Structure
Video version
(Leave your feedback on YouTube)
About the Modular Structure
The modular structure of a project, compared to the standard Laravel structure, has several significant advantages:
- Code Organization:
In a modular structure, each module is responsible for a specific functionality or domain area of the application, which greatly simplifies code navigation and maintenance. - Team Responsibility Division:
The modular structure allows different teams or developers to work on separate modules independently, greatly minimizing git conflicts. This also reduces planning and estimation time - managers know who is responsible for which module and whose errors have occurred. - Ability to Isolate a Module into a Separate Service:
This is the main reason to advise using modules when working with monolithic architecture. In situations where some part of the application needs more resources, it is much easier to isolate it into a service from a modular structure than from the standard Laravel structure. It won't be very easy and quick, but it's a realistic task that can be estimated. When everything is in one heap, it's impossible to estimate such a task. - Dependency Reduction:
The modular structure forces developers to think about the interaction between modules. This passively reduces the number of unnecessary connections. It also standardizes interaction approaches, greatly simplifying code understanding and transitioning to "microservices." - Testing:
On CI/CD, you run all tests. With modules, there is a load advantage in test coverage. You will stop running the first hundred tests because it takes too much time. Running tests for a specific module won't be a problem. Module Reuse:
In all "marketing" texts about modularity, module reuse is the first point. Create a module, package it into a separate project, and use it in all company projects. Theoretically, I can imagine how I could isolate such modules. There are two approaches. The first is to use the module as a composer dependency. This approach is not viable because you'll be afraid to update the package, creating a separate branch for each project out of fear of breaking something for other projects. You'll have a million concurrently "live" branches, each needing individual support. The second option is to publish (copy from dependencies to code) your module once and work with it locally. This is viable but will turn module development from scratch into maintaining something written by someone else. This is always longer than starting from scratch because your approaches, needs, and skills are likely different from the task for which the module was written. So use it at your own risk.
Laravel Modules package
For modularity, I recommend the package nWidart/laravel-modules
Be sure to configure the package for yourself. Generate only the files and directories you use. Minimize the amount of automatically generated code. Remember that connecting/registering an empty configuration file or service provider that you do not use affects the overall system performance.
For my project, I make the following changes:
- Move all dependencies from the
/app
directory. This is simply convenient. In the context of a module, a deep directory structure is unnecessary. TheControllers
orProviders
directory is easily perceived in the root directory of the module. - Change all directory names to Uppercase. This is convenient for the PSR-4 standard, namespaces will match directories, and no aliases for composer rules are needed.
- Avoid composer dependencies inside the module. The reason is simple - it's hard to manage. If we isolate a module into a separate service, we will still have to review a bunch of packages. Here, it's important to allow composer to automatically load classes from modules.
{
"autoload": {
"psr-4": {
"Modules\\": "Modules/"
}
}
}
Also, don't forget that in autoload on production, tests (and seeds, migrations, factories in an ideal world) should not be included. To achieve this, add them to autoload-dev
. I don't add them because in the .dockerignore.production
file I don't upload tests at all - **/**/Tests
.
- Avoid all frontend resources
/route/web.php
,/resources
,vite.config.js
. I only have API methods, no frontend. - Do not generate
config
- configuration is not needed for all modules. When needed, I create it for a specific module. - Do not generate
EventServiceProvider
- if needed, it's easier in terms of load to generate directly inModuleServiceProvider
. - Localization files - they are rarely needed, for example, for email text.
Laravel stub
In Laravel, a "stub" is a code template used to generate various classes and files. Any artisan command that creates a file does so from a .stub
file. The stub can be published. For nWidart/laravel-modules, this is mandatory, as configuring modules for yourself will be necessary in 99% of cases.
So let's publish the stubs:
php artisan vendor:publish --provider="Nwidart\Modules\LaravelModulesServiceProvider" --tag="stubs"
And specify the directory with the published templates in the configuration.
[
'stubs' => [
'path' => base_path('stubs/nwidart-stubs')
]
]
Besides the code, I also advise setting up the stub for Code Style immediately. This allows the team to automatically generate anything. The easiest way is to create a command that generates a module with all the most common files and run the code style command. Repeat until the module's code style is perfect.
rm -rf Modules/Blog &&
docker compose exec app php artisan module:make Blog &&
docker compose exec app php artisan module:make-model Post Blog &&
docker compose exec app php artisan module:make-request PostRequest Blog &&
docker compose exec app php artisan module:make-request PostResource Blog &&
docker compose exec app php artisan module:make-migration some_migration Blog &&
docker compose exec app php artisan module:make-test SomeTest Blog &&
docker compose exec app php artisan module:make-test SomeTest Blog --feature
And the check command.
docker compose exec app php artisan insights --no-interaction
Change the
stub
when code style rules change.
A tip from personal experience - remove comments from
stub
files. The team will generate files constantly, and looking at the same texts during every review isn't ideal (even though the brain ignores them, why waste its resources).
Custom Module Commands
nWidart/laravel-modules has many commands for working with modules. But sometimes there is a need for custom solutions or to change a command's behavior. For example, I find it convenient to generate a model immediately with migration, seeds, and a factory. This reminds the team that factories and seeds are mandatory. In cases where they aren't, they can always be deleted.
Just create a command like a usual Laravel one. To minimize code, extend an existing command. Then add it in
[
'commands' => ConsoleServiceProvider::defaultCommands()
->merge([
\App\Console\Command\ModelMakeCommand::class
])->toArray(),
]
Migrations and Seeds
By default, migrations in modules work just like regular ones, only stored in the module directories. There are optimization issues you should be aware of.
And my personal "pet peeve". Not that I invented it, this is how the first versions of the module package worked. Migrations after merging cannot be edited. They have already run on staging, production, and each developer's local machine. Using modules, we can isolate migrations in the module. And publish them only as the final stage of work.
I like this approach; it allows isolated work on a module. Merge, make mistakes, and fix them.
private function loadMigrations(): void
{
$isTestEnv = App::environment('testing');
if ($isTestEnv) {
$this->loadMigrationsFrom(module_path($this->moduleName, 'Database/Migrations'));
}
}
Here I load migrations only in the testing environment for tests.
But there is a big downside:
All development participants will forget to publish migrations.
Of course, you can go the extra mile and create a command to run on pre-push. But another time.
And about seeders. Laravel does not see seeds from modules by default. For comfortable work, they need to be added to the global database/seeders/DatabaseSeeder.php
.
I recommend collecting seeds in SomeModelDatabaseSeeder
and calling it in DatabaseSeeder
.
Tests setup
By default, Laravel does not see tests from module directories. This is easy to fix: add 2 blocks to phpunit.xml
: ModuleFeature
and ModuleUnit
.
<phpunit>
<testsuites>
...
<testsuite name="ModuleFeature">
<directory>./Modules/**/Tests/Feature</directory>
</testsuite>
<testsuite name="ModuleUnit">
<directory>./Modules/**/Tests/Unit</directory>
</testsuite>
</testsuites>
</phpunit>
Optimization and Cache
Modules, like all service providers, are loaded with each project initialization. For Laravel Octane, this is not critical, but for other solutions, it can affect optimization. Therefore, first, do not register what you do not use, and second, use cache.
MODULES_CACHE_ENABLED=true
MODULES_CACHE_DRIVER=file
Telescope, Octane, PHPstan
Add modules.*
to telescope EventWatcher.
This will free the telescope from spamming
the registration events of the laravel modules package
[
'watchers' => [
'ignore' => [
'modules.*'
],
]
]
If you use Octane for local work, add the modules directory to the watch
configuration
[
'watch' => [
...
'Modules'
],
]
And if you use PHPstan, don't forget to add modules to scan
parameters:
paths:
- Modules/