PHPでは未定義の定数は文字列として処理される


# 要約
- PHP では **未定義の定数** は **文字列** として処理続行する
- Warning を無視するべからず
- ついに PHP 8 からは動かなくなる
- みんな PHPStan を使おう！

# 問題
突然ですが、PHP でこう書いたとき、どのように動くでしょうか？

```php
echo date(Ymd);
```

これを書いた人は、おそらく `date("Ymd")` と書きたかったのでしょう。

うん、 **気持ちは** 解る。

一見すると、ダブルクォーテーションが前後にないので、エラー終了しそうです。

ところが、実際には **警告は出力されるが処理続行します。**

```
PHP Warning:  Use of undefined constant Ymd - assumed 'Ymd' (this will throw an Error in a future version of PHP) in test.php on line 3
20191231
```

# 理由
## 未定義の定数？
処理結果を見てみると、 `Use of undefined constant Ymd` （未定義の **定数** Ymd の使用）と書いてあります。

どうやら、PHP は `Ymd` を **定数** として認識しているようです。

定数というと「全て大文字のアルファベット」という慣習に親しみすぎて、それ以外は定数でないかのように思ってしまいますが、定数名の定義は以下のとおりです（ [PHP マニュアル](https://www.php.net/manual/ja/language.constants.php) ）。

> 有効な定数の名前は、 文字またはアンダースコアで始まり、任意の数の文字、数字、 アンダースコアが後に続きます。

なるほど。たしかに `Ymd` は定数ですね。

## なんで処理続行したの？
しかし、先程のコードは、定数 `Ymd` が未定義であるにもかかわらず、処理続行しています。

試しに、こんな風に直接 `var_dump()` してみましょうか。

```php
var_dump(Ymd);
// --> string(3) "Ymd"
```

未定義の定数なのに **文字列** として処理されている！

## こんな仕様あるの？
はい。ちゃんと [マニュアル](https://www.php.net/manual/ja/language.constants.syntax.php) にも書いてありました。

> 未定義の定数を使用した場合、ちょうどstringとして コールしたかのように(CONSTANT vs "CONSTANT")、 PHPはその定数自体の名前を使用したと仮定します。 このような振る舞いは、PHP 7.2.0 以降では非推奨となり、E_WARNING が発生するようになりました。 (それより前のバージョンでは、 E_NOTICE が発生していました)

ここで重要なのは、`E_NOTICE` だろうが `E_WARNING` だろうが **処理は続行する** ということです。

# 問題2
先程は、たまたま期待どおり動いていました。試しに、こう書いてみたら、どうでしょうか？ ログファイルに現在時刻を書き込んでいくだけの簡単なプログラムです。

```php
define("ERROR_LOG_FILE", "error.log");

// 定数名間違えちゃった！
$fp = fopen(EROR_LOG_FILE, "a");
fwrite($fp, date("Ymd H:i:s") . PHP_EOL);
fclose($fp);
```

これを実行すると、ディレクトリの中に `EROR_LOG_FILE` というファイルが作られます。

だいたい、こういうのは単体テストで見つかるのですが、1回、そのままリリースしてしまったことがあってですね・・・[^1]

[^1]: 「テストコード書いてないの？」とツッコミがありそうなので、先に書いておきます。令和のこの時代にPHP5.3を使っているような現場で、テストコードを書くという基本的人権が保証されているわけないじゃないですか・・・

テスト担当者は **なんかたくさん Warning が出てるなー** ぐらいにしか思わなかったらしく・・・

**たかが Warning、されど Warning 。Warning を無視するべからず。**

# PHPStan
最近の IDE は優秀なので、未定義の定数を書くと、未定義だよと教えてくれます。
VSCode だと、定数の下に赤い波線を出してくれたり。
でも、それはまだまだ人力に頼ってしまっていると思います。もっと機械的にできないものでしょうか。

そこで、 **静的解析の出番です！！！（ドンドン）**

例えば、冒頭のコードを [PHPStan](https://github.com/phpstan/phpstan) で静的解析させてみましょう。

```php
echo date(Ymd);
```

静的解析すると、以下のようにエラーが表示されます（実際の解析結果は [こちら](https://phpstan.org/r/70797dfa-5c91-4779-87c1-251399845781) ）。

```
+--------------------------------+
| Line | test.php                |
+--------------------------------+
| 3    | Constant Ymd not found. |
+--------------------------------+
```

こうして地上に平和がもたらされたわけです。IN TERRA PAX!

# 今後
PHP 7.2 から、Warning に `this will throw an Error in a future version of PHP` と表示されるようになりました。
そして、ついに、PHP 8 では、もはや処理続行を許さず、 [Error](https://www.php.net/manual/ja/language.errors.php7.php) として例外が送出されるようになります。

ちなみに、この件を扱っている RFC （[PHP RFC: Deprecate and Remove Bareword (Unquoted) Strings](https://wiki.php.net/rfc/deprecate-bareword-strings?s[]=constant&s[]=undefined)）を見ると、以下のようなコードが載っています。

```php
$foo = flase; // typo!
// ...
if ( $foo ) {
   var_dump($foo); // string(5) "flase"
}
```

`$foo` が `false` ならば、 `if` 文の中のコードは実行されないはずです。なのに `flase` とタイプミスしたせいで真逆の結果になるという・・・。おそろしい・・・。

RFC によると、この奇妙な動作は、1998 年にリリースされた PHP 3.0 で導入されたそうです。どうして PHP 3.0 で導入されたのか？

> The old source code also includes the documentation with which PHP 3 shipped, which seem to have no mention of this behaviour, and no examples which take advantage of it.
>
> 古いソースコードには、そのときのドキュメントも含まれている。だけど、この動作については何も言及されていないし、それを使った例も記載されていないようだ。

何も言えねえ・・・

こうして、約 20 年間、数多の開発者が陥ってきた落とし穴は、全会一致（41 vs 0）で廃止されることになりました。南無〜

# 余談
PHPStan で以下のコードを静的解析してみました（実際の解析結果は [こちら](https://phpstan.org/r/fae5c81e-4eaa-4543-8c76-fc72e211db05) ）。

```php
define("ERROR_LOG_FILE", "error.log");

// 定数名間違えちゃった！
$fp = fopen(EROR_LOG_FILE, "a");
fwrite($fp, date("Ymd H:i:s") . PHP_EOL);
fclose($fp);
```

すると、このように出力されたのですが、6行目と7行目がどうしてエラーになるのかが分からないです。
`$fp` は明らかに `resource` 型だと思うんですけどねぇ・・・

```
+------------------------------------------------------------------------------------+
| Line | test.php                                                                    |
+------------------------------------------------------------------------------------+
| 5    | Constant EROR_LOG_FILE not found.                                           |
| 6    | Parameter #1 $fp of function fwrite expects resource, resource|false given. |
| 7    | Parameter #1 $fp of function fclose expects resource, resource|false given. |
+------------------------------------------------------------------------------------+
```