PHPではグローバル変数は閉じ込めることができる。そして、関連して引っかかったこと
前提として、PHPでは変数には2つのスコープしかない。グローバルと、各関数(メソッド)ごとのものだ。ブロックスコープがないのが不便とはいえ、グローバル変数を使わなければ割と平和だ。
しかしレガシーコードを扱う場合など、仕方なくグローバル変数を使わざるをえない場合はある。今回、あるファイルに並んだグローバル変数が設定として使われている例があった。
ぜんぶ書き換えられるなら書き換えたいところだったが、なかなかそうも行かない。仕方ないので、せめて新しいコードからはラッパクラス経由でアクセスすることにした。
以下のようにすればベストだったと思われる。
<?php # legacy-config.php $xxx = 'OK';
<?php # lib/MyApp/Config.php namespace MyApp; class Config { public function get_xxx() { require 'legacy-config.php'; return $xxx; } }
require
は、実行されたコード上のその位置でそのまま実行されたようにふるまう(おそらく)。そのため、 $xxx
は、 get_xxx()
のスコープになる。
こうすればグローバル変数のはずのものを、関数のスコープに閉じ込めることができて安全である。が、そのことを知らずによくない実装をしてしまっていた。
<?php # lib/MyApp/Config.php namespace MyApp; require 'legacy-config.php'; class Config { public function get_xxx() { global $xxx; return $xxx; } }
こちらの実装でも、グローバル変数が漏れてはいるものの、動作自体は変わらない。
そのためしばらくは問題なかったのだが、あるとき設定の取得ができなくなった。調べてみると、 get_xxx()
がなにも返していない。
原因はオートローダを導入したことだった。オートローダを導入するとなぜグローバル変数が消えるのか。さらに調べて、最初の「グローバル変数を関数に閉じ込めることができる」仕様を知った。
PHPのオートローダの実装は、 spl_autoload_register()
という組み込み関数によって実現される。
クラスが必要とされた際に、この spl_autoload_register()
の第1引数に設定した関数がコールバックされて、そこで呼び出すという形だ。たとえば最小限(かつ適当)なオートローダの実装はこんな感じになる。
<?php spl_autoload_register(function ($class) { require strtr($class, '\\', '/') . '.php'; }); new \NS\Cls(); # NS/Cls.php が自動的に require される
見ての通り、 require
は関数内で行われている。そのため、
- オートローダで読み込まれたコード内のあらゆるグローバル変数は関数スコープになる。
- さらに、オートローダで読み込まれたコード内で読み込まれたコードも関数スコープになる。
対策としては、今回のような場合は最初のコードのように、そもそも関数(メソッド)内で require
すればよい。
なお require_once
にするとまた別の問題が起きるので注意。すでにほかで読み込まれているときにわかりづらく問題になる。