🔖

【Composer】基本的な使い方、composer.lockやオートロードに関するメモ

2023/07/22に公開

Composerってチーム業務で当たり前のように使ってたり、PHPを使う様々な文脈で当然の用にでてくるけどなんかよく分かってないなーと思ったので改めて調べてみました。
その際に、手元で実際に動かしてみたり、中身を確認することを意識してみたのでメモを残しておこうと思います。

PHPのライブラリ管理ツールであるComposer

Composer is a tool for dependency management in PHP. It allows you to declare the libraries your project depends on and it will manage (install/update) them for you. [1]

ComposerはPHPで依存関係を管理するためのツールであり、プロジェクトが依存するライブラリを宣言することで、そのライブラリを管理(インストール/更新)することができるらしい。
しかし、そもそも「PHPで依存関係を管理するとは?」「プロジェクトが依存するライブラリとは?」といった理解なのでそこから学習を進めてみようと思います。

とはいえ、手を動かさないことには理解がなかなか進まないので、まずは実際にComposerを使ってみるところから始めてみました。

とりあえずComposerが動く環境を用意する

とりあえず、phpとapacheとComposerが動くコンテナを用意しておきます。

├ Dockerfile
├ docker-compose.yml
└ html
  └ index.php
Dockerfile
FROM php:8.1-apache

# パッケージリスト更新後にパッケージをインストール
RUN apt-get update && apt-get install -y git zip unzip
COPY . /usr/src/myapp

# composerのインストール
COPY --from=composer /usr/bin/composer /usr/bin/composer
WORKDIR /usr/src/myapp

# apacheのドキュメントルートを変更
ENV APACHE_DOCUMENT_ROOT /usr/src/myapp/html
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf
RUN sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf
docker-compose.yml
version: '3'

services:
  test:
    build: .
    volumes:
      - "./:/usr/src/myapp"
    stdin_open: true
    ports:
      - 8080:80
index.php
hello

上記の構成を作ったうえで、docker compose up -d でコンテナを立ち上げ、http://localhost:8080/ で hello と出ることを確認しました。
また、docker-compose exec test composer --version でcomposerのバージョンを見てみることで、composer自体を用意できているかも確認しておきます。
(ここでのtestというのは、docker-compose.ymlで定義しているコンテナ名)

Composerの役割

例えば、ubuntu上でphpunitをインストールしようと思った場合、sudo apt -y install phpunit などを実行することになると思います。
phpunitを一つだけインストールする場合ならばこれで問題ないのですが、現実には複数のライブラリをインストールする必要があることがほとんどでしょう。
また、複数人で開発環境を共有する際にはライブラリのバージョンを統一する必要もあります。
ライブラリ毎に必要となる前提ライブラリに関してもバージョンの依存関係があったり、各ライブラリのバージョンを上げる際にはそれらもすべて対応する必要があるので、これを全て手動で行うのはかなり厳しいということが理解できると思います。

そこで、Composerにライブラリの依存関係やバージョンの管理を自動で行ってもらいたいということになるわけですが、実際にはどのように管理しているのでしょうか?
ランダムにライブラリを拾ってくるわけはないだろうし、よしなにこちらの意図を組むというエスパーを発揮されるのも恐ろしいので、何らかの定義を基にしてインストールするライブラリを決定しているはずです。

composer.jsonの作成

インストールする対象を決定している「何らかの定義」が書かれているファイルとしてcomposer.jsonというものが存在しており、ここに欲しいライブラリとそのバージョンについて記述しておくと良いということになります。

具体的には下記のようにcomposer.jsonが設置される形になります。
今回は手動でcomposer.jsonを書いて追加しました。

.
├ Dockerfile
├ docker-compose.yml
└ composer.json      # New!
composer.json
{
    "require-dev": {
        "phpunit/phpunit": "*"
    }
}

この状態で、docker compose exec test composer installを実行することでライブラリがインストールされます。
その際にインストールされるものはcomposer.jsonに記述したphpunitのみならず、phpunitを動かすために必要となる依存パッケージもすべてインストールされます。
実際にインストールされたものは以下のような構成になっており、たしかに依存パッケージが入っていることが確認できると思います。

.
├ Dockerfile
├ docker-compose.yml
├ composer.json
├ composer.lock     # New!
└ vendor            # New!
  ├ bin
  ├ composer
  ├ myclabs
  ├ nikic
  ├ phar-io
  ├ phpunit
  ├ sebastian
  └ theseer

ここで誕生しているのはcomposer.lockvendorであり、インストールされたパッケージの中身はvendorの中に入っているという認識です。
では、もう一方のcomposer.lockとは何なのでしょうか?

