SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

PHPカンファレンス実行委員プレゼンツ PHPの最前線

PHPパッケージ管理ツール「Composer」の処理の仕組みを見てみよう!

PHPカンファレンス実行委員プレゼンツ PHPの最前線 第4回

  • X ポスト
  • このエントリーをはてなブックマークに追加

パッケージ取得の流れ

 パッケージ取得の流れを簡略化して示すと、以下のような手順になります。

  1. 目的のパッケージの存在の確認
  2. 提供されているバージョンの確認
  3. 間接依存の解決
  4. パッケージ本体の取得・展開

 これを基本としつつ、現実的にはより複雑な処理が発生します。大規模なリポジトリでは、効率的にパッケージとバージョンを特定するための工夫も必要です。

 典型的な例として、Packagistを利用した際のシナリオについて見ていきましょう。

1. 目的のパッケージの存在の確認

 目的のパッケージが存在するかを確かめるには、まずは「リポジトリ自体の情報を取得する」ところから始まります。Packagistの場合はhttps://packagist.org/packages.jsonです。特殊な方法を用いずにブラウザ等でも閲覧可能なので、試しに開いてみてください[2]

[2] 「composer」タイプのリポジトリではpackages.jsonを参照する必要があるというのは、Composerのソースコードでは ComposerRepositoryクラスで示されています。

 packages.jsonによって、そのリポジトリに存在するパッケージ情報にアクセスが可能になるわけです。その実装を考えると、最も素朴な方法は「全てのパッケージとそのバージョンを列挙した完全なリスト提供する」というものでしょう。Composerの公式ドキュメントでは、次の例が紹介されています。

{
    "packages": {
        "vendor/package-name": {
            "dev-master": { @composer.json },
            "1.0.x-dev": { @composer.json },
            "0.0.1": { @composer.json },
            "1.0.0": { @composer.json }
        }
    }
}

 しかしながら、「完全なリスト」をいちいち取得して利用するには、Packagist上で登録されているパッケージの数が膨大すぎます[3]。 また、レポジトリ上にある利用可能なパッケージの大半は、自身のプロジェクトとは関係のない存在です。

 そこで、metadata-urlという仕組みが提供されています。パッケージの全数を取得しなくても、目的のパッケージに限定して利用可否を確かめられるようにするものです。

 Packagistのpackages.jsonには、"metadata-url": "/p2/%package%.json"と記述されています。%package%の部分を目的のパッケージ名に置換して、GETリクエストを送信します。存在すれば利用可能なバージョンごとのcomposer.jsonのリストが、存在しなければ404レスポンスが返ってくる仕様です。これによって、目的外のパッケージについては一切関与すること無く、効率的にパッケージ情報を取得できます。

[3] 2024年11月1日現在で、約41万件のパッケージが登録されています。Packagistの利用統計は https://packagist.org/statistics で公開されています。

 ここまでの流れを、実際のComposerのrequireコマンドを実行して確認してみましょう。

 -vvvオプションを付けることで、コマンド内部で行われているHTTPリクエストの情報などを細かく確認できます。

composer require --no-cache psr/log -vvv

 出力されたログを示します。本稿の目的であるパッケージ情報の取得に関わるもの以外は割愛してあります。

/app# composer require --no-cache psr/log -vvv
(中略)
Downloading https://repo.packagist.org/packages.json
[200] https://repo.packagist.org/packages.json
Downloading https://repo.packagist.org/p2/psr/log.json
[200] https://repo.packagist.org/p2/psr/log.json

 まずpackages.jsonを取得する、次に/p2/%package%.json(今回の例ではpsr/log)を取得しにいっていることが分かります。

 存在しないパッケージ名を指定した場合はどうなるでしょう?

/app# composer require --no-cache psr/_cache -vvv
(中略)
Downloading https://repo.packagist.org/packages.json
[200] https://repo.packagist.org/packages.json
Downloading https://repo.packagist.org/p2/psr/_cache.json
[404] https://repo.packagist.org/p2/psr/_cache.json
Downloading https://packagist.org/providers/psr/_cache.json
[200] https://packagist.org/providers/psr/_cache.json
Downloading https://repo.packagist.org/p2/psr/_cache~dev.json
[404] https://repo.packagist.org/p2/psr/_cache~dev.json
Downloading https://packagist.org/search.json?q=psr%2F_cache&type=
[200] https://packagist.org/search.json?q=psr%2F_cache&type=
Could not find package psr/_cache.
Pick one of these or leave empty to abort:
  [0] psr/cache
  [1] psr/cache-util
  [2] psr/simple-cache
  [3] epwt/psr-cache
  [4] viloveul/cache
 >

 metadata-urlでの解決には404応答が返ってきています。その後にproviders-urlという、packages.jsonによって情報が提供されている仕組みによる解決を試行します。更に、searchを利用して、名前が似ているパッケージの情報を提供してくれています。とても便利ですね。

 以上が、パッケージの存在の確認までの流れになります。

