Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
September 21, 2021 09:43 pm GMT

Hacking Laravel to achieve modularity

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 this:

php artisan make:model --module billing --app 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 let him know how to discover what's in it. 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));        }    }}

Let's not forget to 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.phpbilling/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_tableMigrated:  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


Original Link: https://dev.to/bdelespierre/very-very-simple-laravel-modules-4927

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To