composer.lockについて

先ほど自分で追加したcomposer.jsonを基にcomposer installを行ったことでパッケージと共に生成されたcomposer.lockファイルですが、これはいったい何なのでしょうか?
中身を見てみると_readmeに以下のように描かれていることが確認できると思います。

    "This file locks the dependencies of your project to a known state",
    "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
    "This file is @generated automatically"

これを直訳すると「このファイルは、プロジェクトの依存関係を既知の状態にロックします、このファイルは自動生成されます」となります。
プロジェクトの依存関係を既知の状態にロックしますというのは、このファイルが存在する状態でcomposer installを実行するとcomposer.jsonではなくcomposer.lockの方を参照してパッケージをインストールしてくるということを表しています。
実際に、composer.jsonを以下の様に変更してdocker compose exec test composer installを再度実行してみると次のようにエラーが出ることが確認できます。

composer.json
{
    "require-dev": {
        "phpunit/phpunit": "*",
        "guzzlehttp/guzzle": "^6.3"
    }
}
$ docker compose exec test composer install

Installing dependencies from lock file (including require-dev)
Verifying lock file contents can be installed on current platform.
Warning: The lock file is not up to date with the latest changes in composer.json. You may be getting outdated dependencies. It is recommended that you run `composer update` or `composer update <package name>`.
- Required (in require-dev) package "guzzlehttp/guzzle" is not present in the lock file.

上のエラー文を見ると「composer.jsonとlockで定義されている内容が違うぞ」と怒られていることが分かります。
ここに書かれている通り、composer updateを実行してcomposer.lockの内容を上書きしてみましょう。
すると、vendorの中にguzzlehttpなどが増えたことが確認できると思います。

なので、composer.json, conposer.lock の役割をまとめると以下の様になることが分かります。

  • 初回のcomposer installでは.jsonを参照し、二回目以降は.lockを参照する
  • .jsonに変更が入っている場合は、composer updateによって.lockの内容を上書きできる

上記に関して嬉しいこととしては以下のような点があります。

  • チーム開発などでパッケージの依存関係に関する情報を共有したい場合に便利
  • 誰かが用意したcomposer.lockをgit等で管理し、各自のローカルでcomposer installすることにより共通の開発環境を比較的手軽に用意できる

composer requireについて

また、いちいち手動でcomposer.jsonに追記したり、composer updateをかけるのが面倒だという場合にはcomposer require パッケージ名コマンドを使うことで自動化できます。
例えば、docker compose exec test composer require monolog/monolog を実行すると、composer.jsonにmonologに関する定義が追加されていることが確認できます。

composer.json
{
    "require-dev": {
        "phpunit/phpunit": "*",
        "guzzlehttp/guzzle": "^6.3"
    },
    "require": {
        "monolog/monolog": "^3.3"
    }
}

ここで"require"となっているのは、本番環境で必要とされるパッケージ群を定義している部分だと考えて良いでしょう。
"require-dev"と書いてある方は開発環境で必要となるライブラリの定義であって、本番環境では必要なくなるものをまとめているということになります。
devの方にはcomposer require --dev <パッケージ名>としてあげることで追加することができます。

この状態で何も指定せずにcomposer installを実行すると、requirerequire-devもどちらもインストールされます。
composer install --no-devとすると、require-devの記載分を除く本番環境用のライブラリだけをインストールすることができます。

requireされるパッケージについてもう少し詳しく

そもそも、composer.jsonをディレクトリに配置した時点で、そのディレクトリは1つのパッケージになると考えて良いでしょう。
つまり、requireをプロジェクトに追加するとき、その行為自体が「他のパッケージに依存するパッケージ」を新たに作っているということになります。
上記を踏まえてプロジェクトとパッケージの違いは何なのか考えてみると、「プロジェクトは名前のないパッケージ」だと解釈できます。

少し上で具体例として書いてあるcomposer.jsonを例に考えてみましょう。
すると、これはphpunit, guzzle, monologが入ったパッケージを定義しているのと同じであることが分かります。
とはいえ、上の例ではパッケージと呼ぶには少し足りていないこともなんとなく分かるでしょう。

インストール可能なパッケージを作成するためには、このパッケージ定義に名前を付けてあげる必要があります。
たとえば、phpunitであればphpunit/phpunitとなっている部分を指定してあげる感じです。
試しに適当に名前を付けるとすると、以下のようになります。

composer.json
{
    "name": "kerokero/hello-composer",
    "require-dev": {
        "phpunit/phpunit": "*",
        "guzzlehttp/guzzle": "^6.3"
    },
    "require": {
        "monolog/monolog": "^3.3"
    }
}