2. 提供されているバージョンの確認

 metadata-urlで取得されたのは、JSONファイルです。「Composer」タイプのリポジトリの情報を提供しています。必須項目はpackagesというリストの記述です。すなわち、理屈としては先述の「全てのパッケージとそのバージョンを列挙した完全なリスト提供する」ための構造を利用している訳です。

 https://repo.packagist.org/p2/guzzlehttp/psr7.jsonの中身は、次のようになっています。なお、説明のために改変し、大幅に項目を削っています。

{
  "minified": "composer/2.0",
  "packages": {
    "guzzlehttp/psr7": [
      {
        "name": "guzzlehttp/psr7",
        "version": "2.7.0",
        "version_normalized": "2.7.0.0",
        "license": [/** ..省略.. */]
        "source": {/** ..省略.. */},
        "dist": {/** ..省略.. */}
        "require": {
          "php": "^7.2.5 || ^8.0",
          "psr/http-factory": "^1.0",
          "psr/http-message": "^1.1 || ^2.0",
          "ralouphie/getallheaders": "^3.0"
        },
        "require-dev": {
          "bamarni/composer-bin-plugin": "^1.8.2",
          "http-interop/http-factory-tests": "0.9.0",
          "phpunit/phpunit": "^8.5.39 || ^9.6.20"
        },
        "provide": {
          "psr/http-factory-implementation": "1.0",
          "psr/http-message-implementation": "1.0"
        }
      },
      {
        "version": "2.6.3",
        "version_normalized": "2.6.3.0",
        "source": {/** ..省略.. */},
        "dist": {/** ..省略.. */}
      },
      {
        "version": "2.6.2",
        "version_normalized": "2.6.2.0",
        "source": {/** ..省略.. */},
        "dist": {/** ..省略.. */}
        "require-dev": {
          "bamarni/composer-bin-plugin": "^1.8.2",
          "http-interop/http-factory-tests": "^0.9",
          "phpunit/phpunit": "^8.5.36 || ^9.6.15"
        }
      },
      /** 以降、様々なバージョンが続く */
    ]
  },
  "security-advisories": [/** ..省略.. */]
}

 ベースとなるのは、元々のプロジェクトに含まれるcomposer.jsonです。各バージョンごとの情報が記載されます。これによって、「このバージョンを使うには、これらの依存が要求される」と判断できるようになっているのです。

 一方で、依存パッケージを示すrequirerequire-devフィールドが、バージョンによっては存在していないことに気づいたでしょうか? これらのフィールドは、紙面の都合で割愛したわけではなく、実際に項目が削除されています。

 これはデータ量を少しでも小さくするための工夫です。1つ前に記述されたバージョンと比較して、差分がない場合は省略されています。

 具体的に見ていくと、

  1. 2.7.0requirerequire-devを持つ
  2. 2.6.3は持たない
  3. 2.6.2require-devを持つ

ようになっています。

 これは、

  1. 2.7.0は先頭のバージョンなので、完全な情報が記述されている
  2. 2.7.02.6.3の間では変更が無かった
  3. 2.6.32.6.2の間では、require-devの変更があった

ことを意味しています。

 実際にguzzlehttp/psr7のソースコードを取得して、tagごとのcomposer.jsonのdiffを取ってみた結果は、次の通りです。

$ git --no-pager diff  2.6.3...2.7.0 composer.json |wc -l
       0
$ git --no-pager diff  2.6.2...2.6.3 composer.json |wc -l
      15
$ git --no-pager diff  2.6.2...2.6.3 composer.json
diff --git a/composer.json b/composer.json
index 70293fc..28d15f5 100644
--- a/composer.json
+++ b/composer.json
@@ -61,8 +61,8 @@
     },
     "require-dev": {
         "bamarni/composer-bin-plugin": "^1.8.2",
-        "http-interop/http-factory-tests": "^0.9",
-        "phpunit/phpunit": "^8.5.36 || ^9.6.15"
+        "http-interop/http-factory-tests": "0.9.0",
+        "phpunit/phpunit": "^8.5.39 || ^9.6.20"
     },
     "suggest": {
         "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"

 詳しい仕組みは、composer/metadata-minifierを確認してください。\Composer\MetadataMinifier\MetadataMinifier::minifyが重複情報を削る仕事の実態を担っており、\Composer\MetadataMinifier\MetadataMinifier::expandが復元を担っています[4]

[4] composer/metadata-minifierについては、バージョン1.0.0時点での実装をもとに解説しています。GitHub上のURLは https://github.com/composer/metadata-minifier/tree/1.0.0 です。

 こうして、目的とするパッケージに「バージョンは何が存在しているか」「特定のバージョンについての情報をどう入手するか」の問題が解明されました。

次のページ
パッケージ取得の流れ(2)

関連リンク

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
PHPカンファレンス実行委員プレゼンツ PHPの最前線連載記事一覧

もっと読む

この記事の著者

金城 秀樹(キンジョウ ヒデキ)

 ずっとPHPにお世話になり続けているWeb系プログラマー。趣味は積ん読とNBA観戦。最近は、本を読んだ感想を話すPodcast「readlinefm」もやっています。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/20455 2024/11/13 11:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング