先日、PHP カンファレンス沖縄 2021 にて「自分たちのコードを Composer パッケージに分割して開発する」というテーマで発表しました。
プロポーザル
スライド
www.slideshare.net
発表資料作成の過程で、Composer において Packagist 以外(GitHub リポジトリ、ローカルのファイル)からパッケージを取得する方法もまとめていました。 しかし発表で伝えたかった主題はそこではないため、最終版ではその説明を大幅に簡略化する判断を下したのでした。
そこで、お蔵入りとなった部分を改めてブログ記事として公開しようと思います。
- Composer とは
- Composer パッケージの作り方
- リポジトリとは
- GitHub リポジトリを直接 Composer パッケージとして扱う
- ローカルのディレクトリを Composer パッケージとして扱う
- その先の話
Composer とは
Composer とは、PHP における依存関係管理ツールのデファクトスタンダードであり、例えば Web アプリケーションフレームワークでも
- Laravel
- CakePHP
- Symfony
- CodeIgniter
- Laminas
- Yii
といった多くのフレームワークで使われています。
そのプロジェクトが依存しているライブラリ(パッケージ)とそのバージョンを明示し、管理のためのインターフェース(コマンド)を提供しています。
{ "require": { "monolog/monolog": "2.0.*" } }
普段特に意識せずに Composer を使っている場合、依存するパッケージは Packagist からインストールされます。
実はこれはデフォルトの挙動であって、設定によって Packagist 以外の場所からのインストールを指定することができます。
Composer パッケージの作り方
まず前提知識として、Composer パッケージを作る方法を説明しておきましょう。
とは言ってもその方法はとても単純です。 公式ドキュメントに
As soon as you have a composer.json in a directory, that directory is a package.
と記されている通り、composer.json
ファイルが存在している時点でそのディレクトリは Composer パッケージなのです。
これは composer create-project
コマンドを実行した場合でも例外ではなく、Laravel を使って開発した Web アプリケーションなどもまた Composer パッケージなのです。
とにかく「composer.json
ファイルが存在していれば Composer パッケージ」という点を覚えておいてください。
リポジトリとは
続いて、Composer における「リポジトリ*1」という概念について解説します。
Composer におけるリポジトリについて、公式ドキュメントには次のように説明されています。
A repository is a package source. It's a list of packages/versions. Composer will look in all your repositories to find the packages your project requires.
composer install
や composer require
といったコマンドが実行されたときに、Composer がパッケージを探したり、取得元とするのがリポジトリです。
リポジトリは複数指定することができ、パッケージを探す際は順番に探していき最初に発見したものが使われます。 Packagist はデフォルトのリポジトリとして、何も設定しなくても最後に探索されます*2。
Composer uses this information to search for the right set of files in package "repositories" that you register using the repositories key, or in Packagist, the default package repository.
リポジトリを追加するにはcomposer.json
のrepositories
キーに配列*3の形で記述していきます。
以下は公式ドキュメントの記述例です。 今は詳細まで読み解く必要はありません、ざっくり雰囲気だけ掴んでおいてください。
{ "repositories": [ { "type": "composer", "url": "http://packages.example.com" }, { "type": "composer", "url": "https://packages.example.com", "options": { "ssl": { "verify_peer": "true" } } }, { "type": "vcs", "url": "https://github.com/Seldaek/monolog" }, { "type": "package", "package": { "name": "smarty/smarty", "version": "3.1.7", "dist": { "url": "https://www.smarty.net/files/Smarty-3.1.7.zip", "type": "zip" }, "source": { "url": "https://smarty-php.googlecode.com/svn/", "type": "svn", "reference": "tags/Smarty_3_1_7/distribution/" } } } ] }
repositories
キーに設定された配列内の各要素がリポジトリです。
それぞれ type
という属性を持っており、それに応じて他の属性も適宜設定されています。
GitHub リポジトリを直接 Composer パッケージとして扱う
Packagist 以外からパッケージを取得できる例として、まず、GitHub 上に Composer パッケージを用意しました。
中身はただ "Hello World." と出力する関数が定義されているだけです。
こちらのソースコードは GitHub 上に公開していますが、Packagist には登録していません。
そのため、普通に composer require
を実行してインストールしようとしてもパッケージを発見できずにエラーになってしまいます。
$ composer require okashoi/hello-world:dev-main [InvalidArgumentException] Could not find a matching version of package okashoi/hello-world. Check the package spelling, your version constraint and that the package is available in a stability whi ch matches your minimum-stability (stable).
パッケージをインストールしたいプロジェクトの composer.json
に以下を追記します。
{ ... "repositories": [ { "type": "github", "url": "https://github.com/okashoi/composer-hello-world" } ] }
この状態でcomposer require
を実行すると GitHub からパッケージを取得し、インストールに成功します。
$ composer require okashoi/hello-world:dev-main ./composer.json has been updated Running composer update okashoi/hello-world Loading composer repositories with package information Updating dependencies Lock file operations: 1 install, 0 updates, 0 removals - Locking okashoi/hello-world (dev-main 3f65e95) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 1 install, 0 updates, 0 removals - Syncing okashoi/hello-world (dev-main 3f65e95) into cache - Installing okashoi/hello-world (dev-main 3f65e95): Cloning 3f65e95130 from cache Generating autoload files
この挙動を利用することで例えば、利用しているパッケージにバグを見つけた際に「本家にバグ修正の PR を出しつつ、それがマージされるまでは fork した GitHub リポジトリのパッケージを利用する」ということができます。
ローカルのディレクトリを Composer パッケージとして扱う
先ほどの例は独立した、GitHub 上に公開された Git リポジトリを Composer パッケージとして扱ったものでした。
一方で、ローカルに存在するディレクトリを Composer パッケージとして扱う方法も存在しています。
リポジトリの type
属性を "path"
に設定すると、ローカルに存在するディレクトリをリポジトリとして指定できます。
例として、今度は以下のようなディレクトリ構造を考えます。
apps/my-app
および packages/my-package
の 2 箇所に Composer パッケージが存在しています(composer.json
の存在するディレクトリが Composer パッケージ、ということを思い出してください)。
... ├── apps │ └── my-app │ └── composer.json ├── packages │ └── my-package │ └── composer.json ...
my-app の依存関係に my-package を追加する(my-app パッケージが my-package パッケージを利用する形)には、まずapps/my-app/composer.json
を次のようにします。
{ ... "repositories": [ { "type": "path", "url": "../../packages/my-package" } ], ... }
type
属性には前述のとおり "path"
を設定し、url
属性には依存関係に追加したいパッケージ(ここでは my-package)のファイルパスを記述します。
この状態で次のコマンドを実行します。
composer require "my/package:*@dev"
この例では packages/my-package/composer.json
の name
属性に my/package
が設定してあることを想定しています。
また、バージョン(この例では *
)の後ろに指定している @dev
は、 minmum-stability
の設定を下回る stability のパッケージをインストールするための記述です(stability-flag)。
コマンドを実行した結果、 apps/my-app/composer.json
は次のように更新され、my-app パッケージから my-package パッケージを利用できるようになります。
{ ... "require": { ... "my/package": "*@dev", ... }, "repositories": [ { "type": "path", "url": "../../packages/my-package" } ], ... }
apps/my-app/vendor
下には packages/my-package
へのシンボリックリンクが作成されています。
その先の話
ここまでで「Packagist 以外からパッケージを取得する方法」の説明は終わりです。
発表ではこの先「何を、どういった基準でパッケージに分割していくのが良いのか」という話に発展していきます。 気になる方は資料を参照してください。
あるいはそれも、語りきれなかったというか、説明(と、サンプルコード)が不足している感が否めないので、気が向いたら補強してブログに書くかもしれません。