As a project grows it becomes advantageous to break down its monolythical app/
folder into smaller chunks called modules. You know, to keep things structured and stuff ๐
A typical approach is to replicate the Laravel folder structure in many folders. For Example
modules/
billing/
app/ database/ routes/
shop/
app/ database/ routes/
blog/
app/ database/ routes/
But there's a small problem. The make:something
commands that we love ๐ and rely on every day for our spectacular productivity won't work with this structure ๐
Well, fear not, my friend! Today, I'm going to show you how you can fix that problem and make make:*
commands play nicely with a modular folder structure!
And we're going to tackle that in just 5 minutes of copying & pasting code around โกโกโก Ready?
A new option for all Artisan commands
We want to be able to do something like this:
php artisan make:model --module billing --all Invoice
But we don't want to rewrite all the *MakeCommand
classes. So we're going to inject ๐ this snippet directly inside the artisan
file:
require __DIR__.'/vendor/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php'; // <--- be sure to paste AFTER this line
/*
|--------------------------------------------------------------------------
| Detect The Module Context
|--------------------------------------------------------------------------
|
| If you wish to run a given command (usually a make:something) in the
| context of a module, you may pass --module <name> as arguments. The
| following snippet will swap the base directory with the module directory
| and erase the module arguments so the command can run normally.
|
*/
if ((false !== $offset = array_search('--module', $argv)) && !empty($argv[$offset + 1])) {
$modulePath = $app->basePath("modules/{$argv[$offset + 1]}");
$app->useAppPath("{$modulePath}/app");
$app->useDatabasePath("{$modulePath}/database");
unset($argv[$offset], $argv[$offset + 1]);
}
We also need to make a small change at line 56:
$status = $kernel->handle(
$input = new Symfony\Component\Console\Input\ArgvInput($argv), // <---- add ($argv) here!
new Symfony\Component\Console\Output\ConsoleOutput
);
Introducing a new service provider
Laravel is somewhat made to handle modules and packages, but we need to tell it how to discover them. For that, we're going to need a service provider:
php artisan make:provider ModuleServiceProvider
Fill it with:
namespace App\Providers;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
class ModuleServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
/** Fixing Factory::resolveFactoryName */
Factory::guessFactoryNamesUsing(function (string $modelName) {
$namespace = Str::contains($modelName, "Models\\")
? Str::before($modelName, "App\\Models\\")
: Str::before($modelName, "App\\");
$modelName = Str::contains($modelName, "Models\\")
? Str::after($modelName, "App\\Models\\")
: Str::after($modelName, "App\\");
return $namespace . "Database\\Factories\\" . $modelName . "Factory";
});
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
foreach (glob(base_path('modules/*')) ?: [] as $dir) {
$this->loadMigrationsFrom("{$dir}/database/migrations");
$this->loadTranslationsFrom("{$dir}/resources/lang", basename($dir));
$this->loadViewsFrom("{$dir}/resources/views", basename($dir));
}
}
}
Register it in config/app.php
:
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\ModuleServiceProvider::class, // <--- here it is!
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
Let's make our first module
We need the folder structure for our module (or model classes will be generated at the root of modules/name/app/
):
mkdir -p modules/billing/app/Models
And we need to update composer.json
as well:
{
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/",
"Modules\\Billing\\App\\": "modules/billing/app",
"Modules\\Billing\\Database\\Factories\\": "modules/billing/database/factories/",
"Modules\\Billing\\Database\\Seeders\\": "modules/billing/database/seeders/"
}
}
}
You're going to copy/paste those last three lines for each module you create down the road.
Now we can make our model and its associated classes:
php artisan make:model --module billing --all Invoice
The result?
billing/app/
Http/Controllers/OrderController.php
Models/Order.php
Policies/OrderPolicy.php
billing/database/
factories/OrderFactory.php
migrations/2021_09_21_203852_create_orders_table.php
seeders/OrderSeeder.php
Fixing some stuff
Some make commands won't generate the correct namespace, no matter what base_path()
we're using (for the seeder stub, it's even hardcoded ๐คฆ). They were simply not intended to work this way. So let's fix that.
In modules/billing/database/factories/InvoiceFactory.php
:
namespace Modules\Billing\Database\Factories; // <--- add the Modules\Billing prefix
Do exactly the same in modules/billing/database/seeders/InvoiceSeeder.php
.
That's it. Now if you run php artisan migrate
, you'll see somehting like:
Migrating: 2021_09_21_203852_create_invoices_table
Migrated: 2021_09_21_203852_create_invoices_table (38.79ms)
And if you try to generate an invoice using tinker:
Psy Shell v0.10.8 (PHP 8.0.9 โ cli) by Justin Hileman
>>> Modules\Billing\App\Models\Invoice::factory()->create()
=> Modules\Billing\App\Models\Invoice {#3518
updated_at: "2021-09-21 21:32:15",
created_at: "2021-09-21 21:32:15",
id: 1,
}
Looks like everything works well in our database ๐
Congratulations, you're done!
Well, that's pretty much it. I tested all the vanilla make:*
commands available, and most of them work fine (except for the database ones we had to fix, of course.)
Now if your module needs views, routes, events etc. I suggest you abuse the make:provider
command.
php artisan make:provider --module billing RouteServiceProvider
Thanks for reading
I hope you enjoyed reading this article! If so, please leave a โค๏ธ or a ๐ฆ and consider subscribing! I write posts on PHP, architecture, and Laravel monthly.
Disclaimer I had this idea this morning taking my shower ๐ฟ I haven't thoroughly tested the implications of this, and I suggest you exert caution applying this method. Let me know in the comment what you found out ๐
Top comments (16)
Interesting idea, I am currently working on my personal website and blog API, and one of the things I am experimenting is something similar to what you've done here, complicating it for educational purposes mainly.
Here's the repository if you want to check it out.
It's in early development but I'll clean things up as I develop it further.
Hey Davor, thanks for your comment ๐
I see you've restructured Laravel's default folder structure entirely. How does that work for you?
It's been working great in the past when I had to work on a project from scratch and think of something modular like this one. I'll see how it will work in the near future, even though this repository won't grow too much, but I do plan to use these ideas for other side projects as well and see if it will be a good idea or not.
Good work! I've been using a laravel-modules package for a while now which works wonders. Registers new commands called
php artisan module-make:x Name
Thanks for your comment ๐
There are a lot of packages like this out there, which one is it?
Will play with it, looks promising, the whole idea of DDD in laravel needs more development. I really like zend's approach and this article reminded me of it
Hey @saltibarsciai thanks for you comment ๐
Glad you like it. Youโll find some more articles on DDD and Laravel on my page. Donโt forget to follow me to stay updated ๐ and as always, your input & notes are welcome ๐
Why don't you make a wrapper command to take care of the module syntax though? The way you do it would be gone the moment you upgrade the framework. I'm talking about the artisan file changes.
Hey @ltsochevdev thanks for your message ๐
I'm not sure
artisan
updates when you upgrade the framework ๐ค Anyway at this stage it's just a hack. If I see people are interessted, I may make a package out of it.You should make a package with this in order to get this in any project with just a composer require.
I love it!
Thanks for your comment ๐
Several other packages ๐ฆ already exists to deal with Laravel modules. I want to see if people are actually interested before investing time and efforts into making another one ๐
great info, you can use github.com/shunnmugam/laravel-admin
for achieving modular structure with admin feature s
Thanks for your comment ๐
This is brilliant! Thank you
I think this architectural pattern would be interesting to you:
github.com/Mahmoudz/Porto
And there is an implementation of it for Laravel:
github.com/apiato/apiato
Thanks for your comment Mohamed ๐