includeって書きたくない僕たちのためのオートローディングとComposer

  • 49
    いいね
  • 0
    コメント

こんにちはこんにちは、PHP書いてますか? include_once してますか? それともキミは require_once 派?

ところで、現代的なPHPではクラスファイル(ここではclass, trait, interfaceを含む定義ファイル)では、わざわざファイルをinclude/requireしなくても自動的に読み込む機能をカンタンに構築できる環境があるので、紹介いたします。

この記事は手を動かして動作確認しながら読めるように構成してありますので、斜め読みするだけではもったいないですよ ヾ(〃><)ノ゙

はじめに

今回の記事ではクラスの自動ロード(オートローディング)の概要に絞って解説しますが、名前空間の文法や細かい説明を含めて包括的に解説した記事は、既にWEB+DB PRESS Vol.91|技術評論社にて「PHP大規模開発入門 第12回 名前空間とオートローディング」として発表済みです。 現代仮名遣いで書かれてるので職場でもオススメしやすいですね!!!1

この記事の内容とは重なるところも重ならないところもありますが、業務コードへの適用を含めて検討される型は、できれば雑誌掲載の記事と合せてお読みいただければ理解の助けになるのではないかと思ひます。 <!-- 私は原稿料をいただかない記事は絶対にまじめに書かないと決めてますので… -->

オートローディングのための仕組み

PHP公式マニュアルのPHP: クラスのオートローディング - Manualを嫁。

以上、で完結する話ではあるのですが、もうすこし噛み砕いて説明を試みます。

いますぐわかるオートローディングの仕組み

ここにPHPプロジェクトがあるじゃろ

問題の単純化のために、以下のようなカンタンなPHPプロジェクトを用意しました。

PHPを動かす環境があるひとは、ぜひ自分で手を動かして試してみてくださいね ヾ(〃><)ノ゙☆

