PHP5.3と5.4のパスを指定しないrequire(include)の違い

**タイトルは不正確です。詳細は[コメント欄]へ**

# 概要

**パスは(相対的)絶対パスで表記しましょう**
(e.g. `__DIR__ . '../hoge.php'`)
という前提のもと、パスを指定しない`require`で、
エラーと理解できない動作が起きている状態です。
理解できない動作は両バージョンで起き、
エラーに関しては5.4のみで起きています。

この記事はすべて推測です。

相対パスでハマる、ということはネットで良く出るのですが、イマイチ同一事象かの判別もつきません。

※マニュアル時点で作業ディレクトリとカレントディレクトリ両方が(別ページですが)使われていますので、参照元を真似する感じで両方の表記を使用しています。

# 何が起きるのか

## エラー

`require`の部分的失敗、`include_path`の探索中に

```
PHP Warning:  require(hoge.php): failed to open stream: No such file or directory 
PHP Notice:  blenc_compile: unable to open stream, compiling with default compiler.
```

が出る(読み込みは成功する)

## 理解できない動作

`require`されたファイルからの`require`時、大本の読み込みパスからは読み込めない相対パスによる指定が読み込めている。


# 検証環境

* CentOS5 PHP 5.3
* CentOS7 PHP 5.4


# `require`のおさらい

`require(include)`がどのような動きをするかマニュアルから確認します。

[PHP: include - Manual]

>以下の記述内容は require にも当てはまります。 

***

>ファイルのインクルードは、指定されたパスから行います。**パスを指定しない場合は、 include_path の設定を利用します。 ファイルが include_path に見つからないときは、include は呼び出し元スクリプトのディレクトリと現在の作業ディレクトリも探します。**

ここの
>呼び出し元スクリプトのディレクトリ

が、最初の`require`か直近の`require`かは難しいです…
流れ的には直近だと思いますが。
相対パスでハマるのは作業ディレクトリが最初のファイルのディレクトリになるからですね。

また、パス無指定の読み込みは

1. `include_path`
2. 読み出し元スクリプトのディレクトリ
3. 作業ディレクトリ

の三段階であることは初耳でした。
なお、三段階と書きましたが、読み出し元スクリプトのディレクトリと作業ディレクトリ、どちらが先に探索されるかはここでは明言されていないと受け取ります。
(素直に読むと読み出し元スクリプトのディレクトリからですが…)



***

>パスを指定した場合 — 絶対パス (Windows ならドライブレターあるいは \ で始まるパス、Unix/Linux 系なら / で始まるパス) あるいはカレントディレクトリからの相対パス (. あるいは .. で始まるパス) のどちらでも — は **include_path は無視されます。**たとえば ../ ではじまるファイル名を指定した場合は、 親ディレクトリからそのファイルを探します。 

`include_path`は無視されても

1. 読み出し元スクリプトのディレクトリ
2. 作業ディレクトリ

は見るのでしょうか。

***

[PHP: コア php.ini ディレクティブに関する説明 - Manual]

>PHP は、インクルードするファイルを探す際に インクルードパスの各エントリを個別に調べます。 まず最初のパスを調べ、見つからなければ次のパスを調べ、…… というように、ファイルが見つかるか warning あるいは error が発生するまで続けます。インクルードパスを実行時に変更したり設定したりするには set_include_path() を使用します。 

現在の事象は`warning`が出ても継続している様に見えるんですよね。
`require`には数段階あり、`warning`が出れば、そのある段階は終了して次の段階の探査、ということでしょうか。
それともエラー箇所が`include_path`ではないので`warning`で止まらないのか

***

>インクルードパスに . (カレントディレクトリ) を指定すると、相対パス指定によるインクルードができるようになります。

この場合の相対パスとはカレントディレクトリからか、呼び出し元のパスからかは不明です。

>しかし、インクルードのたびに毎回 PHP にカレントディレクトリをチェックさせるよりは、 明示的に include './file' を指定したほうが効率的です。 

あれ、やっぱりカレントディレクトリからなのかな？
それはそうか。

# コード

## tree

```
.
|-- dir
|   |-- a.php
|   `-- b.php
`-- root.php

```

## root.php

```php:/tmp/root.php
<?php

error_reporting(-1);

echo "START\n";

echo getcwd() . "\n";
echo get_include_path() . "\n";

require '/tmp/dir/a.php';

echo "END\n";

```

```php:a.php
<?php

echo "a\n";

echo getcwd() . "\n";
echo get_include_path() . "\n";

require ('b.php');

```

```php:b.php
<?php

echo "b\n";

echo getcwd( ) . "\n";
echo get_include_path() . "\n";

```

## PHP5.3出力

