LoginSignup
38
29

More than 5 years have passed since last update.

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

Posted at

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

モノレポとは

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

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

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

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

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

ちなみに、モノレポはPHPに限った戦略ではないが、PHP界隈ではSymfonyLaravelもこのモノレポ戦略を採用している。

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

MonorepoBuilderとは

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

これらを手作業で行うのは手間だが、それを代わりにやってくれるのがSymplify/MonorepoBuilderである。

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

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

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に依存する

すべてのパッケージに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.jsonphpunit/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.jsonrequireにそれぞれ別のパッケージが追加されているはずだ:

$ 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"
    }
}

ここまで行うと各パッケージにvendorcomposer.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.jsonrequireなどをマージしたモノレポのcomposer.jsonを生成する:

./vendor/bin/monorepo-builder merge

するとcomposer.jsonの中身は次のように、各パッケージのrequirerequire-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に追加する:

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

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
38
29
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38
29