PHP でファイルをインクルードするときは絶対パスを使おう
『ロマンシング サ・ガ』三部作のリマスター版サウンドトラック発売を受けてゲーム熱が再燃している kagata です。
さて今回は PHP のこまかい話です。インクルードするファイルを相対パスで指定していると、思わぬ挙動をみせたり、異なる環境での使いまわしに難が出ることがあります。どんな環境でも意図したとおりのファイルをインクルードできる記述を考えてみましょう。
相対パスでインクルードした場合の挙動(Webの場合)
まず、相対パスでインクルードすると何が問題になるのか見てみましょう。
例えば、次のようなファイル構成の Web アプリケーションがあったとします。
- /index.php
- /module.php
- /include/
- /include/inc.php
- /include/module.php
で、それぞれのファイルの中身を次のようにします。このとき、index.php にアクセスするとどんな表示が得られるでしょう?
/index.php
<?php include 'include/inc.php'; // ./include/inc.php をインクルードする
/module.php
<?php echo '/module.php をインクルードしました' . "\n";
/include/inc.php
<?php include 'module.php'; // 自分と同階層の module.php をインクルードする(つもり?)
/include/module.php
<?php echo '/include/module.php をインクルードしました' . "\n";
表示結果
/module.php をインクルードしました
処理の流れは次のとおりです。
- /index.php を読み込む
- include 文に従って /include/inc.php をインクルード
- /include/inc.php の include 文に従って /index.php と同階層の module.php をインクルード
- /module.php の echo 文を実行
3番目がポイントです。/include/inc.php の記述を見ると、自分自身と同階層にある module.php をインクルードするように見えます。しかし、include 文のパスは実行したファイルからの相対パスとして解釈されるため、/index.php と同階層の module.php がインクルードされるのです。
では、include 文の相対パスは実行したファイルの位置から必ず解釈されるのかというと、そうでもありません。
コマンドラインからだともっと大変
実は、この記事のコマンドラインから使うスクリプトを PHP で書いたことがきっかけになっています。このような場合には、もっと事態は複雑になります。
今度は、次のようなファイル構成の PHP スクリプトをコマンドラインから利用することを考えてみます。
- /script.php
- /module.php
- /directory/
- /directory/module.php
/script.php
<?php include 'module.php';
/module.php
<?php echo '/module.php をインクルードしました' . "\n";
/directory/module.php
<?php echo '/directory/module.php をインクルードしました' . "\n";
実行結果
$ php script.php
/module.php をインクルードしました
$
ちゃんと script.php と同階層の module.php を読み込んでいるので一安心、と思いきや。
$ cd /directory/
$ php ../script.php
/directory/module.php をインクルードしました
$
ディレクトリを移動して script.php を起動すると、インクルード先が変わってしまいました。
これは、include 文の相対パスが次のように解釈されるためです。
- まずはカレントディレクトリからの相対パスとして解釈する
- ファイルが見つからなければ、実行したファイルの位置からの相対パスとして解釈する
また、php.ini で include_path が指定されていれば、上記に続いてファイルが探索されます。
ということで、include 文に相対パスを使うとたいへんややこしいということがお分かりいただけたかと思います。
絶対パスを使おう
とはいえ、絶対パスを頭から書き下そうと思うと、別の環境に持って行ったときの互換性が気になります。手元の開発環境では /home/ 配下に置いているものを、本番環境では /var/www/html に持っていかないといけない…なんてときに、手作業で書き換えたり if 文で切り替えたりしていてはたいへんです。
こんなときは dirname(__FILE__)
が便利です。__FILE__
は PHP のマジカル定数と呼ばれるもののひとつで、ファイル自身の絶対パスが常に入っています。関数 dirname()
でディレクトリを切り出してやることで、そのファイル自身の位置を起点にパスを指定するという方法がよくみられます。
// 自分自身と同階層の include.php をインクルードする
include dirname(__FILE__) . '/include.php';
さらに、PHP5.3 から __DIR__
というマジカル定数が追加されました。これひとつで dirname(__FILE__)
と同じ結果になります。今後はこちらが主流になるかもしれません。
include __DIR__ . '/include.php';
フレームワークの API をうまく使おう
また、フレームワークを利用している場面なら、そのフレームワークが用意している API を使うと便利ですね。CakePHP なら App::import()
とか、WordPress なら get_template_part()
とか。
相対パス解釈の罠にはまらずにすむだけでなく、ものによってはディレクトリトラバーサルなど脆弱性への対策が取られていたりするかもしれません。
余談:WordPress のコードも最近変わった
ちなみに、WordPress のコアのコードでも、相対パスによるインクルードを dirname(__FILE__)
による絶対パスに置き換える変更が最近行われました。
この変更に気がついたのはつい最近のことで、いつの間に…?と調べてみたらバージョン3.7からみたいです。
まとめ
- インクルードするファイルは絶対パスで指定しよう
- 絶対パスを使うときはマジカル定数
__DIR__
やdirname(__FILE__)
を使うと環境に依存せず便利 - フレームワークが用意する API に頼るのも一案