この例では、プロジェクト名がkerokero/hello-composerであり、ベンダー名がkerokero、パッケージ名がhello-composerということになります。

作ったライブラリはPackagistなどのレポジトリに登録することができ、誰でも簡単に使うことができるようになります。
なので、これまでcomposer installでインストールしていたパッケージというのは、このような形で誰かが作ったものであると知っておくと良いかもしれません。

プラットフォームパッケージについて

composerはプラットフォームパッケージというものも持っています。
これはシステムにインストールされる仮想的なパッケージのことであり、具体的にはphpcurlなどがあり"php" : "^5.5 || ^7.0",などという形で指定することができます。
これらは別途誰かが作ったパッケージというよりは、仮想的にcomposerでインストールできるようにパッケージ化しているものと考えてよさそうです。
具体的な例は以下のようになります。

composer.json
{
    "require": {
        "monolog/monolog": "^3.3",
        "php": ">=5.3"
    }
}

composerでインストールするパッケージのバージョン指定について

composer.jsonでrequireできるパッケージはもちろんバージョン指定ができます。
上記のcomposer.jsonの例で見ると"guzzlehttp/guzzle": "^6.3"ってかいてある^6.3ってやつです。
^6.3はキャレット(^)といって、[6.3, 7.0) という風に指定バージョン以上かつ最新バージョン未満という指定になります。
なので、キャレットを使ってあげると^6.3の場合のバージョンは6.9~くらいにしかならないということです。
つまり、キャレットの使用によって後方互換性が維持される範囲での更新が行われることになります。

キャレットの他にはチルダ(~)やワイルドカード(*)によって指定されたり、シンプルにレンジ(<=>)やハイフン(-)で指定される場合もあります。
この辺りは調べたり、定義を理解したうえで実際に見てみるとよくわかると思います。

pharについて

pharアーカイブを使用すると、PHPアプリケーションやライブラリをひとつのファイルにまとめて配布できるようになります。
つまり、zipやtarの様に複数のPHPファイルを1つにまとめられると考えてよさそうです。
簡潔には、.pharファイルというのは上記の通り、PHPUnitなどのパッケージをまとめてアーカイブ化した現物を提供するための定義であると考えられます。

しかし、composer.jsonやlockを使ってinstallできる状態で、いったいpharを使う意味とは何なのでしょうか?
例えば、pharでまとめることがなくとも、上の方で書いたようにcomposer require --dev <パッケージ名>で開発時にのみ使う依存パッケージをインストールできます。
これは便利だし、直感的にも何をしているかわかりやすいのでこれでよさそうな気がしますよね。

多くのプロジェクトはツール単体で開発されているわけではなく、ツール自体も他のパッケージに依存しているということはこれまでの話からも理解できていると思います。
PHPUnitなども不用意に外部パッケージに依存しないようになっていて、ほとんどが自作パッケージで構成されているのですが、それでも依存はゼロではないようです。

依存パッケージのバージョン問題が引き起こす問題の例としては、PHP-CS-Fixerなどがあげられているようです。
コードフォーマッタであるPHP-CS-Fixerは多くのSymfonyコンポーネントに依存しているので、何かの事情で古いバージョンに依存しているパッケージがインストールできない場合などがあります。
このような場合、ツールを含めた依存パッケージを全て同時にバージョン管理する必要があるため、ライブラリの依存関係がrequire-devのみで解決することができずにプロダクトで使われているパッケージ全体に問題が波及してしまうこともあるわけです。
例えば、静的解析もユニットテストもすべてパスしているのに、PHP-CS-Fixerがrequireしていたパッケージのバージョンの問題で、本番環境にデプロイするとエラーが発生してしまうなどといったことがあり得るわけです。[2]

Composer自体もcomposer.pharというファイルで配布されていたり、PHPUnitなどもPharファイルとして配布されています。
これらはComposerの依存関係(vendor/ディレクトリ)を丸ごと同梱しているので、個々人がcomposer installなどで各依存をかき集めることなく完成品のパッケージをインストールできるわけです。

実際に、Composerの公式ドキュメントのインストール方法の部分を見てみると、composer-setup.phpを実行してcomposer.pharをダウンロードしてきていることが分かります。[3]

autoloadについて

Composerを使う際にも登場するワードとしてオートロードというものがあります。
オートロードが何なのかはさておき、上の方でcomposer installをした際にvendorディレクトリの中にautoload.phpが自動生成されていることに気付くかと思います。
これの中身を見てみると、require_once __DIR__ . '/composer/autoload_real.php';と書かれており、何やらcomposerの中のファイルを呼び出していることが分かります。

