Laravel Module Pattern

Laravel has a folder structure which ties similar entities of MVC together e.g. controllers in one folder, views into another. There are few set-backs of this approach :

  • If your project scales exponentially having lot of module, each of these directories scale as well. It becomes difficult to maintain.
  • If you want to remove the module entirely, there are files scattered in different folders to consider. This sometimes lead to unused files still present in your project repository.
  • Re-using a module into different project is a hassle (unless you have it installed coumpled as a plugin via composer)
  • While using code editors, as the module files are in different folders. We might need to expand the folders in project sidebar to view them at a glance. The scattered folder structure makes it difficult to for a quick view. (Forgive me for being picky here ;))

Implementation :

Before you dive in to further sections, if you are familier with basic concepts of larave, most part of the code will be very familier to you. Tt is really easier that you might think.

  • Creating the module :

Let's create a new folder called Modules in the project root. (You might want to create it inside app folder, I prefer it this way.) To use the \Modules namespace, we need to autoload it from composer.json in the psr-4 section.

 "autoload": {
        "psr-4": {
            "App\\": "app/",
            "Modules\\": "Modules/",
        }
    },

Let's dump the updated autoloads by doing following from terminal shell :

composer dump-autoload

Now we are good to start with the first module. Let's consider a ticket module where user can submit a ticket from frontend and we store it into the database. We will not focus much on the actual implementation of the ticketing ystem. We will emphasize on the structure of module.

  • Create a new directory Ticket.

Create a new directory Ticket inside Modules folder. I like to keep module names singular (Ticket instead of Tickets).

  • Creating a service provider class :

On a broader level, frameworks like laravel have special entry point wrappers which can find, register and instantiate the core functionalities. In case of Laravel it's the ServiceProvider class.

Create a new file TicketServiceProvider.php inside Modules/Ticket.

<?php

namespace Modules\Ticket;

use Illuminate\Support\ServiceProvider as BaseServiceProvider;

class TicketServiceProvider extends BaseServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot() {}

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register() {}
}

To make sure laravel considers this while booting up, we need to register it. Add this inside config/app.php's providers array :

App\Modules\Ticket\TicketServiceProvider::class,
  • Creating migrations :

We need to create a table to store new ticket data. Create a folder Migrations inside Modules/Ticket. Now we can create a new migration from terminal shell :

php artisan make:migration create_tickets_table --path=Modules/Ticket/Migrations

It will create a new migration class inside Modules/Ticket/Migrations path. Let's add the table script :

<?php

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

class CreateTicketsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tickets', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title')->nullable();
            $table->text('body')->nullable();
            $table->boolean('is_closed')->default(0);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('tickets');
    }
}

However, you must have noticed that this is not coming from Laravel's default database/migrations folder. We need to inform laravel to load this file as well for running migration. It can be done from service provider by adding following in boot method.

<?php

namespace Modules\Ticket;

use Illuminate\Support\ServiceProvider as BaseServiceProvider;

class TicketServiceProvider extends BaseServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot() {

        // Load ticket module migrations from 
        // `Modules/Ticket/Migrations` folder path
        $this->loadMigrationsFrom(__DIR__.'/Migrations');
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register() {}
}

Now to run the migration from terminal shell :

php artisan migrate
  • Alternatively:

If you are thinking migrations are done very rarely, so why should we register it in service provider for a registration overhead? Laravel has got you covered.

You can skip the step of doing loadMigrationsFrom() in service provider class. Instead you can specify the path while running migrations :

php artisan migrate --path=Modules/Ticket/Migrations

If your project has CICD deployments, mostly it will just have the migrate command without the path option. The first method is preferable to keep it simple for deployments.

  • Creating Model :

Create a file Ticket.php inside Modules/Ticket. (You may create a Models folder inside Modules/Ticket and create model class inside it as per your preference.)

<?php

namespace App\Modules\Ticket;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class Ticket extends Model
{
    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 'tickets';

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'title',
        'body',
        'is_closed'
    ];

}
  • Creating Controller :

Create a new directory inside Modules/Ticket called Http. Let's create TicketController.php inside it :

<?php

namespace Modules\Ticket\Http;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Modules\Ticket\Ticket;

