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. The Controllers or Providers 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.
composer.json
{
  "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 in ModuleServiceProvider.
  • 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:

cli
php artisan vendor:publish --provider="Nwidart\Modules\LaravelModulesServiceProvider" --tag="stubs"

And specify the directory with the published templates in the configuration.

config/modules.php
[
    '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.

cli
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.

cli
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

config/modules.php
[
'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.

stubs/nwidart-stubs/scaffold/provider.stub
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.xml
<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.

.env.example
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

config/telescope.php
[
  'watchers' => [
     'ignore' => [
                'modules.*'
     ],
  ]
]

If you use Octane for local work, add the modules directory to the watch configuration

config/octane.php
[
 'watch' => [
        ...
        'Modules'
    ],
]

And if you use PHPstan, don't forget to add modules to scan

parameters:
    paths:
        - Modules/