ComposerのPharパッケージという選択肢

みなさんはPHPで書かれたツール類などを、どうやってインストールしているでしょうか。

まあComposerを使うのが基本ではあるのですが、 [Phive](https://phar.io/)みたいなツールもあったり、Composerを使うとしても `composer global require` を使うとか、`composer require --dev`を使うとか、[Composer bin plugin](https://github.com/bamarni/composer-bin-plugin)を使うとかの方法がありますよね。


PHPStan開発者のOndřejはこのように言っています。

https://twitter.com/OndrejMirtes/status/1541744071609405443

PHPStanのインストール方法の選択肢については別に記事を書いています。

https://zenn.dev/tadsan/articles/00bf49f8ec2f16

PHPStanについては上の記事にも書きましたが、`composer require --dev phpstan/phpstan`でインストールするだけで外部に依存性を持たない状態でパッケージをインストールできます。

## Pharファイルを見る

そもそもPHPStanのComposerパッケージはどのように実現されているのか順に見ていきましょう。

### ツールを`composer require --dev`でインストールする問題

`composer require --dev`は開発時にしか必要のない依存パッケージをインストールするためにとても便利なコマンドです。たとえばテスティングフレームワークである[PHPUnit](https://phpunit.de/)などはテストに必要なものなので、もちろん本番にデプロイする必要はないわけです。

多くのプロジェクトはツール単体で開発されているわけではなく、ツール自体も他のパッケージに依存しています。PHPUnitは不用意に外部パッケージに依存しないようになっていますが(ほとんどが自作パッケージ)、それでもゼロではありません。そのPHPUnitでさえ、開発者のSebastianはこのように言っています。

https://twitter.com/s_bergmann/status/999635212723212288

たとえばコードフォーマッターの[PHP-CS-Fixer]は多くのSymfonyコンポーネントに依存しているので、何かの事情があって古いバージョンに依存しているパッケージがインストールできないことがあります。そのようなパッケージがある場合はツールを含めた依存パッケージをまるごと同時に上げる必要が生じるために、依存解決が`require-dev`だけで完結せず、プロダクションで依存しているパッケージにも波及しがちです。

:::note alert
ツールのための依存がプロダクションと絡みあうことで、最悪の場合には静的解析もユニットテストも全てパスしているが、実はPHP-CS-Fixerがrequireしていたパッケージのクラスや関数を使ってしまっていて、本番環境にデプロイしてみると突然エラーが発生することもありえます。
:::

:::note
ところで、PHPStan・PHPUnit・PHP-CS-Fixerのような静的解析・テスティング・フォーマッターなどを含む品質管理に関わる開発支援ソフトウェア類は「**QAツール**」と総称されます。sasezakiさんの「[**このPHP QAツールがすごい！2019**](https://fortee.jp/phpcon-2019/proposal/01abf927-eb46-4708-95a1-ae05b9ac2bcf)」などを参照されると良いでしょう。
:::

### Pharとは何か

Pharファイルは複数ファイルからなるPHPプロジェクトをまるごと1ファイルに固めたアーカイブファイルです。PHPスクリプトだけでなく任意のファイルを内包できる非常に便利なものです。

https://www.php.net/manual/ja/book.phar.php

PHPで開発しているみなさんも普段から非常にお馴染みであろう[Composer](https://getcomposer.org/)も`composer.phar`というファイルで配布されています。

また、[PHPUnitなど](https://phar.phpunit.de/)もPharファイルとして配布されています。このようなファイルはComposerの依存関係(`vendor/`ディレクトリ)をまるごと同梱することで、個々のユーザーが`composer install`などで依存をかきあつめてくることなく完成品のパッケージとしてダウンロードできるのです。

こんなにも便利なPharアーカイブはPHPの標準機能で作成可能ですが、[BOX](https://github.com/box-project/box)や[PHP Autoload Builder(phpab)](https://github.com/theseer/Autoload/#readme)などを使うことでより簡単に作成できます。

Pharとして公開されているプロジェクトは[Phive(PHAR.IO)](https://phar.io/)というツールで管理できます。

:::note info
**残念ながらPhiveを使おうとしたが使いにくい…** という話は以下の記事に書きました。
この記事で取り上げているのは前述のComposer bin pluginと同じ方法を手動でやる方法です。
:::

https://tadsan.fanbox.cc/posts/328653

この方法にもまだ問題があります、PharアーカイブでインストールしたフレームワークからComposerで依存関係をインストールしたプロジェクトのコードを実行した場合は先に読み込まれたクラスが優先されてしまいます。クラス定義が重複しても動作に一貫性があれば問題なく動きますが、インターフェイスが食い違っていたり挙動に違いがあると意図しないエラーが生じます。

このような問題を避けるために、[PHP-Scoper](https://github.com/humbug/php-scoper)というツールを使うことで[**名前空間の隔離**](https://github.com/box-project/box/blob/main/doc/code-isolation.md)ができます。BoxはPHP-Scoperと統合されてますし、PHP-Scoperで名前空間を隔離してからほかのPhar構築ツールを使うこともできます。

### 単体Pharファイルの制約

PHPスクリプトのファイル群を単一ファイルで配布でき、ComposerやPHPUnitといったPHPエコシステムになくてはならない重要ツールを担うComposerですが、みなさまは普段どのようにして起動していらっしゃいますでしょうか。

 * a: `php composer.phar` のようにファイルとして実行
 * b: `composer` コマンドとして実行
    * b-1: https://getcomposer.org/download/ の説明通りにダウンロードして `/usr/local/bin` などに移動
    * b-2: Homebrewやapt-getなどのパッケージマネージャでインストール
    * b-3: Dockerfileで `COPY --from=composer /usr/bin/composer /usr/bin/composer`

などなど、いろいろな実行方法と導入方法がありますが、どれも実体は同じ`composer.phar`です。なぜこれらの方法で動くのでしょうか。

`composer.phar`をテキストエディタで開いてみると、冒頭部分が以下のような構造になっていることが確認できます。

![スクリーンショット 2023-01-07 7.26.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4121/24e27a90-4a16-d301-3764-9fadd425f717.png)

なんと、Pharファイルは冒頭部分はただのテキストによるPHPスクリプトで[^1]、[`__HALT_COMPILER()`](https://www.php.net/manual/ja/function.halt-compiler.php)よりも後の部分にバイナリが含まれています。バイナリといってもシンプルにファイルを連結したものになっています。

重要なのが `#!/usr/bin/env php` の部分です。これは[shebang](https://ja.wikipedia.org/wiki/%E3%82%B7%E3%83%90%E3%83%B3_(Unix))と呼ばれるもので、LinuxやmacOSを含むUNIX系のOSではファイルパーミッションで実行権限がついているテキストファイルの先頭に `#!` から始まる行があると、OSがここで指定された実行ファイルを使ってプログラムを起動してくれます。通常のシェルであれば単に `php` とコマンドとして実行すれば `$PATH` 環境変数に設定されたディレクトリを探索して実行ファイルを探してくれますが、shebangは `#!php` のように書いて勝手に探してくれるようなことはなく、 `#!/usr/bin/php` のようにファイルパスで指定してあげる必要があります。しかし実際には環境によってPHPの実行ファイルは `/usr/local/bin/php` だったり `$HOME/` `$HOME/local/bin/php` だったりします。

`env`は `env FOO=bar php`のように「環境変数を設定しつつコマンドを起動する」コマンドですが、環境変数の設定が0個でも `env php` として実行するだけでそのまま`$PATH`からコマンドを検索して起動します。そしてこのコマンドの実行ファイルは一般的なUNIX環境では `/usr/bin/env` に置いてあります。よって、`#!/usr/bin/env php`のように書くだけでユーザー環境の`$PATH`に沿ってコマンドが探索されるようになっています(一般的でないUNIX環境を除く[^2])。

このような仕掛けによってPHPスクリプトやPharファイルをUNIX環境での実行スクリプトとして利用可能にしているのですが、何かおかしくないでしょうか。PHPの文法では`<?php ... ?>`の外側にある文字は標準出力されるのが原則のはずです。だからこそ`<p><?= h($str) ?></p>`のようなHTMLテンプレートが書けます。

実はPHPは[phpコマンドでファイルを直接実行](https://www.php.net/manual/features.commandline.usage.php)したときだけ例外でshebangの`#!`行を読み飛ばすようになっています。そうではない場合、たとえば `cat script.php | php` として実行されたときや `include 'script.php'` で読み込まれた場合は **`#!`行がそのまま出力されてしまいます**。この制約によって、Pharファイルを単体で配布する場合は**実行スクリプト用途とincludeでライブラリとして読み込める用途を両立できません**。

そのために、PHPUnitは直接実行用の`phpunit-x.y.z.phar`とライブラリとしての読み込み用途の`phpunit-library-x.y.z.phar`に[分けて配布](https://phar.phpunit.de/)されています。

:::note warn
「**えっ、[`Phar::loadPhar()`](https://www.php.net/manual/phar.loadphar.php)を使えばいいでしょ**」という指摘は完全に正しいです。
ただ、Pharファイルをライブラリとして使うと一般的なPHPスクリプトとしては別の取り扱いになってしまいます。
:::

[^1]: 正確にはPharファイルのファイルフォーマットには「Phar形式」「zip形式」「tar形式」の三種があるのですが、この記事で紹介しているPhar形式以外を目にすることはあまりないので、これを前提としています。本文で触れられているshebangはPhar形式でしか利用できないからです
[^2]: たとえばAndroidには `/usr/bin` がありません。AndroidをUNIX環境として意識することは[Termux](https://termux.dev/en/)などを使っていない限りはないでしょうが。

### 改めてComposerのPharパッケージ

Phiveは単に使いにくいだけでなく、バージョン指定でのインストールはできるものの再帰的な依存管理の仕組みではありません。また、Pharファイルそのものを実行スクリプトにしてしまうとライブラリとしての読み込みができないという問題もあります。

これを解決するのがComposerパッケージでPharファイルを配布するというアイディアです。

現在のPackagistで[phpstan/phpstan](https://packagist.org/packages/phpstan/phpstan)として配布されているパッケージのファイル構造を見てみましょう。

```
vendor/phpstan/phpstan/
├── LICENSE ← ライセンスファイル
├── README.md ← ドキュメント(README)
├── bootstrap.php ← PHPStanをクラスとして使いたいときに読み込むファイル
├── composer.json ← Composer パッケージ設定ファイル
├── conf
│   └── bleedingEdge.neon ← bleedingEdgeを有効化するための設定ファイル
├── phpstan ← PHPStan実行スクリプト
├── phpstan.phar ← PHPStanのクラス一式がパッケージされたアーカイブ
└── phpstan.phar.asc ← PGP署名ファイル
```

:::note info
お手元のプロジェクトでPHPStanをインストールしていたら同じものが見えているはずです。
:::

そろそろお気付きでしょうか、この記事で紹介したい「**Pharパッケージ**」とは、中にPharアーカイブが入っているだけの**ただの変哲もないComposerパッケージ**です。

<https://github.com/phpstan/phpstan>はかつてPHPStanのメインリポジトリでしたが、現在はソースコードの開発は<https://github.com/phpstan/phpstan-src>に移動し、**phpstan**はissueの受け付けとPharファイルのホスティングとWebサイト・ドキュメント・ツール類の関連リソースの管理、PHPStan開発としてのPull Requestは基本的に**phpstan-src**側で受け付けています。

また、`bootstrap.php`はこのようなスクリプトになっていて、このスクリプトはComposerで自動的に読み込まれるのでPHPStanが提供するクラスをライブラリとして読み込むことで普通のPHPスクリプトからPharファイル内のクラスを普通に使えるようになっています。

https://github.com/phpstan/phpstan/blob/1.9.x/bootstrap.php

このようにユーザーにPharファイルの存在を一切意識させずに (`Phar::loadPhar()`とか知らなくても) `composer require`と`'vendor/autoload.php'`を読み込ませる(要はComposerユーザーが標準でやってること)だけで機能を提供できることが**Pharファイル単体ファイルを大きく上回るComposerを介したPharパッケージ配布の大きなメリット**です。

## Pharパッケージはどのように実現されているのか

基本的にはGitリポジトリとPackagistで公開されるComposerパッケージは一対一で対応していますが、ここで示しているPHPStanのファイル構成には<q>Webサイト・ドキュメント・ツール類の関連リソースの</q>が含まれていません。Composerはファイルを配布するときに[`.gitattributes`](https://github.com/phpstan/phpstan/blob/1.9.x/.gitattributes)を反映してくれるのでライブラリとして不必要なファイルは含まれません。

Pharファイルを管理するリポジトリはソースコードの開発リポジトリからGitHub Actionsなどで自動的にアップデートするか、コマンドで半自動的にアップデートできそうです。[phar-io/composer-distributor: Distribute PHAR files via Composer](https://github.com/phar-io/composer-distributor)のようなツールもあります。

## 2023年現在ComposerでPharパッケージ配布されているプロジェクト

| メインリポジトリ       | Pharパッケージ | 非Pharパッケージ |
|-------------------|---------------|------------|
| [PHPStan] | [phpstan/phpstan] | なし (開発のみ[phpstan-src]) |
| [Rector] | [rector/rector] | なし (開発のみ[rector-src]) |
| [PHP-CS-Fixer] | [php-cs-fixer/shim] | [friendsofphp/php-cs-fixer] |
| [Psalm] | [psalm/phar] | [vimeo/psalm] |
| [phpDocumentor] | [phpdocumentor/shim] | [phpdocumentor/phpdocumentor] |
| [Deptrac] | [qossmic/deptrac-shim] | [qossmic/deptrac] |
| [GrumPHP] | [phpro/grumphp-shim] | [phpro/grumphp] |
| [CaptainHook] | [captainhook/captainhook-phar] | [captainhook/captainhook]

[PHPStan]: https://github.com/phpstan/phpstan
[phpstan/phpstan]: https://packagist.org/packages/phpstan/phpstan
[phpstan/phpstan-shim]: https://packagist.org/packages/phpstan/phpstan-shim
[phpstan-src]: https://github.com/phpstan/phpstan-src
[Rector]: https://github.com/rectorphp/rector
[rector/rector]: https://packagist.org/packages/rector/rector
[rector-src]: https://github.com/rectorphp/rector-src
[Psalm]: https://github.com/vimeo/psalm
[psalm/phar]: https://packagist.org/packages/psalm/phar
[vimeo/psalm]: https://packagist.org/packages/vimeo/psalm
[PHP-CS-Fixer]: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer
[php-cs-fixer/shim]: https://packagist.org/packages/php-cs-fixer/shim
[friendsofphp/php-cs-fixer]: https://packagist.org/packages/friendsofphp/php-cs-fixer
[phpDocumentor]: https://github.com/phpDocumentor/phpDocumentor
[phpdocumentor/shim]: https://packagist.org/packages/phpdocumentor/shim
[phpdocumentor/phpdocumentor]: https://packagist.org/packages/phpdocumentor/phpdocumentor
[Deptrac]: https://github.com/qossmic/deptrac
[qossmic/deptrac-shim]: https://packagist.org/packages/qossmic/deptrac-shim
[qossmic/deptrac]: https://packagist.org/packages/qossmic/deptrac
[GrumPHP]: https://github.com/phpro/grumphp
[phpro/grumphp]: https://packagist.org/packages/phpro/grumphp
[phpro/grumphp-shim]: https://packagist.org/packages/phpro/grumphp-shim
[CaptainHook]: https://github.com/captainhookphp/captainhook
[captainhook/captainhook-phar]: https://packagist.org/packages/captainhook/captainhook-phar
[captainhook/captainhook]: https://packagist.org/packages/captainhook/captainhook

Pharパッケージは**shim**と呼ばれることもあります。この最初にこの形態のパッケージを「shim」と呼んだのは、おそらくPHPStan[^3]ではないかと思います<sup>[要出典]</sup>。

[^3]: 2017年に公開された[fprochazka/phpstan-compiler](https://github.com/fprochazka/phpstan-compiler)と、それを[xificurk/phpstan-shim](https://packagist.org/packages/xificurk/phpstan-shim)が再配布、それらをOndřej Mirtesが継承してPHPStan公式として発展させていったという流れがあります。

:::note warn
PHPStanはかつて[phpstan/phpstan-shim]というパッケージに分離されていましたが、2019年にリリースされたPHPStan 0.12.0以降は[phpstan/phpstan]がデフォルトでPharパッケージ化され、実装コードが[phpstan-src]に移動しました。
:::

:::note warn
Psalmの[psalm/phar]は長らくPHP 8.1との互換性の問題があり実用できない状態が続いていたのですが、[2022年11月末に修正](https://github.com/vimeo/psalm/pull/8770)されたので現在は問題なくなっていると思います。
:::

:::note warn
[Deployer](https://github.com/deployphp/deployer)のようにdistパッケージが廃止されて[Pharファイル配布に回帰](https://github.com/deployphp/deployer/pull/3051)したプロジェクトもあるようです。ただこれは[名前空間/autoloadの問題も提起](https://github.com/deployphp/deployer/pull/3258)されており、単に名前空間隔離のノウハウが不足しているだけに見えるので、誰かがコントリビューションすれば再分離して本格的なPharパッケージにできるのではないでしょうか。 *(私はDeployerユーザーではないので…)*
:::

## Pharパッケージの課題とこれから

上記の表にRectorを混ぜていますが、実は厳密にはPhar管理はしておらず、Composerでインストールした`vendor`ディレクトリ以下のPHPファイルを[PHP-Scoper](https://github.com/humbug/php-scoper)で名前空間を隔離して**PHPファイルのまま配布リポジトリに自動コミットする**という手法をとっています。

:::note
具体的に何をやっているのかは開発者の**Tomas Votruba氏**が書いた「[PHP 8.1で開発して7.2にダウングレードする方法](https://tomasvotruba.com/blog/how-to-develop-sole-package-in-php81-and-downgrade-to-php72/)」や「[単一リポジトリでPHP 8.1以上向けと7.2〜8.0向けのバージョンを同時リリースする方法](https://tomasvotruba.com/blog/how-to-release-php-81-and-72-package-in-the-same-repository/)」といった記事も参考になるでしょう。
上記の表に記載していませんが、同作者の[symplify/easy-coding-standard](https://github.com/symplify/easy-coding-standard)は標準リリース(PHP 8.1以上向け)では単に外部パッケージへの依存を持っていますが、7.2リリースではダウングレードと名前空間隔離を行なうという構成になっています。
:::

そもそもComposerでのパッケージバージョンの衝突の問題はPHPの名前空間は階層化されているものの空間としては実行プロセス全体でひとつのものを共有していて[JavaScriptモジュール](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Modules)のように局所的に依存できず、Composerでもパッケージにつきひとつのバージョンにしか依存できないということにあります。

じゃあパッケージごとにばらばらのバージョンに個別に依存できたりするのが良いのかというとそれはそれでめんどくさいというか`node_modules`みたいなネスト地獄でファイル数が肥大化したり[npm v3で依存をできるだけフラット化しよう](https://npm.github.io/how-npm-works-docs/npm3/how-npm3-works.html)とかいろいろな話が出てくるわけですが、PHP-Scoperのような名前空間隔離は単一名前空間の中でもツールごとのプレフィクスを付加することで擬似的に空間を分けて再現しようという手法なわけですよね。このようなものを推進していくことが良いかは議論があると思うのですが、ツールのような独立性の高いアプリケーションに限って局所的に適用される選択肢があるのは良いことなのかなと思っています。

Composerで**Pharパッケージ配布**とTomas Votruba氏が実践している**書き換えたファイル直コミット(vendor含む)配布**のどちらが良いか議論はあると思うのですが、クラスがPharファイル内にあると(現在の一般的な環境では)コードジャンプで飛んでいけないとか静的解析で辿れないとかの問題も無視はできないかなと思っていて、Pharファイル版のPHPUnitが使いにくいと感じる理由もここにあります。かといってPharのためにスタブファイル用意するのかとか考えるとうーん悩ましい。

とはいえ将来的にはPHPUnitのようなテスティングフレームワークもPharパッケージないしライブラリの名前空間隔離して配布してもらえるとやはり嬉しみもあるので、たとえば**公開APIのクラスとインターフェイスだけPHPスクリプト直配布してフレームワーク内部機能や名前空間隔離されたライブラリはPhar内に押し込める**みたいなハイブリッド構成も考えられそうです。

あとはDockerとかでホストのディレクトリをマウント(共有)するときにちっちゃいファイルが大量にあるよりもPharファイル一個の中に入れてメモリに載せちゃった方がもしかすると高速なのでは<sup>[未検証の臆測]</sup>などとも思ったりするので、一概にどちらが良いとも今は判断できなさがあります。

夢は無限大なので、PHPでのQAツール開発文化がもっと便利で使いやすく洗練されていくといいですね。