Laravel 9 Modules — HMVC

Bayram EKER
14 min readSep 6, 2022

There is a simple application at the end of the article

Hierarchical Model–View–Controller (HMVC)

The largest practical benefit of using an HMVC architecture is the “widgetization” of content structures.[3] An example might be comments, ratings, Twitter or blog RSS feed displays, or the display of shopping cart contents for an e-commerce website. It is essentially a piece of content that needs to be displayed across multiple pages, and possibly even in different places, depending on the context of the main HTTP request.

What is Module Management in Laravel?

Let’s imagine that you are working on a bigger scale application where you have to manage lots of features. You are managing multiple clients and your clients might have different requirements.

It would be hard to write custom code for each client rather you can create different types of modules and enable this module for only required client. That way you do not have to worry about writing custom code.

Another big advatange of using modular approach is that it comes with folder structure your feature is organized in nice directory structure as below:

app/
bootstrap/
vendor/
Modules/
├── Blog/
├── Assets/
├── Config/
├── Console/
├── Database/
├── Migrations/
├── Seeders/
├── Entities/
├── Http/
├── Controllers/
├── Middleware/
├── Requests/
├── Providers/
├── BlogServiceProvider.php
├── RouteServiceProvider.php
├── Resources/
├── assets/
├── js/
├── app.js
├── sass/
├── app.scss
├── lang/
├── views/
├── Routes/
├── api.php
├── web.php
├── Repositories/
├── Tests/
├── composer.json
├── module.json
├── package.json
├── webpack.mix.js

How will we use it ?

nwidart/laravel-modules is a Laravel package which created to manage your large Laravel app using modules. Module is like a Laravel package, it has some views, controllers or models. This package is supported and tested in Laravel 9.

Install

Composer

To install through Composer, by run the following command:

composer require nwidart/laravel-modules

The package will automatically register a service provider and alias.

Optionally, publish the package’s configuration and publish stubs by running:

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

To publish only the config:

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

To publish only the stubs

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

Autoloading

By default the module classes are not loaded automatically. You can autoload your modules using psr-4. For example :

