PSR に関する投稿を表示しています

Composer は何者か。あるいは install と update の違い。そしてオートロードの仕組み。

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

たいとるそのまま。

Composer 使って Packagist から ライブラリを入れられるけど結局何者なんだ、 install と update の違いはなんだ、つーかオートロードってどうなってんねん、などなどがよくわからなかったのでドキュメントとかソースを見た。

 

Composer とは?

Introduction - Composer

このあたりを、見ると大体解決する。

Composer "Dependency Manager for PHP"
→ PHP向けの依存関係管理システム

Ruby における Bundler や Node.js における npm のように、 PHP に対しての Composer 。

Composer 自体も PHP で書かれていて、PHP 5.3.2 以降の環境ならうまく動いてくれる。
場合によっては、コンパイルオプションの指定やモジュールの追加、コンフィグの問題などが発生するが、 Composer が判別し、警告を出してくれるのでその時に対応すれば良い。

 

依存関係管理?

作っていく PHP アプリケーション(あるいは PHP ライブラリ)が、ほかのライブラリに「依存」している場合、依存しているライブラリもセットにしないと動作しない。

仮にそのライブラリがまた別のライブラリに「依存」していたら…?
あるいは、ライブラリ A と別のライブラリ B が、ある同じライブラリ C に依存しているけど、 C と C' のように違うバージョンが対象だったら…?

などなどの非常にやっかいなライブラリ依存関係を、簡単な記述で管理できるようにしたものが依存関係管理システム。

PHP なら PEAR がある。が、今この時代では Composer のほうがよく使われている…はず。

 

Composer インストール

composerのインストールは非常に簡単。

$ curl -sS https://getcomposer.org/installer | php

この場合はカレントディレクトリに comopser.phar というファイルが作られ、これがComposerの本体になる。

どのディレクトリからでも実行できるように、どのユーザでも実行できるようにするためにはシステムにインストールする必要がある。

その際には、以下のコマンドで。

$ sudo mv composer.phar /usr/local/bin/composer
$ sudo chown root:root /usr/local/bin/composer
$ sudo chmod +x /usr/local/bin/composer

 

Composer がすること

  • composer コマンドの提供
  • 依存するライブラリについて
  • 依存関係の解決(ダウンロード&展開)
  • ライブラリ検索
  • 依存関係の追加
  • 依存関係の除去
  • オートロード用ファイルの提供

 

composer コマンドの提供

Composer をインストールすると composer というコマンドが利用できる。

※リネームしていない場合 composer.phar が実行コマンド。
実行権限が無い場合は php composer.phar XXXX のように php コマンドへの入力として実行する必要あり。

composer コマンドには、おおざっぱに以下の機能がある。

  • init
    • カレントディレクトに新しく composer.json を生成する
  • require
    • 依存関係を追加する
  • remove
    • 依存関係を削除する
  • install
    • composer.json に基づいて依存関係を解決する
    • composer.lock というファイルが存在する場合は、それに従う。
  • update
    • composer.json に基づいて依存関係を再度解決し composer.lock ファイルを更新する
  • dump-autoload
    • オートロード用ファイルを生成する
  • search
    • ライブラリを Packagist から探す
  • self-update
    • composer コマンド自体を更新する

他にも細かいところがあるので、気になったらドキュメントへ。
Command-line interface / Commands - Composer

 

composer install と composer update の違い

install

  • 7文字
  • composer.lock があればそれに従ってライブラリを導入する
  • composer.json に従ってライブラリを導入する

update

  • 6文字
  • composer.lock があっても composer.json に従ってライブラリを導入する

composer.lock は実際にインストールされているライブラリ、バージョンが記載されている。
composer.json には、このアプリケーション(ライブラリ)が一体どのライブラリ、どのバージョンに依存しているかを記載する。そのときライブラリのバージョンアップを形容できる書き方がある。

 

composer.json におけるバージョン指定の方法

詳しくはドキュメントにまとまってる。
Versions and constraints - Composer

ざっくりまとめると。

  • 固定値
    • 1.0.2
  • 範囲指定
    • > >= < <= != ||
    • 記号は書いたそのままの意味、|| は OR 。スペースで AND 。
    • >=1.0
      • 1.0 以上
    • >=1.0 <2.0
      • 1.0 以上 かつ 2.0 未満
    • >=1.0 <1.1 || >=1.2
      • (1.0 以上 かつ 1.1 未満)または 1.2 以上
  • ハイフンの範囲指定
    • 1.0 - 2.0
      • >=1.0.0 <2.1 と等価
      • 1.0 以上 かつ 2.1 未満
  • ワイルドカード
    • 1.0.*
      • >= 1.0 <1.1
      • 1.0 以上 かつ 1.1 未満
  • チルダ
    • 指定した一番小さいバージョンの変化を形容する
    • ~1.2
      • >= 1.2 <2.0
    • ~1.2.3
      • >= 1.2.3 <1.3
  • ハット
    • セマンティックバージョニングに従う = メジャーバージョンを上げない
    • ただし 1.0 未満はマイナーバージョンに合わせる
    • ^1.2.3
      • >=1.2.3 <2.0.0
    • ^0.3
      • >=0.3.0 <0.4.0