```
START
/tmp
.:/usr/share/pear:/usr/share/php
a
/tmp
.:/usr/share/pear:/usr/share/php
b
/tmp
.:/usr/share/pear:/usr/share/php
END

```

## PHP5.4出力

```
START
/tmp
.:/usr/share/pear:/usr/share/php
a
/tmp
.:/usr/share/pear:/usr/share/php
PHP Warning:  require(b.php): failed to open stream: No such file or directory in /tmp/dir/a.php on line 8
PHP Notice:  blenc_compile: unable to open stream, compiling with default compiler. in /tmp/dir/a.php on line 8
b
/tmp
.:/usr/share/pear:/usr/share/php
END

```

## `a.php`の読み込みファイルを`c.php`(存在しないファイル)にした場合

### PHP5.3出力

```
START
/tmp
.:/usr/share/pear:/usr/share/php
a
/tmp
.:/usr/share/pear:/usr/share/php
PHP Warning:  require(c.php): failed to open stream: No such file or directory in /tmp/dir/a.php on line 8
PHP Fatal error:  require(): Failed opening required 'c.php' (include_path='.:/usr/share/pear:/usr/share/php') in /tmp/dir/a.php on line 8
```

### PHP5.4出力

```
START
/tmp
.:/usr/share/pear:/usr/share/php
a
/tmp
.:/usr/share/pear:/usr/share/php
PHP Warning:  require(c.php): failed to open stream: No such file or directory in /tmp/dir/a.php on line 8
PHP Warning:  require(c.php): failed to open stream: No such file or directory in /tmp/dir/a.php on line 8
PHP Notice:  blenc_compile: unable to open stream, compiling with default compiler. in /tmp/dir/a.php on line 8
PHP Warning:  require(c.php): failed to open stream: No such file or directory in /tmp/dir/a.php on line 8
PHP Fatal error:  require(): Failed opening required 'c.php' (include_path='.:/usr/share/pear:/usr/share/php') in /tmp/dir/a.php on line 8

```

# `b.php`がなぜ読み込めるか

`a.php`で書かれた`require ('b.php');`は、パスが指定されていないことから`include_path`を探索します。

`include_path`は`.:/usr/share/pear:/usr/share/php`
左から順に評価するので、
1. `.` = `/tmp/`(`root.php`がカレントになるので) には`b.php`は無い
2. `/usr/share/pear`,`/usr/share/php`にも無い
3. `include_path`には無いので、呼び出し元スクリプトのディレクトリと現在の作業ディレクトリからも探索します。
4. 呼び出し元スクリプトが`a.php`と仮定すると、ディレクトリは`/tmp/dir/`なので、`b.php`が存在します
5. 作業ディレクトリは`/tmp/`なので無い

となり、`b.php`が読み込めると推測します。

`require`の動的な読み込み方法が、相対パス指定と`include_path`しかないと思っていると、この動作は不思議に思えました。
特に
「`.`は`root.php`のカレントディレクトリになるんだろ。俺知ってるんだぜ」
状態だと謎が深まります…

# なぜエラーが出るのか

5.4でエラーが出るようになった。
これは本当にわかりません…
**failed to open stream**と出ているにもかかわらず読み込めていますしね。

1. `include_path`
2. 読み出し元スクリプトのディレクトリ
3. 作業ディレクトリ

のどれかが失敗するとこのエラーが出るのでは？
と推測して、ひとまずすべてが失敗する`require(c.php)`にしてみてところ、更に謎が増え…

最後の

```
PHP Warning:  require(c.php): failed to open stream: No such file or directory in /tmp/dir/a.php on line 8
PHP Fatal error:  require(): Failed opening required 'c.php' (include_path='.:/usr/share/pear:/usr/share/php') in /tmp/dir/a.php on line 8
```

は、探索パスがすべて失敗したことの`Warning`と、`require`が失敗したことの`Fatal`と捉えています。5.3でも出ているので。
すると、`b.php`の時との差分は
`PHP Warning:  require(c.php): failed to open stream: No such file or directory in /tmp/dir/a.php on line 8`
一個分になるかと思います。

(増えたのは出力最初の`Warning`と考えていますが、メッセージが同一のため、もしかしたら`require`失敗時の`Warning`と`Fatal`のセットは**最初と最後のエラー**の組み合わせもありうるわけですが…)

探索先は3種類、すべて探索して失敗した場合のエラーは2種類。

## 切り分け1 `include_path`から`.`を除去

`a.php`からの読み込み時に`include_path`から`.:`を取り除きます。
元々`.`は`/tmp/`を指しているので、影響は無いと考えます

```php:a.php
<?php

echo "a\n";

echo getcwd() . "\n";
set_include_path('/usr/share/pear:/usr/share/php');
echo get_include_path() . "\n";

require ('a.php');

```

