
弊社ではLaravel用の社内ライブラリをいくつか開発しており、そのうちの少なくとも半数近くはモノリポジトリ(モノリポ)構成で開発しています。
このやり方は、LaravelやSymfony等の有名なフレームワークでも採用されており、メリットとして、開発リポジトリが一つで済むので環境構築がラクなのと、依存関係を一元管理できるため、依存ライブラリのバージョン更新も一斉に対応することができます。
このように様々なメリットがあるモノリポ構成ですが、これを実現するには特殊な開発環境の構築やCI/CDを含めた各種設定を行う必要があって大変なうえ、あまりやる人がいないのか、解説した記事も少なめなので、安定するまでかなり苦労しました。
本記事では、そのような苦労から得たLaravelライブラリのモノリポ開発における知見を解説し、これから始めようとしている人や、現在モノリポ開発をやって困っている人向けに参考になればと思います。
目標
本記事では、 sample-lib と awesome-lib の2つのライブラリを1つのモノリポで開発し、公開可能な状態にするまでを解説します。
1. リポジトリ作成
まずはリポジトリが無いと始まらないため、GitHub上で作成します。
必要なのは、モノリポジトリ( laravel-example-monorepo )とパッケージごとの専用リポジトリ( awesome-lib 、 sample-lib )で、合計3つのリポジトリを作る必要があります。
GitHub - FOURIER-Inc/laravel-example-monorepo
Contribute to FOURIER-Inc/laravel-example-monorepo development by creating an account on GitHub.
https://github.com/FOURIER-Inc/laravel-example-monorepo
GitHub - FOURIER-Inc/awesome-lib
Contribute to FOURIER-Inc/awesome-lib development by creating an account on GitHub.
https://github.com/FOURIER-Inc/awesome-lib
GitHub - FOURIER-Inc/sample-lib
Contribute to FOURIER-Inc/sample-lib development by creating an account on GitHub.
https://github.com/FOURIER-Inc/sample-lib
モノリポなのにパッケージごとのリポジトリを作らなければいけないのは、Composer(PackagistやSatis)はリポジトリごとに1パッケージという前提があり、モノリポのように複数のパッケージがあっても個別には認識されないという問題があります。
そこで、モノリポのGitHub Actionsでコードをパッケージごとに分割し、それぞれのリポジトリにプッシュすることで、1パッケージ1リポジトリになるようにしています。
そのため、 awesome-lib と sample-lib はこれ以降なにか直接操作することはありません。
この方法はLaravelの開発と同じで、Laravelでは laravel/framework リポジトリで開発を行い、GitHub Actionsで illuminateプロジェクトにあるリポジトリ にコードを分割し、その分割された各リポジトリをPackagistが読み取って公開しています。
2. 初期構築
laravel-example-monorepo で初期構築をするため、以下のような composer.json
ファイルを作成します。
{
 "name": "fourier/laravel-example-monorepo",
 "type": "library",
 "autoload": {
 "psr-4": {
 "Fourier\\LaravelExampleMonorepo\\": "src/"
 }
 },
 "authors": [
 {
 "name": "Ryuya Hirayama",
 "email": "ryuya.hirayama@fourier.jp"
 }
 ],
 "require": {}
}
パッケージインストール
composer.json
ファイルを作成後、開発に必要なパッケージをインストールします。
composer require illuminate/support
composer require --dev orchestra/testbench symplify/monorepo-builder
パッケージ名 | 説明 |
---|---|
illuminate/support | Laravelのコード |
orchestra/testbench | Laravelライブラリの開発環境を提供するライブラリ |
symplify/monorepo-builder | モノリポ構成をするのに便利なライブラリ |
Workbench生成
インストール後、 ./vendor/bin/testbench workbench:install
を実行してworkbenchフォルダを生成します。
workbenchフォルダは、Laravelアプリケーションを再現するためのフォルダで、
実行中様々なことを確認されますが、一旦すべて Yes
と答え、 .env
ファイルは .env.example
を生成するようにします。
./vendor/bin/testbench workbench:install

 ┌ Run Workbench DevTool installation? ─────────────────────────┐
 │ Yes │
 └──────────────────────────────────────────────────────────────┘

 ┌ Prefix with `Workbench` namespace? ──────────────────────────┐
 │ Yes │
 └──────────────────────────────────────────────────────────────┘

 Added [Workbench\App\] for [./workbench/app] to Composer ...................................................... DONE
 Added [Workbench\Database\Factories\] for [./workbench/database/factories] to Composer ........................ DONE
 Added [Workbench\Database\Seeders\] for [./workbench/database/seeders] to Composer ............................ DONE
 Prepare [@workbench/app/Models] directory ..................................................................... DONE
 Prepare [@workbench/database/factories] directory ............................................................. DONE
 Prepare [@workbench/database/migrations] directory ............................................................ DONE
 Prepare [@workbench/database/seeders] directory ............................................................... DONE
 Prepare [@workbench/bootstrap] directory ...................................................................... DONE
 Prepare [@workbench/routes] directory ......................................................................... DONE
 Prepare [@workbench/resources/views] directory ................................................................ DONE
 File [@workbench/database/seeders/DatabaseSeeder.php] generated ............................................... DONE
 File [@workbench/routes/console.php] generated ................................................................ DONE
 File [@workbench/routes/web.php] generated .................................................................... DONE
 File [./testbench.yaml] generated ............................................................................. DONE

 ┌ Export '.env' file as? ──────────────────────────────────────┐
 │ .env.example │
 └──────────────────────────────────────────────────────────────┘

 File [@workbench/.env.example] generated ...................................................................... DONE

 ┌ Generate `workbench/bootstrap/app.php` file? ────────────────┐
 │ Yes │
 └──────────────────────────────────────────────────────────────┘

 File [@workbench/bootstrap/app.php] generated ................................................................. DONE

 ┌ Generate `workbench/bootstrap/providers.php` file? ──────────┐
 │ Yes │
 └──────────────────────────────────────────────────────────────┘

 File [@workbench/bootstrap/providers.php] generated ........................................................... DONE
 File [@laravel/database/database.sqlite] generated ............................................................ DONE