class TicketController extends Controller
{
    /**
     * Display a form for the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
        return view('tickets.create');
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  Request  $request
     * @param  UserRepository $users
     * 
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $request->validate([
            'title' => 'string|required|max:255',
            'body'  => 'string|required'
        ]);

        // Create new ticket
        $ticket = Ticket::create($request->only('title', 'body'));

        return redirect()->route('ticket.new')->withSuccess('Thank you for raising a ticket. We will get back to you at the earliest.');
    }
}
  • Creating Routes :

Create a new file called TicketRoutes.php inside Modules/Ticket.

<?php 

Route::group(['namespace' => 'Modules\Ticket', 'middleware' => ['web', 'auth']], function () {

    Route::get('ticket/new', [ 'as' => 'ticket.new', 'uses' => 'TicketController@create' ]);
    Route::post('ticket/store', [ 'as' => 'ticket.store', 'uses' => 'TicketController@store' ]);
});

We need to tell laravel to register these routes as those are coming from a custom folder. We can do that from service provider :

<?php

namespace Modules\Ticket;

use Illuminate\Support\ServiceProvider as BaseServiceProvider;

class TicketServiceProvider extends BaseServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot() {

        // Load ticket module migrations from 
        // `Modules/Ticket/Migrations` folder path
        $this->loadMigrationsFrom(__DIR__.'/Migrations');

        // Load routes
        $this->loadRoutesFrom(__DIR__.'/TickerRoutes.php');
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register() {}
}
  • Creating Views :

Create a new directory inside Modules/Ticket called Views. This will contain al views for our module. This is again not coming from Laravel's default resources/views folder. We need to tell laravel to load these views. It can be done from service provider by adding following in boot method.

<?php

namespace Modules\Ticket;

use Illuminate\Support\ServiceProvider as BaseServiceProvider;

class TicketServiceProvider extends BaseServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot() {

        // Load ticket module migrations from 
        // `Modules/Ticket/Migrations`
        $this->loadMigrationsFrom(__DIR__.'/Migrations');

        // Load routes from `Modules/Ticket/TickerRoutes.php`
        $this->loadRoutesFrom(__DIR__.'/TickerRoutes.php');

        // Load views from `Modules/Ticket/Views`
        $this->loadViewsFrom(__DIR__.'/Views', 'ticket');
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register() {}
}

Note : Sometimes you may see loadViewsFrom() called without the second argument which is the package name. The difference is if you want to render a view without a package name you do ticket.create (file path : Modules/Ticket/Views/ticket/create.blade.php. If you specify package name, you do ticket::create (file path : Modules/Ticket/Views/create.blade.php)

Let's create a simple view create.blade.php which basically has a form :

@extends('layouts.app')

@section('content')
    <div class="panel panel-default">
        <div class="panel-body">
            <div class="col-md-4 col-md-offset-4">
                <form role="form" method="POST" action="{{ route('ticket.store') }}">
                    @csrf
                    <div class="form-group">
                        <label>Title *</label>
                        <input class="form-control" placeholder="Title for your ticket" id="title" name="title">
                        @if ($errors->has('title'))
                            <span class="invalid-feedback" role="alert">
                                <strong>{{ $errors->first('title') }}</strong>
                            </span>
                        @endif
                    </div>
                    <div class="form-group">
                        <label>Description *</label>
                        <textarea class="form-control" placeholder="How can we assist you?" id="body" name="body"></textarea>
                        @if ($errors->has('body'))
                            <span class="invalid-feedback" role="alert">
                                <strong>{{ $errors->first('body') }}</strong>
                            </span>
                        @endif
                    </div>
                    <button type="submit" class="btn btn-primary">Submit Ticket</button>
                </form>
            </div>
        </div>
    </div>
@endsection

  • Creating Policies :

If you would like to use policy to authorize the requests, create a file TicketPolicy.php inside Modules/Ticket.

<?php

namespace App\Modules;

use App\User;


class TicketPolicy
{

    /**
     * Determines user access
     *
     * @param User $user
     * @return bool
     */
    public function index($user)
    {
        return true;
    }

    /**
     * Determines user access
     *
     * @param User $user
     * @return bool
     */
    public function store($user)
    {
        return true;
    }
}

As you must have thought by now, we need to register the policy using the laravel Gate contract inside service provider :

<?php

namespace Modules\Ticket;

use Modules\Ticket\Ticket.php;
use Modules\Ticket\TicketPolicy.php;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Support\ServiceProvider as BaseServiceProvider;

class TicketServiceProvider extends BaseServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot(GateContract $gate) {

        // Load ticket module migrations from 
        // `Modules/Ticket/Migrations`
        $this->loadMigrationsFrom(__DIR__.'/Migrations');

        // Load routes from `Modules/Ticket/TickerRoutes.php`
        $this->loadRoutesFrom(__DIR__.'/TickerRoutes.php');

        // Load views from `Modules/Ticket/Views`
        $this->loadViewsFrom(__DIR__.'/Views', 'ticket');

        // Load the policies from `Modules/Ticket/TicketPolicy.php`
        $gate->policy(Ticket::class, TicketPolicy::class);
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register() {}
}
  • Creating config files :

We can have config files to access environmental variables specific for this module. For example in this ticket module let's say we need to configure an email to cc to. Create a file TicketConfig.php inside inside Modules/Ticket.

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Email to cc for submitted tickets
    |--------------------------------------------------------------------------
    |
    | Type : string
    |
    */

    'ticket_cc_email' => env('TICKET_CC_EMAIL', null),

];

Now, let's register this inside service provider.

<?php

namespace Modules\Ticket;

use Modules\Ticket\Ticket.php;
use Modules\Ticket\TicketPolicy.php;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Support\ServiceProvider as BaseServiceProvider;

class TicketServiceProvider extends BaseServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot(GateContract $gate) {

        // Load ticket module migrations from 
        // `Modules/Ticket/Migrations`
        $this->loadMigrationsFrom(__DIR__.'/Migrations');

        // Load routes from `Modules/Ticket/TickerRoutes.php`
        $this->loadRoutesFrom(__DIR__.'/TickerRoutes.php');

        // Load views from `Modules/Ticket/Views`
        $this->loadViewsFrom(__DIR__.'/Views', 'ticket');

        // Load the policies from `Modules/Ticket/TicketPolicy.php`
        $gate->policy(Ticket::class, TicketPolicy::class);

        // Load the configurations from `Modules/Ticket/TicketConfig.php`
        $this->mergeConfigFrom(
            __DIR__.'/TicketConfig.php', 'ticket';
        );
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register() {}
}

We can then access the configurations as :

$ccEmail = config('ticket.ticket_cc_email');

Now you can see entire module is bundled to a single folder. Easy to manage. There is a downside of registrations inside service provider class which in general you do not worry about. However, this is structurally more intuitive and re-usable.

If you would like your module to have more extensive and detailed structure, you can simply use this ready composer plugin : nwidart/laravel-modules

Note : The structure and coding style are my personal opinions. There can be multiple ways to accomplish the same result. I feel just knowing the possibility that it can be done, opens new doors of imaginations based on personal comfort.

 
 
By : Mihir Bhende Categories : laravel, php Tags : laravel, php, module, bundle