本稿はMonorepoBuilderを使った、PHPのモノレポの作り方を解説するチュートリアルです。
モノレポとは
モノレポ(monorepo)とは「一枚岩のリポジトリ(monolithic repository)」のことで、複数のパッケージをひとつのGitリポジトリで管理するGit戦略のこと。パッケージごとにGitリポジトリを作るメニーレポ(manyrepo)の戦略をとるのが普通だが、パッケージ数が増えてくると次のような問題が顕著になる:
- パッケージごとに、自動テストやCIを設定いちいちしないとならない。
- 統一したテストや静的解析がやりずらい。
- 報告されたIssueやCIの結果は、あちこち見に行かないといけない。
- パッケージを横断した変更がやりずらい。
これらの問題点は、モノレポを採用することで解決することができる。
モノレポ戦略をとった場合に起こる変化
- 開発がシンプルになる
- コミットはひとつのリポジトリにするだけでよくなる
- テストやCIは統一的にできるようになる
- マージリクエストやIssue報告はひとつのリポジトリを見ておけば良くなる
- テスト、CI、リリース、アクセス権設定はひとつのリポジトリで完結するようになる
ちなみに、モノレポはPHPに限った戦略ではないが、PHP界隈ではSymfonyやLaravelもこのモノレポ戦略を採用している。
モノレポについての詳細は以下の記事を参照。
- Monorepos in the Wild – Markus Oberlehner – Medium …
- Why is Babel a monorepo? … なぜBabel(JSのツール)はモノレポなのか?
- A Monorepo vs Manyrepos - Speaker Deck … Symfonyのモノレポの話
MonorepoBuilderとは
モノレポはメニーレポにまさる利点があるが、PHPでは各パッケージのcomposer.jsonをマージして開発用のcomposer.jsonを作る必要が出てくる。また、リリースは結局別々のGitHubリポジトリにしなければPackagistで配布できないので、パッケージごとにタグを切ったりgit pushしなければならない。
これらを手作業で行うのは手間だが、それを代わりにやってくれるのがSymplify/MonorepoBuilderである。
MonorepoBuilderでモノレポするおおまかな流れ
- モノレポを作る (モノレポにMonorepoBuilderをインストールしておく)
- パッケージをモノレポに入れる
- モノレポのcomposer.jsonを自動生成する (各パッケージのcomposer.jsonが解析されマージされる)
- モノレポ上で開発する
- パッケージを一括リリースする (コマンド一発でGitHubにリリースを作れる)
MonorepoBuilderの構成
MonorepoBuilderを使った場合、モノレポの構成は次のようになる:
.
├── .git ... モノレポのGit (例: https://github.com/monorepo-example/monorepo)
├── .gitignore
├── monorepo-builder.yml ... 各パッケージのcomposer.jsonを、モノレポのcomposer.jsonにマージする設定ファイル
├── composer.json ... 上の設定ファイルから自動生成される
├── packages ... パッケージ置き場
│ ├── package1 ... リリース先はhttps://github.com/monorepo-example/package1
│ │ ├── composer.json
│ │ └── src
│ ├── package2 ... リリース先はhttps://github.com/monorepo-example/package2
│ │ ├── composer.json
│ │ └── src
│ └── package3 ... リリース先はhttps://github.com/monorepo-example/package3
│ ├── composer.json
│ └── src
└── vendor ... 各パッケージが依存するライブラリをここに集まる
今回作成するモノレポのデモ
今回作成するモノレポのデモはGitHubで公開している: https://github.com/monorepo-example
モノレポを作る
モノレポはすべてのパッケージをひとつのGitリポジトリで管理する。なので、まずはモノレポをひとつ作ることから始める。
Gitでモノレポを作る:
mkdir monorepo
cd monorepo
git init
git commit --allow-empty --message='モノレポを初期化した'
Githubにもモノレポのリモートリポジトリを作っておく:
hub create monorepo-example/monorepo # hubコマンドがない場合は、GitHubの画面から作る
git push -u origin master
モノレポにcomposer.json
を作る:
composer init --no-interaction --name='monorepo-example/monorepo'
MonorepoBuilderをインストールする:
composer require symplify/monorepo-builder --dev
すると、composer.json
の中身は次のようになっているはずだ:
{
"name": "monorepo-example/monorepo",
"require": {},
"require-dev": {
"symplify/monorepo-builder": "^5.0"
}
}
MonorepoBuilderがインストールされた状態でcomposer.json
をGitにチェックインする:
{ echo /vendor; echo /composer.lock } > .gitignore
git add .gitignore composer.json
git commit --message="MonorepoBuilderをインストールした"
下記の内容でモノレポ設定ファイルmonorepo-builder.yml
を作る。なお、このとき上でインストールしたsymplify/monorepo-builder
のバージョンを合わせておく。
parameters:
# 各パッケージのcomposer.jsonから、モノレポのcomposer.jsonにマージするものを指定する。
merge_sections:
- require
- require-dev
- autoload
- autoload-dev
# 各パッケージが配置されるディレクトリを指定する。
package_directories:
- packages
# 各パッケージのcomposer.jsonから、モノレポのcomposer.jsonにマージするときに、モノレポの
# composer.jsonに追記するものを設定する。
data_to_append:
require-dev:
symplify/monorepo-builder: ^4.7 # モノレポcomposer.jsonを再生成すると消えてしまうため必要
monorepo-builder.yml
をGitにチェックインする:
git add monorepo-builder.yml
git commit --message='モノレポの設定ファイルYAMLを追加した'
以上でモノレポの基本的な設定は完了だ。
パッケージをモノレポに入れる
packagesディレクトリにパッケージのディレクトリを作る
モノレポで管理するパッケージを作る。今回は3つパッケージを作ってみよう。
mkdir -p packages/{package1,package2,package3}
そのパッケージにcomposer.jsonを作る
それぞれにcomposer.json
を作る:
composer init --name='monorepo-example/package1' -d='packages/package1' --no-interaction
composer init --name='monorepo-example/package2' -d='packages/package2' --no-interaction
composer init --name='monorepo-example/package3' -d='packages/package3' --no-interaction
各パッケージにcomposer.json
が作られているはずだ:
$ tree packages
packages
├── package1
│ └── composer.json
├── package2
│ └── composer.json
└── package3
└── composer.json
3 directories, 3 files
その中身:
$ tail -n+1 packages/*/composer.json
==> packages/package1/composer.json <==
{
"name": "monorepo-example/package1",
"require": {}
}
==> packages/package2/composer.json <==
{
"name": "monorepo-example/package2",
"require": {}
}
==> packages/package3/composer.json <==
{
"name": "monorepo-example/package3",
"require": {}
}
作成したcomposer.json
をGitにチェックインする:
git add packages
git commit --message='各パッケージにcomposer.jsonを作成した'
各パッケージにそれぞれ依存するパッケージを追加する。例として、次のパッケージを追加してみる:
- すべてのパッケージ
- phpunit/phpunitに依存する
- 各パッケージ別々に依存するパッケージを追加する
- package1
- symfony/finderに依存する
- package2
- guzzlehttp/guzzleに依存する
- package3
- monolog/monologに依存する
- package1
すべてのパッケージにphpunit/phpunitを追加する
composer require --dev phpunit/phpunit -d packages/package1
composer require --dev phpunit/phpunit -d packages/package2
composer require --dev phpunit/phpunit -d packages/package3
各composer.json
にphpunit/phpunit
が追加されているはずだ:
$ tail -n+1 packages/*/composer.json
==> packages/package1/composer.json <==
{
"name": "monorepo-example/package1",
"require": {},
"require-dev": {
"phpunit/phpunit": "^7.3"
}
}
==> packages/package2/composer.json <==
{
"name": "monorepo-example/package2",
"require": {},
"require-dev": {
"phpunit/phpunit": "^7.3"
}
}
==> packages/package3/composer.json <==
{
"name": "monorepo-example/package3",
"require": {},
"require-dev": {
"phpunit/phpunit": "^7.3"
}
}
Gitにチェックインする:
git add packages/*/composer.json
git commit --message='各パッケージにPHPUnitを追加した'
各パッケージ別々に依存するパッケージを追加する
composer require symfony/finder -d packages/package1
composer require guzzlehttp/guzzle -d packages/package2
composer require monolog/monolog -d packages/package3
各composer.json
のrequire
にそれぞれ別のパッケージが追加されているはずだ:
$ tail -n+1 packages/*/composer.json
==> packages/package1/composer.json <==
{
"name": "monorepo-example/package1",
"require": {
"symfony/finder": "^4.1"
},
"require-dev": {
"phpunit/phpunit": "^7.3"
}
}
==> packages/package2/composer.json <==
{
"name": "monorepo-example/package2",
"require": {
"guzzlehttp/guzzle": "^6.3"
},
"require-dev": {
"phpunit/phpunit": "^7.3"
}
}
==> packages/package3/composer.json <==
{
"name": "monorepo-example/package3",
"require": {
"monolog/monolog": "^1.23"
},
"require-dev": {
"phpunit/phpunit": "^7.3"
}
}
ここまで行うと各パッケージにvendor
とcomposer.lock
が作られるが、モノレポではバージョン管理しないので.gitignore
に追加しておく:
echo '/packages/*/vendor' >> .gitignore
echo '/packages/*/composer.lock' >> .gitignore
Gitにチェックインする:
git add .gitignore packages/*/composer.json
git commit --message='各々のパッケージが依存するパッケージを追加した'
最後に、vendor
などは不要なので削除しておく:
git clean -xfd packages
各パッケージのautoloadを設定する
各パッケージのcomposer.json
を変更して、autoload.psr-4
の設定を追加する。ここではディレクトリと名前空間の対応は次のようにする:
- packages/package1/src →
MonorepoExample\Package1
- packages/package2/src →
Suin\Package2
- packages/package3/src →
Suin\Package3
cat packages/package1/composer.json | jq --indent 4 '.autoload."psr-4"."MonorepoExample\\Package1\\" = "src"' | tee packages/package1/composer.json
cat packages/package2/composer.json | jq --indent 4 '.autoload."psr-4"."MonorepoExample\\Package2\\" = "src"' | tee packages/package2/composer.json
cat packages/package3/composer.json | jq --indent 4 '.autoload."psr-4"."MonorepoExample\\Package3\\" = "src"' | tee packages/package3/composer.json
各composer.json
には、次のようautoload
を追加しておく:
$ tail -n+1 packages/*/composer.json
==> packages/package1/composer.json <==
{
"name": "monorepo-example/package1",
"require": {
"symfony/finder": "^4.1"
},
"require-dev": {
"phpunit/phpunit": "^7.3"
},
"autoload": {
"psr-4": {
"MonorepoExample\\Package1\\": "src"
}
}
}
==> packages/package2/composer.json <==
{
"name": "monorepo-example/package2",
"require": {
"guzzlehttp/guzzle": "^6.3"
},
"require-dev": {
"phpunit/phpunit": "^7.3"
},
"autoload": {
"psr-4": {
"MonorepoExample\\Package2\\": "src"
}
}
}
==> packages/package3/composer.json <==
{
"name": "monorepo-example/package3",
"require": {
"monolog/monolog": "^1.23"
},
"require-dev": {
"phpunit/phpunit": "^7.3"
},
"autoload": {
"psr-4": {
"MonorepoExample\\Package3\\": "src"
}
}
}
各src
ディレクトリも作っておく:
mkdir -p packages/{package1,package2,package3}/src
また、適当に各パッケージにクラスを作っておく:
tee packages/package1/src/Package1Class.php <<< "<?php
namespace MonorepoExample\Package1;
class Package1Class {}"
tee packages/package2/src/Package2Class.php <<< "<?php
namespace MonorepoExample\Package2;
class Package2Class {}"
tee packages/package3/src/Package3Class.php <<< "<?php
namespace MonorepoExample\Package3;
class Package3Class {}"
ディレクトリ構成は次のようになっているはずだ:
$ tree packages
packages
├── package1
│ ├── composer.json
│ └── src
│ └── Package1Class.php
├── package2
│ ├── composer.json
│ └── src
│ └── Package2Class.php
└── package3
├── composer.json
└── src
└── Package3Class.php
変更したcomposer.json
と作ったクラスをGitにチェックインしておく:
git add packages
git commit --message='各パッケージにPSR4オートロードを設定した'
モノレポのcomposer.json
を自動生成する
各パッケージを作り終わったら、モノレポのcomopser.json
に各パッケージのcomposer.json
の内容をマージする。これにより、開発時はモノレポのcomposer.json
だけを扱えば良くなる。モノレポのcomposer.json
は常に自動生成されるので直接編集してはならない。
各パッケージのvendor
を削除する
各パッケージにvendor
が残っているとうまくマージできないため、各パッケージのvendor
ディレクトリを削除しておく必要がある。(先程の手順でvendor
を消しているので、今回は次のような出力は出ないが、モノレポのcomposer.json
を自動生成するときは必ず消しておく必要がある。)
$ git clean -xfd packages
Removing packages/package1/composer.lock
Removing packages/package1/vendor/
Removing packages/package2/composer.lock
Removing packages/package2/vendor/
Removing packages/package3/composer.lock
Removing packages/package3/vendor/
各パッケージのcomposer.json
をモノレポのcomposer.json
にマージする
次のコマンドを実行して、各パッケージのcomposer.json
のrequire
などをマージしたモノレポのcomposer.json
を生成する:
./vendor/bin/monorepo-builder merge
するとcomposer.json
の中身は次のように、各パッケージのrequire
、require-dev
、そしてautoload
がマージされているはずだ:
$ cat composer.json
{
"name": "monorepo-example/monorepo",
"require": {
"symfony/finder": "^4.1",
"monolog/monolog": "^1.23",
"guzzlehttp/guzzle": "^6.3"
},
"require-dev": {
"phpunit/phpunit": "^7.3",
"symplify/monorepo-builder": "^4.7"
},
"autoload": {
"psr-4": {
"MonorepoExample\\Package1\\": "packages/package1/src",
"MonorepoExample\\Package2\\": "packages/package2/src",
"MonorepoExample\\Package3\\": "packages/package3/src"
}
}
}
Gitにチェックインしておく:
git add composer.json
git commit --message='各パッケージのcomposer.jsonをモノレポのcomposer.jsonにマージした'
モノレポ上で開発する
各パッケージが依存するライブラリはモノレポのcomposer.json
に集まっているので、モノレポ上ですべてのパッケージを開発することができる。
composer updateで各パッケージが依存するライブラリを追加する
各パッケージが依存するライブラリを追加するには、モノレポのcomposer.json
に対してcomposer update
する:
composer update
ちなみに、composer show -it
すると、モノレポに各パッケージが依存するライブラリがインストールされたことが確認できる。composer show --self
でオートロードの設定も確認できる。
各パッケージを開発する
いつものようにコードを書いて、テストして、コミットする。
パッケージを一括リリースする
モノレポなのですべてのパッケージを一括してリリースできる。ここではリリースの手順を説明する。
パッケージのGitHubリポジトリを作る
パッケージをPackagistで配布するには、各パッケージごとにGitHubリポジトリが必要になるため、おのおののGitHubリポジトリを作ることになる。今回は次の3つのリポジトリをGitHubに作る:
- package1:
git@github.com:monorepo-example/package1.git
- package2:
git@github.com:monorepo-example/package2.git
- package3:
git@github.com:monorepo-example/package3.git
GitHubでいつものように空っぽのリポジトリを作成する。リポジトリにコミットを作っておく必要はない。
hub create -c -d '[READ-ONLY] Package1' monorepo-example/package1
hub create -c -d '[READ-ONLY] Package2' monorepo-example/package2
hub create -c -d '[READ-ONLY] Package3' monorepo-example/package3
モノレポ運用では、バグ報告やPull Requestはモノレポに対して行うため、GitHubのSettings→Options→Features→Issuesからチェックを外し、Issueを登録できないようにしておく。また、あとでリポジトリの説明の頭に[READ-ONLY]
と書いておく。
GitHubリポジトリのURLをmonorepo-builder.ymlに設定する
GitHubのリポジトリのURLとパッケージを対応付ける設定をmonorepo-builder.yml
に追加する:
parameters:
...
# 各パッケージのリリース先リポジトリの設定
directories_to_repositories:
packages/package1: git@github.com:monorepo-example/package1.git
packages/package2: git@github.com:monorepo-example/package2.git
packages/package3: git@github.com:monorepo-example/package3.git
この変更をGitにチェックインする:
git add monorepo-builder.yml
git commit --message='GitHubリポジトリのURLをmonorepo-builder.ymlに設定した'
リリースバージョンのGitタグを付ける
リリースのためにリリースバージョンをGitタグでつける。このときvX.Y.Z
のようにv
はじまりのセマンティックバージョンにする。
git tag v1.0.0
git push && git push --tags # リモートのモノレポに変更を反映しておく(必須ではないが)
各GitHubリポジトリにgit pushする
次のコマンドを実行することで、パッケージごとにコミットログが分解されて、各GitHubリポジトリにgit pushされる。また、リモートブランチには上でつけたリリースバージョンのタグもpushされる。
vendor/bin/monorepo-builder split