こんにちはこんにちは、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サーバーからアクセスされるファイルを置く場所)です。
<?php
namespace ZonuProject;
require_once __DIR__ . '/../src/bootstrap.php';
$user = new User("重音テト");
?>
<!DOCTYPE html>
<title>自己紹介</title>
<p><?= htmlspecialchars($user->saySelfIntroduce(), ENT_QUOTES) ?></p>
<?php
/**
* 各ファイルから共通で読み込まれる初期化ファイルだよ
*/
namespace ZonuProject;
include_once __DIR__ . '/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
はどうなりますか?
<?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()
に帰ってきます。クラスローダーを登録してやると、クラスがみつからなかったときにそれが呼ばれます。
クラスローダーは、引数で「(名前空間付きの)クラス名」を受け取って、該当するファイルがあれば読み込んでやる機能を持った函数です。この機能は、わざわざ利用者を脅すほど難しいものではないです。もしファイルがあったらrequire
かinclude
をする。それだけ。
クラスローダーの実装はグローバル函数でも、メソッドでも、クロージャでも大丈夫です。
<?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;
}
});
これを実装するときは、実行時にエラーにならないように気をつけて実装してくださいね。 この実装は適当に書いたからエラーになるパターンがあるかもね
ではクラスを追加してみよう
えー、こんな怪しげな実装を持ってきただけで本当に動くの? 試してみてくださいな。
<?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;
}
}
<?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;
}
}
<?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
とかちゃんと設定済みなら、結構自動入力されるのでいちいち入れなくて良いです。
そして、以下のコマンドを打ちます。
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のファイルを読み込んでやります。
<?php
/**
* 各ファイルから共通で読み込まれる初期化ファイルだよ
*
* @copyright 2017 tadsan
* @license WTFPL
*/
namespace ZonuProject;
require_once __DIR__ . '/../vendor/autoload.php';
// spl_autoload_register() は不要なので消しちゃってね
spl_autoload_register()
は用済みなので消します。 短い命だったね
で、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
にあったとします。
<?php
class UltraHoge
{
public static function fugafuga()
{
return "ふがふが";
}
}
それをindex.php
から呼びたいとします。
<p><?= htmlspecialchars(\UltraHoge::fugafuga(), ENT_QUOTES) ?></p>
そんなときは… こうじゃ。
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.php
に include_once __DIR__ . '/functions.php';
とか追記しても良いですけど、あまりにも藝がないので、今度もComposerの機能を利用します。
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
の中身を更新してあげます。追加するのは三行だけ。
<?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("ひゃっはーーーーーーーーー");
とか書いてやればわかります。
カッコイイ画面になりましたね。
では、こんどはboostrap.php
に追加した行を消すか、コメントアウトすると、どうなりますか?
どちらが好きかは、それぞれ好み次第ですね……。
まとめ
- PHPのクラスはいちいち
include_once
しなくても、自動ロードの仕組み(オートローディング)があるよ - オートローディングのためのクラスローダーは、わざわざ自前で実装しなくてもComposerでいいよ
- Composerがあれば、この世はパラダイス
- この記事のサンプルコードは https://github.com/zonuexe/no-include-php にあります
ちなみにオートローディングの仕組みが理解できれば、へんてこなメタプログラミングの温床にできるので、とってもオススメしません。
参考文献
公式の資料とかです。
- PHPマニュアル(公式・日本語)
- PSR (PHP標準勧告)
- Composer
参考にしろ文献
筆者が書いたやつです。勝手に参考にしてもいいし、しなくてもいいです。
- スライド (黒點 さんのマイページ - ニコナレ)
- Qiita
-
インスパイヤされて掲示板を作りたくなった(1)
- ↑ このシリーズでいろいろ書いてるよ ヾ(〃><)ノ゙
- PHPのモダンな開発環境を紹介する
- 名前空間付き函数 VS 静的メソッド ファイッ
どこでもクラスが参照できるふしぎなクラスローダー
-
インスパイヤされて掲示板を作りたくなった(1)
この記事は深夜のテンションで、けものフレンズ第10話を見ながら、おなかすいた気持ちで書きました。もしこの記事が読者の業務に役立つことがあるなら、焼肉でも奢られたい気持ちでいっぱいです ヾ(〃><)ノ゙