{
"autoload": {
"psr-4": {
"App\\": "app/",
"Modules\\": "Modules/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
}
}

Tip: don’t forget to run composer dump-autoload afterwards

Lumen

Lumen doesn’t come with a vendor publisher. In order to use laravel-modules with lumen you have to set it up manually.

Create a config folder inside the root directory and copy vendor/nwidart/laravel-modules/config/config.php to that folder named modules.php

mkdir config
cp vendor/nwidart/laravel-modules/config/config.php config/modules.php

Then load the config and the service provider in bootstrap/app.php

$app->configure('modules');
$app->register(\Nwidart\Modules\LumenModulesServiceProvider::class)

Laravel-modules uses path.public which isn't defined by default in Lumen. Register path.public before loading the service provider.

$app->bind('path.public', function() {
return __DIR__ . 'public/';
});

Creating a module

To make modules use the artisan command php artisan module:make ModuleName to create a module called Posts:

php artisan module:make posts

This will create a module in the path Modules/Posts

You can create multiple modules in one command by specifying the names separately:

php artisan module:make customers contacts users invoices quotes

Which would create each module.

Flags

By default when you create a new module, the command will add some resources like a controller, seed class, service provider, etc. automatically. If you don’t want these, you can add --plain flag, to generate a plain module.

php artisan module:make Blog --plain

or

php artisan module:make Blog -p

Additional flags are as follows:

Generate an api module.

php artisan module:make Blog --api

Do not enable the module at creation.

php artisan module:make Blog --disabled

or

php artisan module:make Blog -d

Naming convention

Because we are autoloading the modules using psr-4, we strongly recommend using StudlyCase convention.

Folder structure

Modules/
├── Blog/
├── Config/
├── Console/
├── Database/
├── factories/
├── Migrations/
├── Seeders/
├── Entities/
├── Http/
├── Controllers/
├── Middleware/
├── Requests/
├── Providers/
├── PostsServiceProvider.php
├── RouteServiceProvider.php
├── Resources/
├── assets/
├── lang/
├── views/
├── Routes/
├── api.php
├── web.php
├── Tests/
├── composer.json
├── module.json
├── package.json
├── webpack.mix.js

Composer.json

Each module has its own composer.json file, this sets the name of the module, its description and author. You normally only need to change this file if you need to change the vendor name or have its own composer dependencies.

For instance say you wanted to install a package into this module:

"require": {
"dcblogdev/laravel-box": "^2.0"
}

This would require the package for this module, but it won’t be loaded for the main Laravel composer.json file. For that you would have to put the dependency in the Laravel composer.json file. The main reason this exists is for when extracting a module to a package.

Module.json

This file details the name alias and description / options:

{
"name": "Blog",
"alias": "blog",
"description": "",
"keywords": [],
"priority": 0,
"providers": [
"Modules\\Blog\\Providers\\BlogServiceProvider"
],
"aliases": {},
"files": [],
"requires": []
}

Modules are loaded in the priority order, change the priority number to have modules booted / seeded in a custom order.

The files option can be used to include files:

"files": [
"start.php"
]

Custom namespaces

When you create a new module it also registers new custom namespace for Lang, View and Config. For example, if you create a new module named blog, it will also register new namespace/hint blog for that module. Then, you can use that namespace for calling Lang, View or Config. Following are some examples of its usage:

Calling Lang:

Lang::get('blog::group.name');@trans('blog::group.name');

Calling View:

view('blog::index')view('blog::partials.sidebar')

Calling Config:

Config::get('blog.name')

Configuration

You can publish the package configuration using the following command:

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

In the published configuration file you can configure the following things:

Default namespace

What the default namespace will be when generating modules.

Default: Modules

The default namespace is set as Modules this will apply the namespace for all classes the module will use when it’s being created and later when generation additional classes.

Overwrite the generated files (stubs)

Overwrite the default generated stubs to be used when generating modules. This can be useful to customise the output of different files.

These stubs set options and paths.

Enabled true or false will enable or disable a module upon creation, the default is false meaning you will have to enable a module manually.

To enable a module edit module_statuses.json or run the command:

php artisan module:enable ModuleName

note the module_statues.json file will be created if it does not exist using this command.

The contents of module_statuses.json looks like:

{
"Users": true
}

The above would be when there is a single module called Users and is enabled.

Path points to a vendor directly where the default stubs are located, these can be published and modified.

Files set the file locations defaults.

Replacements is a way to do a Find and Replace on generation any matches will be replaced.

'stubs' => [
'enabled' => false,
'path' => base_path() . '/vendor/nwidart/laravel-modules/src/Commands/stubs',
'files' => [
'routes/web' => 'Routes/web.php',
'routes/api' => 'Routes/api.php',
'views/index' => 'Resources/views/index.blade.php',
'views/master' => 'Resources/views/layouts/master.blade.php',
'scaffold/config' => 'Config/config.php',
'composer' => 'composer.json',
'assets/js/app' => 'Resources/assets/js/app.js',
'assets/sass/app' => 'Resources/assets/sass/app.scss',
'webpack' => 'webpack.mix.js',
'package' => 'package.json',
],
'replacements' => [
'routes/web' => ['LOWER_NAME', 'STUDLY_NAME'],
'routes/api' => ['LOWER_NAME'],
'webpack' => ['LOWER_NAME'],
'json' => ['LOWER_NAME', 'STUDLY_NAME', 'MODULE_NAMESPACE', 'PROVIDER_NAMESPACE'],
'views/index' => ['LOWER_NAME'],
'views/master' => ['LOWER_NAME', 'STUDLY_NAME'],
'scaffold/config' => ['STUDLY_NAME'],
'composer' => [
'LOWER_NAME',
'STUDLY_NAME',
'VENDOR',
'AUTHOR_NAME',
'AUTHOR_EMAIL',
'MODULE_NAMESPACE',
'PROVIDER_NAMESPACE',
],
],
'gitkeep' => true,
],

Generator Path

By default, these are the files that are generated by default where generate is set to true, when false is used that path is not generated.

Don’t like Entities for the Models here’s where you can change the path to Models instead.

'generator' => [
'config' => ['path' => 'Config', 'generate' => true],
'command' => ['path' => 'Console', 'generate' => true],
'migration' => ['path' => 'Database/Migrations', 'generate' => true],
'seeder' => ['path' => 'Database/Seeders', 'generate' => true],
'factory' => ['path' => 'Database/factories', 'generate' => true],
'model' => ['path' => 'Entities', 'generate' => true],
'routes' => ['path' => 'Routes', 'generate' => true],
'controller' => ['path' => 'Http/Controllers', 'generate' => true],
'filter' => ['path' => 'Http/Middleware', 'generate' => true],
'request' => ['path' => 'Http/Requests', 'generate' => true],
'provider' => ['path' => 'Providers', 'generate' => true],
'assets' => ['path' => 'Resources/assets', 'generate' => true],
'lang' => ['path' => 'Resources/lang', 'generate' => true],
'views' => ['path' => 'Resources/views', 'generate' => true],
'test' => ['path' => 'Tests/Unit', 'generate' => true],
'test-feature' => ['path' => 'Tests/Feature', 'generate' => true],
'repository' => ['path' => 'Repositories', 'generate' => false],
'event' => ['path' => 'Events', 'generate' => false],
'listener' => ['path' => 'Listeners', 'generate' => false],
'policies' => ['path' => 'Policies', 'generate' => false],
'rules' => ['path' => 'Rules', 'generate' => false],
'jobs' => ['path' => 'Jobs', 'generate' => false],
'emails' => ['path' => 'Emails', 'generate' => false],
'notifications' => ['path' => 'Notifications', 'generate' => false],
'resource' => ['path' => 'Transformers', 'generate' => false],
'component-view' => ['path' => 'Resources/views/components', 'generate' => false],
'component-class' => ['path' => 'View/Component', 'generate' => false],
]

Package Commands

The commands you can run is determined from this list. Any commands you don’t want to use can be commented out / removed from this list and will not then be available when running php artisan.

'commands' => [
Commands\CommandMakeCommand::class,
Commands\ComponentClassMakeCommand::class,
Commands\ComponentViewMakeCommand::class,
Commands\ControllerMakeCommand::class,
Commands\DisableCommand::class,
Commands\DumpCommand::class,
Commands\EnableCommand::class,
Commands\EventMakeCommand::class,
Commands\JobMakeCommand::class,
Commands\ListenerMakeCommand::class,
Commands\MailMakeCommand::class,
Commands\MiddlewareMakeCommand::class,
Commands\NotificationMakeCommand::class,
Commands\ProviderMakeCommand::class,
Commands\RouteProviderMakeCommand::class,
Commands\InstallCommand::class,
Commands\ListCommand::class,
Commands\ModuleDeleteCommand::class,
Commands\ModuleMakeCommand::class,
Commands\FactoryMakeCommand::class,
Commands\PolicyMakeCommand::class,
Commands\RequestMakeCommand::class,
Commands\RuleMakeCommand::class,
Commands\MigrateCommand::class,
Commands\MigrateRefreshCommand::class,
Commands\MigrateResetCommand::class,
Commands\MigrateRollbackCommand::class,
Commands\MigrateStatusCommand::class,
Commands\MigrationMakeCommand::class,
Commands\ModelMakeCommand::class,
Commands\PublishCommand::class,
Commands\PublishConfigurationCommand::class,
Commands\PublishMigrationCommand::class,
Commands\PublishTranslationCommand::class,
Commands\SeedCommand::class,
Commands\SeedMakeCommand::class,
Commands\SetupCommand::class,
Commands\UnUseCommand::class,
Commands\UpdateCommand::class,
Commands\UseCommand::class,
Commands\ResourceMakeCommand::class,
Commands\TestMakeCommand::class,
Commands\LaravelModulesV6Migrator::class,
Commands\ComponentClassMakeCommand::class,
Commands\ComponentViewMakeCommand::class,
],

Overwrite the paths

Overwrite the default paths used throughout the package.

Set the path for where to place the Modules folder, where the assets will be published and the location for the migrations.

It’s recommend keep the defaults here.

'paths' => [
'modules' => base_path('Modules'),
'assets' => public_path('modules'),
'migration' => base_path('database/migrations'),

Scan additional folders for modules

By default, modules are loaded from a directory called Modules, in addition to the scan path. Any packages installed for modules can be loaded from here.

'scan' => [
'enabled' => false,
'paths' => [
base_path('vendor/*/*'),
],
],

You can add your own locations for instance say you’re building a large application and want to have multiple module folder locations, you can create as many as needed.

'scan' => [
'enabled' => true,
'paths' => [
base_path('ModulesCms'),
base_path('ModulesERP'),
base_path('ModulesShop'),
],
],

Remember to set enabled too true to enable these locations.

Composer file template

When generating a module the composer.json file will contain the author details as set out below, change them as needed.

Take special notice of the vendor, if you plan on extracting modules to packages later it’s recommend using your BitBucket/GitHub/GitLab vendor name here.

'composer' => [
'vendor' => 'nwidart',
'author' => [
'name' => 'Nicolas Widart',
'email' => 'n.widart@gmail.com',
],
]

Caching

If you have many modules it’s a good idea to cache this information (like the multiple module.json files for example).

Modules can be cached, by default caching is off.

'cache' => [
'enabled' => false,
'key' => 'laravel-modules',
'lifetime' => 60,
],

Registering custom namespace

Decide which custom namespaces need to be registered by the package. If one is set to false, the package won’t handle its registration.

Helpers

Module path function

Get the path to the given module.

$path = module_path('Blog');

Returns absolute path of project ending with /Modules/Blog

module_path can take a string as a second param, which tacks on to the end of the path:

$path = module_path('Blog', 'Http/controllers/BlogController.php');

Returns absolute path of project ending with /Modules/Blog/Http/controllers/BlogController.php

Compiling Assets (Laravel Mix)

Installation & Setup

When you create a new module it also create assets for CSS/JS and the webpack.mix.js configuration file.

php artisan module:make Blog

Change directory to the module:

cd Modules/Blog

The default package.json file includes everything you need to get started. You may install the dependencies it references by running:

npm install

Running Mix

Mix is a configuration layer on top of Webpack, so to run your Mix tasks you only need to execute one of the NPM scripts that is included with the default laravel-modules package.json file

// Run all Mix tasks...
npm run dev
// Run all Mix tasks and minify output...
npm run production

After generating the versioned file, you won’t know the exact file name. So, you should use Laravel’s global mix function within your views to load the appropriately hashed asset. The mix function will automatically determine the current name of the hashed file:

// Modules/Blog/Resources/views/layouts/master.blade.php<link rel="stylesheet" href="{{ mix('css/blog.css') }}"><script src="{{ mix('js/blog.js') }}"></script>

For more info on Laravel Mix view the documentation here: https://laravel.com/docs/mix

Note: to prevent the main Laravel Mix configuration from overwriting the public/mix-manifest.json file:

Install laravel-mix-merge-manifest

npm install laravel-mix-merge-manifest --save-dev

Modify webpack.mix.js main file

let mix = require('laravel-mix');
/* Allow multiple Laravel Mix applications*/
require('laravel-mix-merge-manifest');
mix.mergeManifest();
/*----------------------------------------*/
mix.js('resources/assets/js/app.js', 'public/js')
.sass('resources/assets/sass/app.scss', 'public/css');

Artisan commands

Useful Tip:

You can use the following commands with the --help suffix to find its arguments and options.

Note all the following commands use “Blog” as example module name, and example class/file names

Utility commands

module:make

Generate a new module.

php artisan module:make Blog

module:make

Generate multiple modules at once.

php artisan module:make Blog User Auth

module:use

Use a given module. This allows you to not specify the module name on other commands requiring the module name as an argument.

php artisan module:use Blog

module:unuse

This unsets the specified module that was set with the module:use command.

php artisan module:unuse

module:list

List all available modules.

php artisan module:list

module:migrate

Migrate the given module, or without a module an argument, migrate all modules.

php artisan module:migrate Blog

module:migrate-rollback

Rollback the given module, or without an argument, rollback all modules.

php artisan module:migrate-rollback Blog

module:migrate-refresh

Refresh the migration for the given module, or without a specified module refresh all modules migrations.

php artisan module:migrate-refresh Blog

module:migrate-reset Blog

Reset the migration for the given module, or without a specified module reset all modules migrations.

php artisan module:migrate-reset Blog

module:seed

Seed the given module, or without an argument, seed all modules

php artisan module:seed Blog

module:publish-migration

Publish the migration files for the given module, or without an argument publish all modules migrations.

php artisan module:publish-migration Blog

module:publish-config

Publish the given module configuration files, or without an argument publish all modules configuration files.

php artisan module:publish-config Blog

module:publish-translation

Publish the translation files for the given module, or without a specified module publish all modules migrations.

php artisan module:publish-translation Blog

module:enable

Enable the given module.

php artisan module:enable Blog

module:disable

Disable the given module.

php artisan module:disable Blog

module:update

Update the given module.

php artisan module:update Blog

Generator commands

module:make-command

Generate the given console command for the specified module.

php artisan module:make-command CreatePostCommand Blog

module:make-migration

Generate a migration for specified module.

php artisan module:make-migration create_posts_table Blog

module:make-seed

Generate the given seed name for the specified module.

php artisan module:make-seed seed_fake_blog_posts Blog

module:make-controller

Generate a controller for the specified module.

php artisan module:make-controller PostsController Blog

Optional options:

  • --plain,-p : create a plain controller
  • --api : create a resouce controller

module:make-model

Generate the given model for the specified module.

php artisan module:make-model Post Blog

Optional options:

  • --fillable=field1,field2: set the fillable fields on the generated model
  • --migration, -m: create the migration file for the given model

module:make-provider

Generate the given service provider name for the specified module.

php artisan module:make-provider BlogServiceProvider Blog

module:make-middleware

Generate the given middleware name for the specified module.

php artisan module:make-middleware CanReadPostsMiddleware Blog

module:make-mail

Generate the given mail class for the specified module.

php artisan module:make-mail SendWeeklyPostsEmail Blog

module:make-notification

Generate the given notification class name for the specified module.

php artisan module:make-notification NotifyAdminOfNewComment Blog

module:make-listener

Generate the given listener for the specified module. Optionally you can specify which event class it should listen to. It also accepts a --queued flag allowed queued event listeners.

php artisan module:make-listener NotifyUsersOfANewPost Blog
php artisan module:make-listener NotifyUsersOfANewPost Blog --event=PostWasCreated
php artisan module:make-listener NotifyUsersOfANewPost Blog --event=PostWasCreated --queued

module:make-request

Generate the given request for the specified module.

php artisan module:make-request CreatePostRequest Blog

module:make-event

Generate the given event for the specified module.

php artisan module:make-event BlogPostWasUpdated Blog

module:make-job

Generate the given job for the specified module.

php artisan module:make-job JobName Blogphp artisan module:make-job JobName Blog --sync # A synchronous job class

module:route-provider

Generate the given route service provider for the specified module.

php artisan module:route-provider Blog

module:make-factory

Generate the given database factory for the specified module.

php artisan module:make-factory ModelName Blog

module:make-policy

Generate the given policy class for the specified module.

The Policies is not generated by default when creating a new module. Change the value of paths.generator.policies in modules.php to your desired location.

php artisan module:make-policy PolicyName Blog

module:make-rule

Generate the given validation rule class for the specified module.

The Rules folder is not generated by default when creating a new module. Change the value of paths.generator.rules in modules.php to your desired location.

php artisan module:make-rule ValidationRule Blog

module:make-resource

Generate the given resource class for the specified module. It can have an optional --collection argument to generate a resource collection.

The Transformers folder is not generated by default when creating a new module. Change the value of paths.generator.resource in modules.php to your desired location.

php artisan module:make-resource PostResource Blog
php artisan module:make-resource PostResource Blog --collection

module:make-test

Generate the given test class for the specified module.

php artisan module:make-test EloquentPostRepositoryTest Blog

Basic Application

Syntax of create module command:

php artisan make:module module_name

Then run the following command to create module let’s do an example for Posts module.

php artisan make:module posts

After running above commands it will generate our Posts module under Modules folder. See below Laravel module structures:

app/
bootstrap/
vendor/
Modules/
├── Posts/
├── Assets/
├── Config/
├── Console/
├── Database/
├── Migrations/
├── Seeders/
├── Entities/
├── Http/
├── Controllers/
├── Middleware/
├── Requests/
├── Providers/
├── PostsServiceProvider.php
├── RouteServiceProvider.php
├── Resources/
├── assets/
├── js/
├── app.js
├── sass/
├── app.scss
├── lang/
├── views/
├── Routes/
├── api.php
├── web.php
├── Repositories/
├── Tests/
├── composer.json
├── module.json
├── package.json
├── webpack.mix.js

Now, we successfully generated our Posts module. Let's test it by running the command below:

php artisan serve

Then run the URL to your browser:

http://127.0.0.1:8000/posts

Then you will see the result below:

--

--