モジュール性を実現するための Laravel のハッキング


プロジェクトが成長するにつれて、そのモノリシックな app/ フォルダーをモジュールと呼ばれる小さなチャンクに分割することが有利になります.ご存知のように、構造化されたものを維持するために 😜

典型的なアプローチは、Laravel フォルダー構造を多くのフォルダーに複製することです.例えば

modules/
    billing/
        app/ database/ routes/
    shop/
        app/ database/ routes/
    blog/
        app/ database/ routes/


しかし、小さな問題があります.私たちが大好きな 😍 make:something コマンドは、目覚ましい生産性のために毎日頼っていますが、この構造では機能しません 🙁

恐れるな、我が友よ!今日は、この問題を解決し、make:* コマンドをモジュラー フォルダー構造でうまく機能させる方法を紹介します.

そして、コードをコピーして ⚡⚡⚡ 周りに貼り付けるわずか 5 分でそれに取り組みます 準備はできましたか?

すべての Artisan コマンドの新しいオプション



次のようなことができるようにしたいと考えています.

php artisan make:model --module billing --all Invoice


しかし、*MakeCommand 個のクラスをすべて書き直したいわけではありません. 💉 このスニペットを artisan ファイル内に直接挿入します.

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]);
}


また、56 行目に小さな変更を加える必要があります.

$status = $kernel->handle(
    $input = new Symfony\Component\Console\Input\ArgvInput($argv), // <---- add ($argv) here!
    new Symfony\Component\Console\Output\ConsoleOutput
);


新しいサービス プロバイダーの紹介



Laravel はモジュールとパッケージを処理するように作られていますが、それらを発見する方法を Laravel に伝える必要があります.そのためには、サービス プロバイダーが必要になります.

php artisan make:provider ModuleServiceProvider


次のように入力します.

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));
        }
    }
}

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,


最初のモジュールを作ってみましょう



モジュールのフォルダー構造が必要です (または、モデル クラスが modules/name/app/ のルートに生成されます).

mkdir -p modules/billing/app/Models


また、composer.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/"
        }
    }
}


後で作成する各モジュールの最後の 3 行をコピーして貼り付けます.

これで、モデルとそれに関連するクラスを作成できます.

php artisan make:model --module billing --all Invoice


結果?

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


いくつかのものを修正する



一部の make コマンドは、使用している base_path() に関係なく、正しい名前空間を生成しません (シーダー スタブの場合、ハードコードされています 🤦).彼らは単にこのように動作することを意図していませんでした.それでは、それを修正しましょう.
modules/billing/database/factories/InvoiceFactory.php で:

namespace Modules\Billing\Database\Factories; // <--- add the Modules\Billing prefix

modules/billing/database/seeders/InvoiceSeeder.php でまったく同じことを行います.

それでおしまい. php artisan migrate を実行すると、次のように表示されます.

Migrating: 2021_09_21_203852_create_invoices_table
Migrated:  2021_09_21_203852_create_invoices_table (38.79ms)


また、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,
   }


私たちのデータベースではすべてがうまく機能しているようです👌

おめでとうございます。



まあ、それはほとんどそれです.利用可能なすべてのバニラ make:* コマンドをテストしましたが、それらのほとんどは正常に動作します (もちろん、修正しなければならなかったデータベースのものを除きます).

モジュールにビュー、ルート、イベントなどが必要な場合は、make:provider コマンドを乱用することをお勧めします.

php artisan make:provider --module billing RouteServiceProvider



読んでくれてありがとう



この記事を読んで楽しんでいただければ幸いです.もしそうなら、❤️ または 🦄 を残してください.PHP、アーキテクチャ、Laravel に関する記事を毎月書いています.

免責事項 今朝シャワーを浴びているときにこのアイデアを思いつきました🚿 これの影響を完全にテストしていないので、この方法を適用する際には注意することをお勧めします.気づいたことをコメントで教えてください👍