というわけで、どうやらオートロードという概念がcomposerを使う上で必要になりそうなことがなんとなく分かってきました。
とはいえ、このオートロードという仕組みはComposerに固有のものではなく、実はPHP自体に備わっている仕組みみたいです。[4]
なので、まずはPHPのオートロードの仕組みから見ていこうと思います。

PHP のクラスのオートローディングについて

PHPはオブジェクト指向言語でもあるため、アプリケーションで使われるクラスの定義ごとに一つのPHPソースファイルを作成していくことになります。
その際に問題になる点としては、各スクリプトの先頭に必要な読み込みを行うための長いリストを記述する必要があることが挙げられます。
オートロードを使用しない具体的な例としては、依存するクラスをrequire_once __DIR__.'../classes/A.php';などとして冒頭で呼んであげるなどがあります。

A.php
<?php

    //class A
    class A{

        public $name = "";

        public function __construct()
        {
            $this->name = "kerokero";
        }

        public function Hello()
        {
            echo $this->name."\n";
        }

    }
B.php
<?php

    require_once __DIR__.'/../classes/A.php';

    //class B
    class B extends A{

        public $email = "";

        public function Send()
        {
            echo "Send mail to ".$this->email."\n";
        }

    }
test.php
<?php

    require_once __DIR__.'/classes/B.php';

    //Main
    $b = new B();
    $b->Hello();
    $b->Send();

上の例はautoloadをしていない、クラス別にファイルを分けてrequireしているものになります。
オートロードを実施しない場合に上記のようにクラスをインスタンス化するときは、クラス定義されているファイルをrequire_onceなどで読み込んでからインスタンス化することになるという感じです。

これでも実際に動くので問題なさそうなのですが、オートロードを使ってあげると何がうれしいのかという部分を考えていきます。

autoload(オートロード)とは、簡潔には「名前空間を指定することで、自動的にクラス名を解決してくれる仕組み」のことです。
オートロード、composerに関して知るためには、名前空間だったりクラス名を解決というワードに関しても理解が必要になるので、適宜調べてみるとよさそうです。

名前空間の指定によって対応するクラス名を解決されることのメリットは、上記の例の様にインスタンス化したいクラスの書かれたファイルをrequireする必要がない点になります。
つまり、オートロードを使用しない場合はクラスの依存関係をファイルの読み込みによって制御していて、オートロードを使用する場合はクラスの指定によって操っていると解釈することができそうです。
なので、ここまでの流れでオートロードを理解しようとすると、ディレクトリのパスをたどってファイルから必要なクラスを読み込むのではなく、名前空間で定義してあるクラス名を自動で読み込んでくれる仕組みだと思ってよさそうです。

PHPのオートロードはspl_autoload_registerを使って登録されるらしく、引数としてautoload関数を与えてあげるとキューの中に追加されていき、定義された順に紐づけられていくみたいです。
実際の実装はこのあたりに描いてそうであり、引数がnullの時はspl_autoload()がデフォルトにセットされていそうなことも分かります。
キューに追加するものをautoload関数を定義したのちにこのあたりで追加していそうな匂いがします。
(C言語とphp-srcについてはほとんど理解できていないので、このあたりの実装にこれ以上深入りするのはここではやめておきます)

composerのautoloadの実装を見てみても同じようなことが言えます。
上の方でも書いたように、conposer installによって自動生成されたvendor/autoload.phpの中を見てみると、vendor/composer/autoload_real.phpをrequireしていることが分かります。
これをたどってみると、たしかにspl_autoload_registerを使ってvendor/composer/ClassLoader.phpの中のClassLoaderクラスを引数として与えていることが確認できました。
また、ClassLoaderのコンストラクタで呼ばれているinitializeIncludeClosureメソッドの中を見てみると、以下のように特定のファイルをincludeしていることも理解できます。

vendor/composer/ClassLoad.php
    /** 略 **/

    /**
     * @return void
     */
    private static function initializeIncludeClosure()
    {
        if (self::$includeFile !== null) {
            return;
        }

        /**
         * Scope isolated include.
         *
         * Prevents access to $this/self from included files.
         *
         * @param  string $file
         * @return void
         */
        self::$includeFile = \Closure::bind(static function($file) {
            include $file;  // ← こいつ
        }, null, null);
    }

