🤝

Laravelアプリとモジュール(アプリ依存パッケージ)の連携

に公開

本記事は、鹿児島 Advent Calendar 2025 13日目の記事です。

Laravelアプリにグルーコードを追加し、
アプリとモジュール(アプリ依存パッケージ)を連携させる方法[1]を紹介します。

前提

本記事では、以下を「モジュール(アプリ依存パッケージ)」の定義としています。

アプリに依存するようなファイル(ルーティングやコントローラなど)を含むパッケージ

Reactでのビューを含む場合もありますが、本記事では取り扱いません。

また、モジュール側のサービスプロバイダではなく、
アプリ側のサービスプロバイダでルートを読み込むことに重点を置いています。

実行環境

PHP 8.5.0
Laravel v12.42.0
Node.js v24.12.0

最終的なコード

https://github.com/inoha-kudo/advent

手順

プロジェクトの作成

Laravelアプリの雛形を作成します。

本記事では、Laravel公式が提供しているスターターキットの代わりに、
筆者がカスタマイズしたものをusingオプションで指定しています。
https://packagist.org/packages/miraiportal/dynasty-starter-kit

※ usingオプションを使用しなくても、以降の手順で支障はありません。

以下のコマンドを実行

laravel new advent --using=miraiportal/dynasty-starter-kit

Which testing framework do you prefer?

Pestを選択

Would you like to run npm install and npm run build?

Yesを選択

モジュラモノリス対応

利用するモジュールを列挙する app/Enums/Module.php と、
モジュールのルートを読み込む app/Providers/ModuleServiceProvider.php
を追加します。

app/Enums/Module.php
<?php

declare(strict_types=1);

namespace App\Enums;

use Illuminate\Support\Facades\Route;

enum Module: string
{
    public static function loadWebRoutes(): void
    {
        foreach (self::web() as $module) {
            $webRoute = $module::webRoute();
            assert(is_string($webRoute));

            Route::middleware('web')->group($webRoute);
        }
    }

    public static function loadApiRoutes(): void
    {
        foreach (self::api() as $module) {
            $apiRoute = $module::apiRoute();
            assert(is_string($apiRoute));

            Route::middleware('api')->prefix('api')->group($apiRoute);
        }
    }

    public static function loadConsoleRoutes(): void
    {
        foreach (self::console() as $module) {
            require $module::consoleRoute();
        }
    }

    /** @return class-string[] */
    private static function web(): array
    {
        return self::modulesDefining('webRoute');
    }

    /** @return class-string[] */
    private static function api(): array
    {
        return self::modulesDefining('apiRoute');
    }

    /** @return class-string[] */
    private static function console(): array
    {
        return self::modulesDefining('consoleRoute');
    }

    /** @return class-string[] */
    private static function modulesDefining(string $method): array
    {
        return array_filter(
            array_map(fn (self $module) => $module->value, self::cases()),
            fn (string $class) => is_callable([$class, $method]),
        );
    }
}
app/Providers/ModuleServiceProvider.php
<?php

declare(strict_types=1);

namespace App\Providers;

use App\Enums\Module;
use Illuminate\Support\ServiceProvider;

final class ModuleServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->loadWebRoutes();
        $this->loadApiRoutes();
        $this->loadConsoleRoutes();
    }

    private function loadWebRoutes(): void
    {
        if ($this->app->routesAreCached()) {
            return;
        }

        Module::loadWebRoutes();
    }

    private function loadApiRoutes(): void
    {
        if ($this->app->routesAreCached()) {
            return;
        }

        Module::loadApiRoutes();
    }

    private function loadConsoleRoutes(): void
    {
        if (! $this->app->runningInConsole()) {
            return;
        }

        Module::loadConsoleRoutes();
    }
}

また、以下のように bootstrap/providers.php を変更して、
上記で追加したサービスプロバイダがアプリで読み込まれるようにします。

bootstrap/providers.php
  return [
      App\Providers\AppServiceProvider::class,
+     App\Providers\ModuleServiceProvider::class,
  ];

Pingモジュールの追加

本記事では、筆者が公開しているモジュールを追加します。
https://packagist.org/packages/dynasty/ping

以下のコマンドを実行

composer require dynasty/ping

モジュールで利用するルートを定義する app/Modules/PingModule.php
を追加します。

app/Modules/PingModule.php
<?php

declare(strict_types=1);

namespace App\Modules;

final class PingModule
{
    private const string ROUTES_DIR = 'vendor/dynasty/ping/routes';

    public static function apiRoute(): string
    {
        return base_path(self::ROUTES_DIR.'/api.php');
    }

    public static function consoleRoute(): string
    {
        return base_path(self::ROUTES_DIR.'/console.php');
    }
}

最後に、以下のように app/Enums/Module.php を変更して、
利用するモジュールとしてPingモジュールを追加します。

app/Enums/Module.php

  namespace App\Enums;

+ use App\Modules\PingModule;
  use Illuminate\Support\Facades\Route;

  enum Module: string
  {
+     case Ping = PingModule::class;
+
      public static function loadWebRoutes(): void
      {
          foreach (self::web() as $module) {

動作確認

API

以下のコマンドを実行

composer run dev

http://127.0.0.1:8000/api/ping にアクセスして、
pong という文字列が返ってくることを確認

コンソール

以下のコマンドを実行

php artisan app:ping

pong という文字列が返ってくることを確認

おまけ

アプリ側からモジュール側の実装を切り替えたり

    #[\Override]
    public function register(): void
    {
        $this->app->bind(
            PingUseCase::class,
            PingCustomInteractor::class,
        );
    }

機能フラグ(Laravel Pennant)でルートの公開/非公開を切り替えたり

        foreach (self::api() as $module) {
            $apiRoute = $module::apiRoute();
            assert(is_string($apiRoute));

            Route::middleware([
                EnsureFeaturesAreActive::using($module),
                'api',
            ])->prefix('api')->group($apiRoute);
        }
脚注
  1. 別の表現をすると、
    「外部パッケージを使用せず、モジュラモノリスを限定的に実現する」
    といった内容です。 ↩︎

Discussion