実行後は以下のようなフォルダ構成になります。
tree --gitignore
.
├── composer.json
├── composer.lock
├── src
├── testbench.yaml
└── workbench
 ├── app
 │ ├── Models
 │ │ └── User.php
 │ └── Providers
 │ └── WorkbenchServiceProvider.php
 ├── bootstrap
 │ ├── app.php
 │ └── providers.php
 ├── database
 │ ├── factories
 │ │ └── UserFactory.php
 │ ├── migrations
 │ └── seeders
 │ └── DatabaseSeeder.php
 ├── resources
 │ └── views
 └── routes
 ├── console.php
 └── web.php
3. モジュール作成
初期構築が完了したら、モジュールを作成していきます。
まずは、 src/modules
フォルダを作成し、その中に各モジュールのフォルダを作成します。
.
└── src
 └── Fourier
 ├── AwesomeLib
 └── SampleLib
次に、トップレベルの composer.json
にある autoload
に作成したモジュールのパスを追加します。これで各モジュール内の src
フォルダにあるPHPクラスや関数を use
文で使用することができるようになります。
{
 "autoload": {
 "psr-4": {
 "Fourier\\AwesomeLib\\": "src/Fourier/AwesomeLib/",
 "Fourier\\SampleLib\\": "src/Fourier/SampleLib/"
 }
 }
}
次に、各モジュールのフォルダ内で、以下のセットアップを行います。
PHPライブラリの基本
PHPライブラリは、 composer.json
ファイルを読み込むため、まずはこれがないとライブラリとして認識されません。
そのため、まずは以下のような compoesr.json
ファイルを作成します。作成する際は、 autoload
に先ほどトップレベルの composer.json
に追加したパスと同じ内容を書くことと、依存ライブラリを require
に追加する必要があります。
{
 "name": "Fourier/awesome-lib",
 "description": "description",
 "license": "proprietary",
 "type": "library",
 "require": {
 "php": "^8.2",
 "illuminate/support": "^12.0"
 },
 "autoload": {
 "psr-4": {
 "Fourier\\AwesomeLib\\": ""
 }
 }
}
Laravelライブラリの基本
Laravelライブラリは、 ServiceProvider を起点に以下の機能を読み込みます。
- config
- migration
- resource(blade)
- command
- lang
また、少し高度な機能として、 Facade や Macro といった機能もServiceProviderで追加します。
このサービスプロバイダは自動で読み込む機能がありますが、これは composer.json
のextraに以下のようにServiceProviderの名前を記述することで、自動読み込みの対象になります。
{
 "extra": {
 "laravel": {
 "providers": [
 "Fourier\\AwesomeLib\\AwesomeLibServiceProvider"
 ]
 }
 }
}
ここからモジュールごとに実装内容が異なってくるので、個別に解説してきます。
awesome-libの作成
awesome-libでは、マイグレーションで使用するBlueprintクラスに対しマクロを追加するライブラリを作成するのを目標とします。
マクロは created_at
と updated_at
カラムの定義とそれらのインデックスを定義するための関数を追加し、使用イメージは以下のようになります。
<?php

