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 にするとまた別の問題が起きるので注意。すでにほかで読み込まれているときにわかりづらく問題になる。

また、どうしてもグローバル変数グローバル変数そのままで必要な場合は、そこだけ手動で読み込めばいいだろう。