How to Apply Laravel Modular Approach Architecture in 9 Easy Steps

How to Apply Laravel Modular Approach Architecture in 9 Easy Steps

Laravel is powerful and very intelligent PHP Framework that can serve you from a very simple web application to large and enterprise level applications, Well that’s a good news but in other hand using such a large framework without any architecture approach in mind can lead to a massive mess and downfalls in the process of expanding and in production environment.

While Laravel has it’s own design system we can’t only rely on this. Imagine an Application with tons of Features that heavily depends on Each others, in this case maintaining any little refactoring or improvements can cause much problems and bugs. That’s we need to start with a modular architecture that helps separate and maintain each feature as a standalone package.

What is Modular Architecture

Modular Architecture is a way of your Laravel Project where each Feature is considered a standalone Block that can nicely work with other Blocks or Features. Its also serves the concept of Separation of Concerns so when you develop a Feature you have to only focus on that Feature and its Logic and how it can be used by any other Feature. So each Feature is a Module, for example we have Modules like :

Users, Blog, Pages, Products, .... etc

The Big Picture

In order to Apply the Modular Architecture we will need to separate each module in a dedicated directory which will contain the module’s Controller, Migrations, Views, Routes, Models, Providers and each piece of code that’s related to this Module. So we will need to instruct Laravel how to load each component of each module correctly.

To Achieve this separation we will rely on Service Providers, where each module will contact a Module Service Provider that will instruct Laravel how to interact with and Load that Module.

Create our Project

Lets create our laravel project which we will use to apply modular architecture

composer create-project "laravel/laravel" modular

Give composer few time to download and install required packages then after installation complete go to the installation directory using cd command and run the serve command to start up our application

cd modular
php artisan serve

Now you can visit your application in the browser using the link http://127.0.0.1:8000 , Note : keep eye on terminal output as the port 8000 may change.

1.png

Modules Structure

We will start by creating a directory called Modules that will contain all our modules, Each Module will by consist of multiple directories and files, so our directory tree for our Modules directory will look something like this

2.png

Modules
- Blog
-- Config
-- Database
-- Http
-- Models
-- Providers
-- View
-- Routes
+ Pages
+ Products

Configure Composer

Its very important to tell composer where to find and how to load our modules files and classes and before that we have to keep in mind that we will use namespaces to differentiate modules so each module fille will be prefixed by namespace of Modules\MODULE_NAME so for example

<?php 
namespace Modules\Blog\Providers;

....

So we need to add the base namespace Modules pointing to the Modules directory in composer.json file under autoload > psr-4 as follows

3.png

don’t forget to run composer dump-autoload command

Module Service Provider

Now Composer can find our Modules classes, so it’s the time to create the module Service Provider that will instruct laravel how to interact with our module, we will apply this to our Blog module as demonstration. Go to Modules > Blog > Providers and create a new class file ModuleServiceProvider.php, with following contents

<?php

namespace Modules\Blog\Providers;

use Illuminate\Support\ServiceProvider;

class ModuleServiceProvider extends ServiceProvider
{
    public function register()
    {
        //...
    }

    public function boot()
    {
        //...
    }
}

Load Modules Providers

As we have seen before, each module should has it’s own Service Provider that we will use to instruct laravel how to load and interact with out module, but how laravel will know about this Service Provider ?

We will need to tell Laravel to Scan the Modules Directory to find installed modules and for each module we will look for the ModuleServiceProvider class and load it in the System. Best place to do this is in AppServiceProvider.php located in app/Providers Directory. withint the Register method as follows :

<?php

namespace App\Providers;

use Illuminate\Support\Facades\File;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    //....

    public function register()
    {
        //Search for Modules
        $modulesBasePath = base_path('Modules');
        foreach(File::directories($modulesBasePath) as $moduleDirectory)
        {
            //Get Module Name from the owner Directory name
            $moduleName = basename($moduleDirectory);
            $providerClassName = '\\Modules\\'.$moduleName.'\\Providers\\ModuleServiceProvider';
            if(class_exists($providerClassName))
            {
                //Register Module Service Provider if Exist
                $this->app->register($providerClassName);
            }
        }
    }

    //....
}

Now Laravel will be aware of every module Service Provider, so next steps will be all included inside that providers

Routes Discovery

As each module will contain its own routes then we will create a new directory into the module named Routes which will contain a web.php file, that file where we will define the module routes, and as a start will will add just one route to test if it’s working

<?php

use Illuminate\Support\Facades\Route;

Route::get('/blog', function(){
    return 'Yay, Blog Routes Working';
});

If we tried to visit that route, it will give us 404 Not Found error as Laravel not yet aware of that file, so we will add a RouteServiceProvider.php that will load our web.php routes file, in the directory of Modules/Blog/Providers

<?php

namespace Modules\Blog\Providers;

use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;

class RouteServiceProvider extends ServiceProvider
{

    public function map()
    {
        $this->registerWebRoutes();
    }

    protected function registerWebRoutes()
    {
        //Add Web Routes with web Guard
        Route::middleware('web')
            //Set Default Controllers Namespace
            ->namespace('Modules\\Blog\\Http\\Controllers')
            ->group(base_path('Modules/Blog/Routes/web.php'));
    }
}

Note that we are now extending the Illuminate\Foundation\Support\Providers\RouteServiceProvider not the regular Service provider, this will tell laravel to call the map function automatically

