MonorepoBuilderでPHPのモノレポを作るチュートリアル

本稿はMonorepoBuilderを使った、PHPのモノレポの作り方を解説するチュートリアルです。

## モノレポとは

モノレポ(monorepo)とは「一枚岩のリポジトリ(monolithic repository)」のことで、複数のパッケージをひとつのGitリポジトリで管理するGit戦略のこと。パッケージごとにGitリポジトリを作るメニーレポ(manyrepo)の戦略をとるのが普通だが、パッケージ数が増えてくると次のような問題が顕著になる:

* パッケージごとに、自動テストやCIを設定いちいちしないとならない。
* 統一したテストや静的解析がやりずらい。
* 報告されたIssueやCIの結果は、あちこち見に行かないといけない。
* パッケージを横断した変更がやりずらい。

これらの問題点は、モノレポを採用することで解決することができる。

### モノレポ戦略をとった場合に起こる変化

* 開発がシンプルになる
  * コミットはひとつのリポジトリにするだけでよくなる
  * テストやCIは統一的にできるようになる
  * マージリクエストやIssue報告はひとつのリポジトリを見ておけば良くなる
  * テスト、CI、リリース、アクセス権設定はひとつのリポジトリで完結するようになる

[![](https://qiita-image-store.s3.amazonaws.com/0/889/b6fd1306-397c-400f-8c23-424e8c26541b.png)](https://speakerdeck.com/fabpot/a-monorepo-vs-manyrepos?slide=30)



ちなみに、モノレポはPHPに限った戦略ではないが、PHP界隈では[Symfony](https://github.com/symfony/symfony)や[Laravel](https://github.com/laravel/framework)もこのモノレポ戦略を採用している。

モノレポについての詳細は以下の記事を参照。

* [Monorepos in the Wild – Markus Oberlehner – Medium](https://medium.com/@maoberlehner/monorepos-in-the-wild-33c6eb246cb9) …
* [Why is Babel a monorepo?](https://github.com/babel/babel/blob/master/doc/design/monorepo.md#why-is-babel-a-monorepo) … なぜBabel(JSのツール)はモノレポなのか？
* [A Monorepo vs Manyrepos - Speaker Deck](https://speakerdeck.com/fabpot/a-monorepo-vs-manyrepos) … Symfonyのモノレポの話

## MonorepoBuilderとは

モノレポはメニーレポにまさる利点があるが、PHPでは各パッケージのcomposer.jsonをマージして開発用のcomposer.jsonを作る必要が出てくる。また、リリースは結局別々のGitHubリポジトリにしなければPackagistで配布できないので、パッケージごとにタグを切ったりgit pushしなければならない。

これらを手作業で行うのは手間だが、それを代わりにやってくれるのが[Symplify/MonorepoBuilder](https://github.com/Symplify/MonorepoBuilder)である。

## MonorepoBuilderでモノレポするおおまかな流れ

1. モノレポを作る (モノレポにMonorepoBuilderをインストールしておく)
1. パッケージをモノレポに入れる
1. モノレポのcomposer.jsonを自動生成する (各パッケージのcomposer.jsonが解析されマージされる)
1. モノレポ上で開発する
1. パッケージを一括リリースする (コマンド一発で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でモノレポを作る:

```shell
mkdir monorepo
cd monorepo
git init
git commit --allow-empty --message='モノレポを初期化した'
```

Githubにもモノレポのリモートリポジトリを作っておく:

```shell
hub create monorepo-example/monorepo # hubコマンドがない場合は、GitHubの画面から作る
git push -u origin master
```

モノレポに`composer.json`を作る:

```shell
composer init --no-interaction --name='monorepo-example/monorepo'
```

MonorepoBuilderをインストールする:

```shell
composer require symplify/monorepo-builder --dev
```

すると、`composer.json`の中身は次のようになっているはずだ:

```json:composer.json
{
    "name": "monorepo-example/monorepo",
    "require": {},
    "require-dev": {
        "symplify/monorepo-builder": "^5.0"
    }
}
```

MonorepoBuilderがインストールされた状態で`composer.json`をGitにチェックインする:

```shell
{ echo /vendor; echo /composer.lock } > .gitignore
git add .gitignore composer.json
git commit --message="MonorepoBuilderをインストールした"
```

下記の内容でモノレポ設定ファイル`monorepo-builder.yml`を作る。なお、このとき上でインストールした`symplify/monorepo-builder`のバージョンを合わせておく。

```yaml:
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にチェックインする:

```shell
git add monorepo-builder.yml
git commit --message='モノレポの設定ファイルYAMLを追加した'
```

以上でモノレポの基本的な設定は完了だ。

## パッケージをモノレポに入れる

### packagesディレクトリにパッケージのディレクトリを作る

モノレポで管理するパッケージを作る。今回は3つパッケージを作ってみよう。

```shell
mkdir -p packages/{package1,package2,package3}
```

### そのパッケージにcomposer.jsonを作る

それぞれに`composer.json`を作る:

```shell
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`が作られているはずだ:

```console
$ tree packages
packages
├── package1
│   └── composer.json
├── package2
│   └── composer.json
└── package3
    └── composer.json

3 directories, 3 files
```

その中身:

```json
$ 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にチェックインする:

```shell
git add packages
git commit --message='各パッケージにcomposer.jsonを作成した'
```

各パッケージにそれぞれ依存するパッケージを追加する。例として、次のパッケージを追加してみる:

* すべてのパッケージ
  * phpunit/phpunitに依存する
* 各パッケージ別々に依存するパッケージを追加する
  * package1
    * symfony/finderに依存する
  * package2
    * guzzlehttp/guzzleに依存する
  * package3
    * monolog/monologに依存する

#### すべてのパッケージに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`が追加されているはずだ:

```json
$ 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にチェックインする:

```shell
git add packages/*/composer.json
git commit --message='各パッケージにPHPUnitを追加した'
```

#### 各パッケージ別々に依存するパッケージを追加する

```shell
composer require symfony/finder    -d packages/package1
composer require guzzlehttp/guzzle -d packages/package2
composer require monolog/monolog   -d packages/package3
```

各`composer.json`の`require`にそれぞれ別のパッケージが追加されているはずだ:

```json
$ 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`に追加しておく:

```shell
echo '/packages/*/vendor' >> .gitignore
echo '/packages/*/composer.lock' >> .gitignore
```

Gitにチェックインする:

```shell
git add .gitignore packages/*/composer.json
git commit --message='各々のパッケージが依存するパッケージを追加した'
```

最後に、`vendor`などは不要なので削除しておく:

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

```shell
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`を追加しておく:

```json
$ 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`ディレクトリも作っておく:

```shell
mkdir -p packages/{package1,package2,package3}/src
```

また、適当に各パッケージにクラスを作っておく:

```bash
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 {}"
```

ディレクトリ構成は次のようになっているはずだ:

```cons
$ tree packages
packages
├── package1
│   ├── composer.json
│   └── src
│       └── Package1Class.php
├── package2
│   ├── composer.json
│   └── src
│       └── Package2Class.php
└── package3
    ├── composer.json
    └── src
        └── Package3Class.php
```

変更した`composer.json`と作ったクラスをGitにチェックインしておく:

```shell
git add packages
git commit --message='各パッケージにPSR4オートロードを設定した'
```

## モノレポの`composer.json`を自動生成する

各パッケージを作り終わったら、モノレポの`comopser.json`に各パッケージの`composer.json`の内容をマージする。これにより、開発時はモノレポの`composer.json`だけを扱えば良くなる。モノレポの`composer.json`は常に自動生成されるので直接編集してはならない。

### 各パッケージの`vendor`を削除する

各パッケージに`vendor`が残っているとうまくマージできないため、各パッケージの`vendor`ディレクトリを削除しておく必要がある。(先程の手順で`vendor`を消しているので、今回は次のような出力は出ないが、モノレポの`composer.json`を自動生成するときは必ず消しておく必要がある。)

```shell
$ 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`を生成する:

```shell
./vendor/bin/monorepo-builder merge
```

すると`composer.json`の中身は次のように、各パッケージの`require`、`require-dev`、そして`autoload`がマージされているはずだ:

```json
$ 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にチェックインしておく:

```shell
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でいつものように空っぽのリポジトリを作成する。リポジトリにコミットを作っておく必要はない。

```shell
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`に追加する:

```yamlmonorepo-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にチェックインする:

```shell
git add monorepo-builder.yml
git commit --message='GitHubリポジトリのURLをmonorepo-builder.ymlに設定した'
```

### リリースバージョンのGitタグを付ける

リリースのためにリリースバージョンをGitタグでつける。このとき`vX.Y.Z`のように`v`はじまりのセマンティックバージョンにする。

```shell
git tag v1.0.0
git push && git push --tags # リモートのモノレポに変更を反映しておく(必須ではないが)
```

### 各GitHubリポジトリにgit pushする

次のコマンドを実行することで、パッケージごとにコミットログが分解されて、各GitHubリポジトリにgit pushされる。また、リモートブランチには上でつけたリリースバージョンのタグもpushされる。

```shell
vendor/bin/monorepo-builder split
```