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 👍
great info, you can use github.com/shunnmugam/laravel-admin
for achieving modular structure with admin feature s