```
START
/tmp
.:/usr/share/pear:/usr/share/php
a
/tmp
/usr/share/pear:/usr/share/php
PHP Warning:  require(b.php): failed to open stream: No such file or directory in /tmp/dir/a.php on line 9
PHP Notice:  blenc_compile: unable to open stream, compiling with default compiler. in /tmp/dir/a.php on line 9
b
/tmp
/usr/share/pear:/usr/share/php
END

```

変わりません。

## 切り分け2 カレントディレクトリを`/tmp/dir`にする

`include_path`探索後の探索が

1. 作業ディレクトリ
2. 読み出し元スクリプトのディレクトリ

の場合、作業ディレクトリの探索でエラーになり、読み出し元スクリプトのディレクトリの探索で成功、となっている可能性があります。
なお、`include_path`の'.'があると`include_path`時点で成功するため切り分け1のまま取り除いておきます

```php:a.php
<?php

echo "a\n";

chdir('/tmp/dir');
echo getcwd() . "\n";
set_include_path('/usr/share/pear:/usr/share/php');
echo get_include_path() . "\n";

require ('b.php');

```

```
START
/tmp
.:/usr/share/pear:/usr/share/php
a
/tmp/dir
/usr/share/pear:/usr/share/php
b
/tmp/dir
/usr/share/pear:/usr/share/php
END
```

エラーが出なくなりました！

```
PHP Warning:  require(hoge.php): failed to open stream: No such file or directory 
PHP Notice:  blenc_compile: unable to open stream, compiling with default compiler.
```

は、`include_path`探索後、カレントディレクトリの探索を行い、失敗時に出力し、読み出し元スクリプトのディレクトリからの探索へ続く、と見ていいかもしれません

## 切り分け3 本当にカレントディレクトリの探索が先なのか

現在の構成では`b.php`が`a.php`と同じ場所にあり、カレントディレクトリか読み出し元スクリプトのディレクトリかの識別がつきにくいため、
最後に本当にカレントディレクトリからの探索が先かを調査します。
別ディレクトリに別の`b.php`を作成し、カレントディレクトリをそこにすることで、
`a.php`から呼び出される`b.php`はカレントか呼び出し元かを分けます

```php:a.php
<?php

echo "a\n";

chdir('/tmp/another');
echo getcwd() . "\n";
set_include_path('/usr/share/pear:/usr/share/php');
echo get_include_path() . "\n";

require ('b.php');

```

```php:もう一つの`b.php`(`/tmp/another/b.php`)
<?php

echo "another b\n";

echo getcwd() . "\n";
echo get_include_path() . "\n";

```

```
START
/tmp
.:/usr/share/pear:/usr/share/php
a
/tmp/another
/usr/share/pear:/usr/share/php
b
/tmp/another
/usr/share/pear:/usr/share/php
END

```

**!?**
おかしい…
カレントディレクトリを先に探索するならば
`/tmp/another/b.php`
を読み込むハズなのに…
読み出し元スクリプトのディレクトリを先に処理するならば
`chdir('/tmp/dir');`
は無意味でエラーが消えるはずなのに…

あるいは

* 作業ディレクトリ
* 読み出し元スクリプトのディレクトリ

両方調査し、作業ディレクトリになければエラー出力。
両方にあれば読み出し元スクリプトのディレクトリのファイルを読み込む。
といった動きでしょうか？

ちょっとギブアップ…
目標は5.4で

```
PHP Warning:  require(hoge.php): failed to open stream: No such file or directory 
PHP Notice:  blenc_compile: unable to open stream, compiling with default compiler.
```

をコードとエラーレベルを変更せずに消すことなんですが、
わけがわからないよ。

皆さんはちゃんと絶対パスで表記しましょう。
`__DIR__`付けるだけですので。


# 参考資料

* [include/requireを使った時のパスの扱い - PHP - INOLOG Ver.2.1]
* [PHP: PHP 5.3.x から PHP 5.4.x への移行 - Manual]
* [PHP: include - Manual]
* [PHP: コア php.ini ディレクティブに関する説明 - Manual]

[PHP: include - Manual]:http://php.net/manual/ja/function.include.php
[include/requireを使った時のパスの扱い - PHP - INOLOG Ver.2.1]: http://blog.grimonet.com/articles/2012/01/includerequire_-_php.php

[PHP: コア php.ini ディレクティブに関する説明 - Manual]:http://php.net/manual/ja/ini.core.php#ini.include-path

[PHP: PHP 5.3.x から PHP 5.4.x への移行 - Manual]:http://php.net/manual/ja/migration54.php

[コメント欄]:#comments