$fileの中身はvendor/composer/autoload_real.phpの中で指定されていて、ComposerStaticInit<値>となっていることが分かります。
これはvendor/composer/autoload_static.phpの中身を指していて、その中を見てみると確かにcomposer.lockに書かれているものたちがpublic static $classMapとして設定されていることが確認できると思います。
一部を見てみると以下のようになっており、たしかに名前空間 => ファイルパスというようにマッピングされていることが分かります。

 'PHPUnit\\Event\\Facade' => __DIR__ . '/..' . '/phpunit/phpunit/src/Event/Facade.php',

なので、このことからもcomposerのオートロードがやっていることは「クラスが入ってるファイルのパスを名前空間に紐づけているだけ」ということがなんとなく分かったと思います。
このautoload_static.phpの$classmapの中を見ると、確かにこれだけの量のクラスをいちいちrequireする必要があると考えれば、オートロードが便利だということもなんとなく分かります。

slimで使ってみる

これまでの説明で、composerとそれに採用されているautoloadが便利なことはなんとなく分かります。
とはいえ、いまいちまだピンときていない部分もあるので、実際にSlim Frameworkのコードを見てみることで実感してみます。

上の方で使っていたcomposer.jsonにslimフレームワークを追加します。
docker compose exec test composer require slim/slimでrequireに追加し、composer updateを行いcomposer.lockにslimを入れます。
同様にしてcomposer require slim/psr7も入れます。
そして、composer installを実行すると確かにvendorディレクトリの中にslimが誕生していることが確認できます。

また、html/index.phpの中身を以下の様に書き換えておきます。

index.php
<?php
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;

require __DIR__ . '/../vendor/autoload.php';

// Instantiate App
$app = AppFactory::create();

// Add routes
$app->get('/', function (Request $request, Response $response) {
    $response->getBody()->write('hello slim');
    return $response;
});

$app->run();

http://localhost:8080/ でアクセスしてみると、たしかにslimを使って書かれた情報が返却されます。
コードを見ると/../vendor/autoload.phpをrequireしていることが分かります。
composerでオートロードされたクラスを、use Slim\Factory\AppFactory;のようにして呼び出しているということになります。

※ slimを正しく使えているかと言われたらかなり怪しく、ミドルウェアやコンテナの設定などもあるので、今回はcomposerの挙動を見てみるためだと思って怪しいところは割りきります。

PSR-0, PSR-4 について

上に書かれているSlimの例でも出てきましたが、PHPにはPSRという規約が存在しています。
PSR(PHP Standards Recommendations, PHP標準勧告)とは、PHP-FIGという団体が策定しているPHPの規約のことみたいです。

PSRという規約を定めておくことによって、別々に作られたフレームワークやライブラリ同士が相互に連携できるようになっているというわけです。
実際にPSRにどんなものがあるのかは https://www.php-fig.org/psr/ で見ることができます。

PSRについては大まかな分類が存在しており、下記の4分類で分けられていると考えてよさそうです。

  • オートローディング
  • インターフェース
  • HTTP
  • コーディングスタイル

composerにかかわってくる部分だとオートローディングの項目になり、Slimに出てきたPSR-7はHTTPにかかわる項目になります。
オートローディングにかかわるPSR-4の中身は、PHP-FIGにかいてある PSR-4: Autoloader を見てみるとよくわかります。

また、PSR-4の他に、PSR-0もオートロードの使用を制定しているものになります。PSR-0: Autoloading Standard

簡単にこれらの違いを見てみると、PSR-0は名前空間と実在ディレクトリ構造を同じにする仕様であり、PSR-4は名前空間が実在のディレクトリ構造に依存しない仕様となっているみたいです。
たとえば、PSR-0準拠の場合hoge\apple\beamという名前空間はsrc/hoge/apple/beam.phpのようにディレクトリパスに対応していないといけません。
一方、PSR-4準拠の場合はhoge\apple\beamという名前空間がsrc/lib/foo/beam.phpと紐づいていても問題ありません。
つまり、PSR-4の場合は名前空間に検索パスを紐付けしておき、名前空間からパスを、クラス名からファイル名を判断する仕様というわけです。

PSRについても、ここではあまり深堀せずにこのあたりにとどめておきます。


参考にした記事・ページ

脚注
  1. Getting Started With Composer(ドキュメント)のIntroductionより (https://getcomposer.org/doc/00-intro.md) ↩︎

  2. ComposerのPharパッケージという選択肢 (https://qiita.com/tadsan/items/c77615e39f934866c602) ↩︎

  3. Download Composer (https://getcomposer.org/download/) ↩︎

  4. クラスのオートローディング (https://www.php.net/manual/ja/language.oop5.autoload.php) ↩︎

Discussion