return new class extends Migration
{
 /**
 * Run the migrations.
 */
 public function up(): void
 {
 Schema::create('users', static function (Blueprint $table) {
 $table->id();

						// この関数を実装
 $table->timestampsWithDefault(6);
 
 // 以下の定義と同じことを上の関数1行で書ける
 $table->timestamp('created_at', 6)->useCurrent();
 $table->timestamp('updated_at', 6)->useCurrent()->useCurrentOnUpdate();

 $this->index('created_at');
 $this->index('updated_at');
 });
 }

 /**
 * Reverse the migrations.
 */
 public function down(): void
 {
 Schema::dropIfExists('users');
 }
}
このマクロを追加するServiceProviderは以下のようになります。
<?php

namespace Fourier\AwesomeLib;

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\ServiceProvider;

final class AwesomeLibServiceProvider extends ServiceProvider
{
 public function boot(): void
 {
 Blueprint::macro('timestampsWithDefault', function (
 ?int $precision = 0,
 string $created_at_column = 'created_at',
 string $updated_at_column = 'updated_at',
 ): void {
 /** @var Blueprint $this */
 $this->timestamp($created_at_column, $precision)
 ->useCurrent();

 $this->timestamp($updated_at_column, $precision)
 ->useCurrent()
 ->useCurrentOnUpdate();

 $this->index($created_at_column);
 $this->index($updated_at_column);
 });
 }
}
このクラスをawesome-libの src
フォルダ下に作成します。
作成後の awesome-lib
フォルダ構成は以下のようになっているはずです。
.
├── composer.json
└── AwesomeLibServiceProvider.php
sample-libの実装
sample-libでは、bladeファイルを共有するためのモジュールを作成します。
まずはServiceProviderを作成し、bladeファイルを読み込むように設定します。
<?php

namespace Fourier\SampleLib;

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\ServiceProvider;

final class SampleLibServiceProvider extends ServiceProvider
{
 public function boot(): void
 {
 $this->loadViewsFrom('../resources/views', 'awesome-lib'); //viewフォルダを読み込み
 }
}
loadViewsFrom
関数では、bladeファイルが置かれているフォルダの指定と名前空間の指定します。名前空間は、指定するとライブラリ使用者は {名前空間}::{ファイル名}
のようにしてbladeファイルを使用することができます。
public function get()
{
 return view('awesome-lib::index'); // awesome-libのindexを表示
}
resources/views
フォルダを作成し、動作確認用のサンプルbladeファイルを作成して完了です。
<h1>Sample lib</h1>
.
├── composer.json
├── SampleLibServiceProvider.php
└── resources
 └── views
 └── index.blade.php
4. GitHubアクションの設定
最後に、今回作成したモジュールを各リポジトリを分離する処理を行うGitHubアクションを実装します。
認証情報の準備
リポジトリを分離するには複数のリポジトリへのRead/Writeアクセス権が必要になります。
アクセス権としてPersonal access tokenを発行しても良いですが、今回はより安全な一時トークンを発行する形で行います。
一時トークンを発行するには、GitHub Appsを使用する必要があります。
GitHub Appsの作成
GitHub Appsは、GitHubの機能を拡張するためのツールアプリで、IssueやPull requests等の作成などを行うことができます。
今回はそのような用途には用いず、単純に複数のリポジトリへのRead/Writeのアクセス権のみ付与したGitHub Appsを作成します。
Permissionの設定には、以下のスクリーンショットと同じ権限を与えてください。

プライベートキーの作成
GitHub Appsを作成後、次にPrivate Keyを作成します。これはリポジトリにアクセスするための一時トークンを発行する際に使用されます。
プライベートキーは、GitHub AppsのPrivate keys欄の横にある Generate a private key をクリックすることで、作成&ダウンロードされます。

シークレット変数の登録
次に、先ほど作成したPrivate keysの中身をGitHub Actionsで使用することができるように、 laravel-example-monorepo のリポジトリ設定欄を開いて、以下のシークレット変数を登録します。
Name | Value |
---|---|
INTEGRATOR_APP_ID | GitHub AppのClient ID |
INTEGRATOR_APP_SECRET | ダウンロードしたプライベートキーの中身 |
Workflow作成
認証情報を準備できたら、次にリポジトリを分離するWorkflowを作成します。
name: Split

on:
 push:
 branches:
 - main
 paths:
 - 'src/**'

jobs:
 split:
 name: Split
 runs-on: ubuntu-latest

 steps:
 - name: Generate token
 id: app-token
 uses: actions/create-github-app-token@v2
 with:
 app-id: ${{ vars.INTEGRATOR_APP_ID }}
 private-key: ${{ secrets.INTEGRATOR_APP_SECRET }}
 owner: ${{ github.repository_owner }}
 permission-contents: write
 permission-statuses: write
 permission-pull-requests: write

 - uses: actions/checkout@v4
 with:
 fetch-depth: 0
 token: ${{ steps.app-token.outputs.token }}

 - name: Split and push each package
 run: |
 # repository list
 repos=(
 "awesome-lib:src/Modules/AwesomeLib"
 "sample-lib:src/Modules/SampleLib"
 )
 
 for entry in "${repos[@]}"; do
 repo="${entry%%:*}"
 path="${entry#*:}"
 
 echo "Processing $repo at path $path"
 
 # gitのリモートにモジュールのgitリポジトリのパスを追加
 git remote add "$repo" "https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/${{ github.repository_owner }}/${repo}.git"
 # splitsh-liteを使用して変更履歴を分離しSHA1を作成
 SHA1=$(bin/splitsh-lite --prefix="$path")
 # 分離した変更履歴をモジュールのgitリポジトリにpushする
 git push "$repo" "$SHA1:${{ github.ref }}" -f
 done


やや複雑なWorkflowですが、以下のステップで処理を行っています。
-
一時トークンを発行
リポジトリへアクセスするための一時トークンを発行します。これにGitHub Appsで作成したSecretを使用します。
-
リポジトリチェックアウト
リポジトリをチェックアウトします。この時fetch-depthを0にして、リポジトリのすべてのコミットを取得するようにしてください。
-
コードをリポジトリごとに分離
最も重要な処理をしているステップになります。
ここでは、 splitsh-lite を使用して各モジュールフォルダごとに変更履歴を元のリポジトリから分離し、分離した変更履歴をモジュールのgitリポジトリにpushする処理をしています。
このWorkflowによって、モノリポの変更が各モジュールのリポジトリに分離されます。
分離後は一般的なPHP(Laravel)ライブラリと同じ構成になっているawesome-libとsample-libリポジトリが作成されています。


後は、これをpackagistやプライベートレジストリに登録することでパッケージが公開され、誰でも利用することができるようになります。
まとめ
本記事では、ゼロからモノリポ構成のライブラリ開発について説明しました。
本記事の内容でライブラリの開発は可能ですが、テスト方法やブラウザでの確認、 laravel-ide-helper などといった開発ツールなどとの連携ができるとより効率的です。
そのため、開発編ではそのあたりを解説記事を書くため、本記事で興味が出たらぜひ読んでいただければと思います。