Before we test we need to tell Laravel to also load that RouteServiceProvider inside the register method of the ModuleServiceProvider

<?php

namespace Modules\Blog\Providers;

use Illuminate\Support\ServiceProvider;

class ModuleServiceProvider extends ServiceProvider
{
    public function register()
    {
        //...
        $this->app->register(RouteServiceProvider::class);
    }

    //...
}

Now if we visit the test route at http://127.0.0.1:8000/blog it should work.

4.png

Module Views

Until now we can create routes but we only return a String, what if we want to return a Blade view, and how to differentiate views for each module using views namespacing.

It’s very easy, all we need to do is to register our custom Views path to Laravel views sources inside the ModuleServiceProvider boot method

<?php

namespace Modules\Blog\Providers;

use Illuminate\Support\ServiceProvider;

class ModuleServiceProvider extends ServiceProvider
{

    protected String $moduleName = 'Blog';

    //...

    public function boot()
    {
        $this->registerViews();
        //...
    }

    protected function registerViews()
    {
        $moduleViewsPath = __DIR__.'/../Views';
        $this->loadViewsFrom($moduleViewsPath, strtolower($this->moduleName));
    }
}

Lets test that by creating a new view inside Views directory and return it from our test route.

<?php

use Illuminate\Support\Facades\Route;

Route::get('/blog', function(){
    return view('blog::index');
});
<h1>Blog Module</h1>
<p>This view is loaded directly from Blog Module /Views directory</p>

Continue using Controllers

We can take that a step further and create our first modular controller for our module at the path of Modules\Blog\Http\Controllers named BlogController

<?php

namespace Modules\Blog\Http\Controllers;

use Illuminate\Routing\Controller;

class BlogController extends Controller
{
    public function index()
    {
        return view('blog::index');
    }
}
<?php

use Illuminate\Support\Facades\Route;
use Modules\Blog\Http\Controllers\BlogController;

Route::get('/blog', [BlogController::class, 'index']);

5.png

Register Migrations

Each module may contain its own database migration, that why we need to make laravel aware of this migrations which will once again be done inside ModuleServiceProvider boot method

?php

namespace Modules\Blog\Providers;

use Illuminate\Support\ServiceProvider;

class ModuleServiceProvider extends ServiceProvider
{
    //...
    public function boot()
    {
        $this->registerViews();
        $this->registerMigrations();
        //...
    }
    //...    
    protected function registerMigrations()
    {
        $this->loadMigrationsFrom(base_path('Modules/'.$this->moduleName.'/Database/Migrations'));
    }
    //...
}

Lets create a new Migration at path /Modules/Blog/Database/Migrations , which we will name as create_posts_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {

    public function up()
    {
        Schema::create('posts', function(Blueprint $table){
            $table->string('title');
            $table->text('content')->nullable();
            $table->unsignedBigInteger('author_id')->default(0);
            $table->unsignedBigInteger('category_id')->default(0);
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::drop('posts');
    }

};

Now let’s run the migration command to make sure it works

php artisan migrate

6.png

Merge Configurations

Since your modules may contains a configuration files, then we need to tell laravel to merge this files into main configurations to be able to retrieve it with config() function, this is once again can be done in the ModuleServiceProvider as follows

<?php

namespace Modules\Blog\Providers;

use Illuminate\Support\ServiceProvider;

class ModuleServiceProvider extends ServiceProvider
{
    //...
    public function boot()
    {
        $this->registerConfig();
        //...
    }
       //...
    protected function registerConfig()
    {
        $this->mergeConfigFrom(
            base_path('Modules/'.$this->moduleName.'/Config/config.php'), strtolower($this->moduleName)
        );
    }
}

Now we can reference our config file using config() function by prefixing with the lower-case module name for example

config('blog.post_per_page', 10);

Models, Middlewares and Events, What about ?

Those type of files is just regular classes like controllers, so laravel don’t need any extra work to be aware of as long as you use the right classes namespacing and directory structure.

#Final ModuleServiceProvider

<?php

namespace Modules\Blog\Providers;

use Illuminate\Support\ServiceProvider;

class ModuleServiceProvider extends ServiceProvider
{

    protected String $moduleName = 'Blog';

    public function register()
    {
        $this->app->register(RouteServiceProvider::class);
    }

    public function boot()
    {
        $this->registerConfig();
        $this->registerViews();
        $this->registerMigrations();
    }

    protected function registerViews()
    {
        $moduleViewsPath = __DIR__.'/../Views';
        $this->loadViewsFrom($moduleViewsPath, strtolower($this->moduleName));
    }

    protected function registerConfig()
    {
        $this->mergeConfigFrom(
            base_path('Modules/'.$this->moduleName.'/Config/config.php'), strtolower($this->moduleName)
        );
    }

    protected function registerMigrations()
    {
        $this->loadMigrationsFrom(base_path('Modules/'.$this->moduleName.'/Database/Migrations'));
    }
}

Final Words

Good Applications starts with a good structure and design pattern so it’s always the best to spend sometime designing your application structure.

I would also like to mention that our new Laravel Package has been release which will help developers to add Actions and Filters feature in Laravel Application like WordPress Apps, You can check out full tutorial of Eventual Events Package Here

Also i’m waiting for your questions and requests if you have any.