Laravel 9 Modules — HMVC
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: