Haking with Laravel modules
Tags: laravel, php, beginners, webdev
Published: 2021-09-21 21:43:31

---
title: Haking with Laravel modules
published: true
description: This quick tutorial will walk you through a series of easy steps to make your Laravel app modular
tags: ["laravel","php","beginner","webdev"]
cover_image: https://images.unsplash.com/photo-1530257406797-b63a92f66670?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1003&q=80
---

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:

```bash
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:

```php
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:

```php
$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:

```bash
php artisan make:provider ModuleServiceProvider
```

Fill it with:

```php
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`:

```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/`):

```bash
mkdir -p modules/billing/app/Models
```

And we need to update `composer.json` as well:

```json
{
    "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:

```bash
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`:

```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.

```bash
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!](https://dev.to/bdelespierre) 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 👍