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.
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,
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
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.
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'
];
}
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.');
}
}
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() {}
}
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
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() {}
}
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.
.....