.
|-- public
|   `-- index.php
`-- src
    |-- User.php
    `-- bootstrap.php

2 directories, 3 files

置いてあるファイルも極度に単純化して、こんな感じ。publicはドキュメントルート(Webサーバーからアクセスされるファイルを置く場所)です。

index.php
<?php

namespace ZonuProject;

require_once __DIR__ . '/../src/bootstrap.php';

$user = new User("重音テト");
?>
<!DOCTYPE html>
<title>自己紹介</title>
<p><?= htmlspecialchars($user->saySelfIntroduce(), ENT_QUOTES) ?></p>
bootstrap.php
<?php

/**
 * 各ファイルから共通で読み込まれる初期化ファイルだよ
 */

namespace ZonuProject;

include_once __DIR__ . '/User.php';
User.php
<?php

namespace ZonuProject;

class User
{
    /** @var string */
    private $name;

    /**
     * @param string $name
     */
    public function __construct($name)
    {
        $this->name = $name;
    }

    /**
     * @return string
     */
    public function saySelfIntroduce()
    {
        return "こんにちは、私は{$this->name}です。";
    }
}

汎用性のないクラス設計だな、ってつっこみは置いておくとして、業務で利用するようなPHPアプリケーションの一番シンプルな形ですね。

Apacheのmod_phpで動かせるひとはそのままでいいんですけど、ローカルで動かすサーバーがなくても、 cd public; php -S localhost:3939 でサーバーを起動して、ブラウザで http://localhost:3939/ を開いてみてください。

「こんにちは、私は重音テトです」と表示される

なかまを増やそう

さて、このままプロジェクトが成長していくと、Userと同じようなクラスがたくさん、たっくさん増えてくことが予想できます。そうすると、bootstrap.phpはどうなりますか?

bootstrap.php
<?php

/**
 * 各ファイルから共通で読み込まれる初期化ファイルの未来予想図だよ
 */

namespace ZonuProject;

include_once __DIR__ . '/User.php';
include_once __DIR__ . '/Book.php';
include_once __DIR__ . '/Work.php';
include_once __DIR__ . '/Map.php';
include_once __DIR__ . '/Novel.php';
include_once __DIR__ . '/Album.php';
include_once __DIR__ . '/Tool.php';
include_once __DIR__ . '/Music.php';
// …そのほかいっぱい!

ファイル数が10個とかそこいらならば良いのですが、クラス数が100とか1000とかになってくると、わざわざ追加するのは大変な作業になってきますね。

人力がたいへんな問題もありますが順番も意外と厄介な問題です。たいてい、クラスどうしは依存関係を持ちます。さらに、実行時に別に必要ではないファイルが常に読み込まれるのは、わかってることではありますが無駄です。

いまクラスローダーの力が欲しい

そこで話がspl_autoload_register()に帰ってきます。クラスローダーを登録してやると、クラスがみつからなかったときにそれが呼ばれます。

クラスローダーは、引数で「(名前空間付きの)クラス名」を受け取って、該当するファイルがあれば読み込んでやる機能を持った函数です。この機能は、わざわざ利用者を脅すほど難しいものではないです。もしファイルがあったらrequireincludeをする。それだけ。

クラスローダーの実装はグローバル函数でも、メソッドでも、クロージャでも大丈夫です。

bootstrap.php
<?php

/**
 * 各ファイルから共通で読み込まれる初期化ファイルだよ
 *
 * @copyright 2017 tadsan
 * @license   WTFPL
 */

namespace ZonuProject;

spl_autoload_register(function ($class_name) {
    // 名前空間の先頭が一致しなければこのプロジェクトのクラスではない
    if (strpos($class_name, 'ZonuProject\\') !== 0) {
        return false;
    }

    // 名前空間の \ をディレクトリの / に変換する
    $class_file = strtr(ltrim($class_name, 'ZonuProject\\'), ['\\' => '/']);
    $path = __DIR__ . "/{$class_file}.php";

    if (file_exists($path)) {
        require_once $path;
    }
});

これを実装するときは、実行時にエラーにならないように気をつけて実装してくださいね。 この実装は適当に書いたからエラーになるパターンがあるかもね

ではクラスを追加してみよう

えー、こんな怪しげな実装を持ってきただけで本当に動くの? 試してみてくださいな。

Item.php
<?php

namespace ZonuProject;

/**
 * @property-read string $name
 */
class Item
{
    /** @var string */
    private $name;

    /**
     * @param string $name
     */
    public function __construct($name)
    {
        $this->name = $name;
    }

    public function __get($prop)
    {
        return $this->$prop;
    }
}
User.php
<?php

namespace ZonuProject;

class User
{
    /** @var string */
    private $name;

    /** @var Item */
    private $item;

    /**
     * @param string $name
     */
    public function __construct($name)
    {
        $this->name = $name;
    }

    public function setItem(Item $item)
    {
        $this->item = $item;
    }

    /**
     * @return string
     */
    public function saySelfIntroduce()
    {
        $msg = "こんにちは、私は{$this->name}です。";

        if ($this->item) {
            $msg .= "持ち物は{$this->item->name}です。";
        }

        return $msg;
        }
}
index.php
<?php

namespace ZonuProject;

require_once __DIR__ . '/../src/bootstrap.php';

$teto = new User("重音テト");

$negi = new Item("ネギ");
$miku = new User("初音ミク");
$miku->setItem($negi);

?>
<!DOCTYPE html>
<title>自己紹介</title>
<p><?= htmlspecialchars($teto->saySelfIntroduce(), ENT_QUOTES) ?></p>
<p><?= htmlspecialchars($miku->saySelfIntroduce(), ENT_QUOTES) ?></p>

動きましたな? 動きましたね? 完璧です。

「こんにちは、私は重音テトです。」「こんにちは、私は初音ミクです。持ち物はネギです。」と表示される

じつはここまでの話は、PSR-4: Autoloaderの付属文書であるPSR-4-autoloader-examples.mdを読めばわかることです。といふかこの記事で書いた実装は当てずっぽうで書いたので、この文書の方のサンプル実装をまねした方が絶対いいですよ。

Composer

さて、ここまでクラスのオートローディングの仕組みを体験していただいたわけですが、これからComposerの世界を体験していただきます。Composerを使ったことがあるひとも、ないひとも試してみてくださいね。

コンポーザー、キミの番!

ComposerはPHPの依存性管理マネージャーです。要は、PHPの依存ライブラリとかを管理できます。筆者もコンポーザーがだいすきなので、2016年4月にPHPカンファレンス北海道で仕事で使えるComposerって発表をしたりしました (←いまじゃなくて後で読んでね)

さて、ここからは手順に注意してくださいね。

シェルでプロジェクトのルートディレクトリに移動して、Download Composerの説明の通りにコマンドをコピペして、ディレクトリにcomposer.pharを用意します。

以下のようなディレクトリ構成になってたら、ここまでの説明通りです。せっかくなので、ここでgit initコマンドを打っておいてください。git commitは、してもしなくてもいいです。

.
├── composer.phar
├── public
│   └── index.php
└── src
    ├── Item.php
    ├── User.php
    └── bootstrap.php

2 directories, 5 files

次に、composerを初期化します。シェルで以下のようなコマンドを打ってみます。

php composer.phar init

説明がだる…けふん、めんどくさ…けふんけふん、長くなるのでアニメーションGIFで見てください。git configとかちゃんと設定済みなら、結構自動入力されるのでいちいち入れなくて良いです。

composer-interactive.gif

そして、以下のコマンドを打ちます。

php composer.phar install

ここまで実行すると、ディレクトリ構成は以下のようになります。

.
├── composer.json
├── composer.phar
├── public
│   └── index.php
├── src
│   ├── Item.php
│   ├── User.php
│   └── bootstrap.php
└── vendor
    ├── autoload.php
    └── composer

4 directories, 15 files

載ろう巨人の肩

さて、Composerは何もしなくても勝手にロードされる… わけではないので、bootstrap.phpからCompsoserのファイルを読み込んでやります。

bootstrap.php
<?php

/**
 * 各ファイルから共通で読み込まれる初期化ファイルだよ
 *
 * @copyright 2017 tadsan
 * @license   WTFPL
 */

namespace ZonuProject;

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

// spl_autoload_register() は不要なので消しちゃってね

spl_autoload_register()は用済みなので消します。 短い命だったね

で、composer.jsonをエディタで開いて編集してやります。

composer.json
diff --git a/composer.json b/composer.json
--- a/composer.json
+++ b/composer.json
@@ -9,5 +9,10 @@
             "email": "tadsan@zonu.me"
         }
     ],
+    "autoload": {
+        "psr-4": {
+            "ZonuProject\\": "src/"
+        }
+    },
     "require": {}
 }

済んだら、またシェルでコマンドを打ちます。(一回だけ実行すれば大丈夫です)

php ./composer.phar dump-autoload

この状態でブラウザを開いてみて、動きましたか? 動きましたよね。

バンザイ! これでわれわれは、いちいち自前でクラスローダーを実装しなくてもファイルを自動ロードできます。これでこの記事の目的は達成されましたね。

この設定では、お作法よくファイルを追加する限り(つまり規則的な名前空間とクラス名と一致したファイル名を利用する限り)、ずっとずっとファイルが自動ロードされます。 あたし完璧

うちのクラスに命名規則はない

さて、ここまで新しく作るプロジェクトにクラスのオートローディングを利用するケースを紹介しました。が、現実の既存プロジェクトでは名前空間がなかったり、クラス名とファイル名が揃ってなかったりします。

例として、プロジェクト内の命名規則を完全に無視したクラスがsrc/tools/Hoge.phpにあったとします。

src/tools/Hoge.php
<?php

class UltraHoge
{
    public static function fugafuga()
    {
        return "ふがふが";
    }
}

それをindex.phpから呼びたいとします。

index.php
<p><?= htmlspecialchars(\UltraHoge::fugafuga(), ENT_QUOTES) ?></p>

そんなときは… こうじゃ。

composer.json
diff --git a/composer.json b/composer.json
--- a/composer.json
+++ b/composer.json
@@ -10,6 +10,7 @@
         }
     ],
     "autoload": {
+        "classmap": ["src/tools"],
         "psr-4": {
             "ZonuProject\\": "src/"
         }

composer.jsonを編集したら、例によってシェルでphp composer.phar dump-autoloadします。さて、動きましたか?

classmapは、既存プロジェクトに対して理性的な命名規則整理ができないときの最終手段です。新規プロジェクトでいきなりこの設定をつっこむのは、あんまり賢明ではないです。この方式の弱点は、クラスを追加するたびにphp composer.phar dump-autoloadする必要があることです。

函数は自動ロードできない

さて、クラスファイル(class, trait, interface)はオートローディング機能による自動ロード対象ですが、クラスに属さない函数は自動ロードできないです。なので、必要なら前もって読み込んでおくのが定石です。

ここで、functions.phpを追加するとします。

<?php

namespace ZonuProject;

/**
 * htmlspecialchars() の短縮記法
 *
 * @param string $input
 */
function h($input)
{
    return \htmlspecialchars($input, \ENT_QUOTES);
}

bootstrap.phpinclude_once __DIR__ . '/functions.php'; とか追記しても良いですけど、あまりにも藝がないので、今度もComposerの機能を利用します。

composer.json
diff --git a/composer.json b/composer.json
--- a/composer.json
+++ b/composer.json
@@ -11,6 +11,7 @@
     ],
     "autoload": {
         "classmap": ["src/tools"],
+        "files": ["src/functions.php"],
         "psr-4": {
             "ZonuProject\\": "src/"
         }

filesに記述したファイルは自動で読み込まれます。クラス定義用のファイルは自動ロードした方がいいので、ふつうここには追加しません。

依存ライブラリは自動ロードされる(たいてい)

さて、せっかくなのでPHP errors for cool kidsこと、僕らのイカしたエラー画面であるwhoops!を導入してみます。

シェルでこんなコマンドを打つと良いよ。

php ./composer.phar require filp/whoops

するとcomposer.jsonの中身が勝手に更新されるので、そのまま弄らずに git add composer.jsonしてあげるのがオススメ。

あと、このコマンドを打つとcomposer.lockが生まれます。いま作ってるのはライブラリじゃなくてアプリケーションなので、これもgit add composer.lockしてあげます。大事Daiji。

そしたら今度は、bootstrap.phpの中身を更新してあげます。追加するのは産業だけ。

bootstrap.php
<?php

/**
 * 各ファイルから共通で読み込まれる初期化ファイルだよ
 *
 * @copyright 2017 tadsan
 * @license   WTFPL
 */

namespace ZonuProject;

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

$whoops = new \Whoops\Run;
$whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler);
$whoops->register();

おめでとう、これであなたもクールなPHPerの仲間入りです。どういふことかって?

どこでもいいから、適当な場所でthrow new \Exception("ひゃっはーーーーーーーーー");とか書いてやればわかります。

whoops!を使った、わりとカッコイイエラー画面

カッコイイ画面になりましたね。

では、こんどはboostrap.phpに追加した行を消すか、コメントアウトすると、どうなりますか?

PHP7 標準エラー画面

どちらが好きかは、それぞれ好み次第ですね……。

まとめ

  • PHPのクラスはいちいちinclude_onceしなくても、自動ロードの仕組み(オートローディング)があるよ
  • オートローディングのためのクラスローダーは、わざわざ自前で実装しなくてもComposerでいいよ
  • Composerがあれば、この世はパラダイス
  • この記事のサンプルコードは https://github.com/zonuexe/no-include-php にあります

ちなみにオートローディングの仕組みが理解できれば、へんてこなメタプログラミングの温床にできるので、とってもオススメしません

参考文献

公式の資料とかです。

参考にしろ文献

筆者が書いたやつです。勝手に参考にしてもいいし、しなくてもいいです。


この記事は深夜のテンションで、けものフレンズ第10話を見ながら、おなかすいた気持ちで書きました。もしこの記事が読者の業務に役立つことがあるなら、焼肉でも奢られたい気持ちでいっぱいです ヾ(〃><)ノ゙