チルダ + マイナーバージョン指定にしておけば、大きな変更に殺されることはなさそう。

 

Composer の提供するオートローダーとは

composer dump-autoload を実行すると、 composer.json の内容にもとづいてオートロード用のファイルが作られる。ファイルは複数あり、以下の場所で確認できる。

  • vendor/autoload.php
  • vendor/composer/*

これらのファイルは次のような仕組みで成り立っている。ソースにコメントをつけて流れを見ていく。

<?php
// vendor/composer/autoload_real.php を読み込み、そちらに処理が移る。

require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitaf2b1a382e2c643bceff8e5e7a5cce1d::getLoader();

vendor/composer/autoload_real.php

<?php
// 余談だけどこのハッシュ値っぽいものは指定できる。
// composoer config に autoloader-suffix があればそれで、なければ md5(uniqid('', true)) が使われる。
//
// composer.json に書くなら config > autoloader-suffix になるようにする。
// composer config autoloader-suffix HogeeeHugaaa でも可。
class ComposerAutoloaderInitaf2b1a382e2c643bceff8e5e7a5cce1d
{
    // ClassLoader インスタンス
    private static $loader;

    // spl_autoload_register によって利用される ClassLoader を読み込む
    public static function loadClassLoader($class)
    {
        if ('Composer\Autoload\ClassLoader' === $class) {
            require __DIR__ . '/ClassLoader.php';
        }
    }

    public static function getLoader()
    {
        if (null !== self::$loader) {
            return self::$loader;
        }

        // ClassLoader の読み込み
        spl_autoload_register(array('ComposerAutoloaderInitaf2b1a382e2c643bceff8e5e7a5cce1d', 'loadClassLoader'), true, true);
        self::$loader = $loader = new \Composer\Autoload\ClassLoader();
        spl_autoload_unregister(array('ComposerAutoloaderInitaf2b1a382e2c643bceff8e5e7a5cce1d', 'loadClassLoader'));

        // PHP で、かつ Zend Guard Loader の拡張が有効なら autoload_static.php を利用する
        // 余談)Zned Guard Loader は Zned Optimizer によって作られたエンコードされた PHP ファイルを実行するためのもの
        // http://www.zend.com/en/products/guard/zend-optimizer-zend-loader
        $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
        if ($useStaticLoader) {
            require_once __DIR__ . '/autoload_static.php';

            call_user_func(\Composer\Autoload\ComposerStaticInitaf2b1a382e2c643bceff8e5e7a5cce1d::getInitializer($loader));
        } else {
            // 条件に該当しなかった場合は、各ファイルを個別に読み込む

            // 名前空間の定義がされたものを読み込み
            $map = require __DIR__ . '/autoload_namespaces.php';
            foreach ($map as $namespace => $path) {
                $loader->set($namespace, $path);
            }

            // PSR-4 形式で定義されたものを読み込み
            $map = require __DIR__ . '/autoload_psr4.php';
            foreach ($map as $namespace => $path) {
                $loader->setPsr4($namespace, $path);
            }

            // 直にクラスが指定されているものを読み込み
            $classMap = require __DIR__ . '/autoload_classmap.php';
            if ($classMap) {
                $loader->addClassMap($classMap);
            }
        }

        // 読み込んだファイル群を spl_autoload_register に登録する
        $loader->register(true);

        return $loader;
    }
}

vendor/composer/ClassLoader.php (抜粋)

// spl_autoload_register を使って登録する
public function register($prepend = false)
{
    spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}


// spl_autoload_register によって呼ばれるメソッド
// ファイルが見つかれば include する
public function loadClass($class)
{
    if ($file = $this->findFile($class)) {
        includeFile($file);

        return true;
    }
}


// オードロード要求のクラスが含まれるファイルを探す
public function findFile($class)
{
    // classmap ファイルから読み込んだリストにいれば、それを返す
    if (isset($this->classMap[$class])) {
        return $this->classMap[$class];
    }

    // 「ファイルが見つからなかった」がキャッシュされていれば当然見つからない
    // あるいは dump-autoload のときに classmap-authoritative が指定されていれば classmap だけにする
    if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
        return false;
    }

    // APCu による最適化が有効になっていれば APCu から読み込めるか試みる
    if (null !== $this->apcuPrefix) {
        $file = apcu_fetch($this->apcuPrefix.$class, $hit);
        if ($hit) {
            return $file;
        }
    }

    // クラス名 .php のファイルを探す
    $file = $this->findFileWithExtension($class, '.php');

    // HHVM で動いているなら .hh ファイルを探す
    if (false === $file && defined('HHVM_VERSION')) {
        $file = $this->findFileWithExtension($class, '.hh');
    }

    // APCu 最適化が有効なら APCu にキャッシュ登録する
    if (null !== $this->apcuPrefix) {
        apcu_add($this->apcuPrefix.$class, $file);
    }

    // 「ファイルが見つからなかった」のキャッシュ
    if (false === $file) {
        // Remember that this class does not exist.
        $this->missingClasses[$class] = true;
    }

    return $file;
}


// クラス名 + 拡張子 のファイルを探すメソッド
private function findFileWithExtension($class, $ext)
{
    // PSR-4 形式のファイルを探す
    $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;

    $first = $class[0];
    if (isset($this->prefixLengthsPsr4[$first])) {
        $subPath = $class;
        while (false !== $lastPos = strrpos($subPath, '\\')) {
            $subPath = substr($subPath, 0, $lastPos);
            $search = $subPath.'\\';
            if (isset($this->prefixDirsPsr4[$search])) {
                foreach ($this->prefixDirsPsr4[$search] as $dir) {
                    $length = $this->prefixLengthsPsr4[$first][$search];
                    if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
                        return $file;
                    }
                }
            }
        }
    }

    // PSR-4 のフォールバック
    // ※ 名前空間 => ディレクトリ名 という指定をしたもの
    foreach ($this->fallbackDirsPsr4 as $dir) {
        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
            return $file;
        }
    }

    // PSR-0 形式のファイルを探す
    if (false !== $pos = strrpos($class, '\\')) {
        // namespaced class name
        $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
            . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
    } else {
        // PSR-0 は PEAR 形式のファイル名も形容するので、それを探す
        $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
    }

    if (isset($this->prefixesPsr0[$first])) {
        foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
            if (0 === strpos($class, $prefix)) {
                foreach ($dirs as $dir) {
                    if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
                        return $file;
                    }
                }
            }
        }
    }

    // PSR-0 のフォールバック
    // ※ プリフィックス => ディレクトリ名 という指定をしたもの
    foreach ($this->fallbackDirsPsr0 as $dir) {
        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
            return $file;
        }
    }

    // PSR-0 のインクルードパスの解決
    if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
        return $file;
    }

    // 見つからねーもんは見つからない。
    return false;
}


// 指定したファイルを include する
// ※これだけグローバル空間にいる
// ※includeしたものから $this や self を利用できないように、のため
function includeFile($file)
{
    include $file;
}

で、autoload_real から 読み込まれるファイルがいくつかあるけどなんやねん、という話は composer.json に書く。書き方はドキュメントに詳しくある。

The composer.json Schema - Composer

ざっくりとまとめると。

  • PSR-4
    • 名前空間でディレクトリを切る
    • プリフィックス : ディレクトリ名 として名前空間を設定できる
  • PSR-0
    • 名前空間でディレクトリを切る
    • プリフィックス : ディレクトリ名 として名前空間を設定できる
    • PSR-4 との違いは アンダースコアでも切ることができる、名前空間なしファイルが存在すること
  • classmap
    • 名前空間など PSR-0 または PSR-4 に従っていないもの
    • クラスのあるディレクトリやファイルを直に指定する
  • file
    • 細かいこととかいいから、俺はこのファイルを読み込みたいんだ

こんな感じ。 PSR-0 と PSR-4 はざっくりだけど、詳しく書くともっと長くなるのでまた。

 

オートローダーの最適化

このあたりのざっくりまとめ。

Autoloader Optimization - Composer

特にオプションなしに composer install または composer update 。 あるいは composer dump-autoload を実行すると、上記のまま処理が動く。

多数のライブラリを読み込むような巨大なフレームワーク、アプリケーションなどは、特に PSR-0 や PSR-4 のディレクトリ構成を解析して読み込む件で時間がかかってしまう。そこで、事前にオートローダーを最適化することができる。 -o オプションをつけるとよい。

$ composer dump-autoload -o

これで PSR-0 も PSR-4 も全てのオードロードの記述が classmap に変換される。 classmap になることで、クラス名からディレクトリを探しにいくことがなくなりその分高速化が見込める。

 
さらに本番環境など、ソースがもりもり変わることがなければ -a というオプションも付けるとよい。上のソース解説で出てきた classmap-authoritative のくだり。

$ composer dump-autoload -o -a

このオプションがあれば classmap に見つからないときに処理を諦める。そもそも composer dump-autoload なしに本番のソースが変わることはないだろうので、つけたほうが良い。が、可能性として、クラスが見つかりません!になってしまうことがある。

 
何かしらの都合で -a オプションはだめだー!となるなら APCu を有効にする戦略も取れる。 -a に比べて遅くはなるが、クラスが見つかりません!問題に対応しやすくなる。とはいえ APCu はメモリに乗っかってくるので、メモリに余裕があるような環境じゃないと使えない。

$ composer dump-autoload -o -apcu

上のソースを見てわかるとおり classmap-authoritative と APCu は同居しない、どちらかしか使えない。


複雑に絡み合ったひもがちょっとずつ解けていくように Composer の動きに詳しくなっていく気持ちがする。

が、ちょっとつかれたのでここでおちまい。