PHPで現在のスコープやオブジェクト内部の状態を対話的に確認する

PHPプログラミングの際にコードをデバッグするのに最も手軽なのは、var_dumpやロガーで変数の中身を見る方法だと思う。例えば何やらおかしな動きをするメソッドがあった時に、その中のコードにvar_dumpを差し込んでコマンドラインで実行する。そして本来とるべき値から外れている変数や値を見つけることで、バグの原因を見つけるのに有用な情報を得ることができる。
このやり方は簡単だが問題がある。おかしな動きをするメソッドの中に、var_dump($a);というコードを挿入して、コマンドラインで実行して、$aという変数の中身を確認する。が、特に何もおかしなところがない。コードを書き換えて次は$bという変数の中身を見るが問題はない。次にコードを書き換えて$cという変数の中身を…という風に、おかしな値がなかなか見つからない時に

  1. var_dump等のコードを挿入する
  2. コマンドラインで実行する
  3. 表示された値が何かおかしくないか確認する

という三つのアクションを何度も繰り返さなければならない。ちょっとしたバグを見つけるのにはいいが、そうではないバグの時はこれは少し面倒だ。
というわけで、シェルのように対話的に変数や値の中身を確認できるものを書いた。コードは以下。

<?php

${''} = debug_backtrace();
${''} = isset(${''}[1]) ? ${''}[1] : ${''}[0]; 

echo "starting in {${''}['file']} on line {${''}['line']}." , PHP_EOL;
if (isset(${''}['function']) && count(debug_backtrace()) > 1) {
    echo "current context is " ,
         (isset(${''}['class']) ? "{${''}['class']}{${''}['type']}{${''}['function']}()" 
                               : "{${''}['function']}()") , 
         "." , PHP_EOL;
}
echo 'type "q" to exit.' , PHP_EOL;

while (true) {
    echo '>> ';
    ${''} = '';
    
    while (true) {
        ${''} .= trim(fgets(STDIN));
        if (preg_match('#\\\s*$#', ${''})) {
            ${''} = substr(${''}, 0, -1);
            echo '.. ' ;
        }
        else {
            break;
        }
    }
    
    // 終了
    if (preg_match('#^\s*q\s*$#', ${''})) {
        unset(${''}, ${' '});
        break;
    }
    
    // 無視
    if (preg_match('#^\s*$#', ${''})) {
        continue;
    }
    
    list(,, ${' '}) = token_get_all('<?php  ' . ${''}) + array(2 => false);
    if (in_array(${' '}, array('$', '!', '-', '^', '(', '@'), true) || is_array(${' '}) && 
        in_array(${' '}[0], array(T_ARRAY, T_ARRAY_CAST, T_BOOL_CAST, 
                                  T_CONSTANT_ENCAPSED_STRING, 
                                  defined('T_DIR') ? T_DIR : T_ARRAY, 
                                  T_DNUMBER, T_DOLLAR_OPEN_CURLY_BRACES, T_DOUBLE_CAST, 
                                  T_EMPTY, T_END_HEREDOC, T_EVAL, T_FILE, T_FUNC_C,
                                  T_INCLUDE, T_INCLUDE_ONCE, T_INT_CAST, T_ISSET,
                                  T_LINE, T_LIST, T_LNUMBER, T_METHOD_C, 
                                  defined('T_NS_C') ? T_NS_C : T_ARRAY, 
                                  T_NEW, T_NUM_STRING, T_OBJECT_CAST, T_REQUIRE,
                                  T_REQUIRE_ONCE, T_STRING, T_STRING_CAST, 
                                  T_STRING_VARNAME, T_UNSET_CAST, T_VARIABLE), true)) {
        var_export(eval('return ' . ${''} . ';'));
    }
    elseif (is_array(${' '}) && ${' '}[0] === T_RETURN) {
        var_export(eval(${''} . ';'));
    }
    else {
        eval(${''} . ';');
    }
    
    echo PHP_EOL;
}

使い方を説明すると、上記のコードをどこか適当な場所に保存して、var_dumpの代わりにこれをincludeしてやる。そしてコマンドラインで実行すると対話的な環境が開始されるので、好きなようにスコープの中身やオブジェクトのメンバ変数を見るといい。
上のコードをsh.phpという名前でinclude path以下に保存したものとして、実際に使ってみる例を示す。

<?php
class Foo
{
    private $a = 256;
    function bar($a, $b)
    {
        include 'sh.php';
        return $a + $b;
    }
}

$foo = new Foo;
$foo->bar(0, 20);

これを実行すると以下のように表示される。

starting in /path/to/foobar.php on line 14.
current context is Foo->bar().
type "q" to exit.
>>

対話的な環境が開始されている。ここからは好きなようにコードを打ち込んで試してみる。

>> $a
0
>> $b
20
>> $a + $b
20
>> $this->a
256
>> print_r(array($this->a, $a))
Array
(
    [0] => 256
    [1] => 0
)
true

スコープだけではなくオブジェクトのプライベートな変数も確認できることが分かる。
これならvar_dumpデバッグのようにコードを何度も修正してそのたびに実行するという手間が省ける。

まとめ

  • var_dumpデバッグPHPプログラミングの際にバグの原因を見つけるのに手軽な方法である。
  • しかし、繰り返しコードを修正してコマンドラインで実行するのは面倒だ。
  • 上で示した対話的なシェルを使えば、それらの手間を減らすことができる。