【PHP】無名関数で再帰呼び出しの落とし穴

こんにちは。 システム部のたかくです。業務でPHPに触りだしてから2ヶ月ほどになります。 デバッガがないのでステップ実行もない、変数の内容を拾うのにいちいち出力するの煩わしいなどと、 まだまだ開発環境そのものを見直したくて仕方ない時期です。

概要

さて、記事タイトルのお話になります。 “WordPressで、特定のスラッグを持つ最上位の固定ページから、最下位までの固定ページをリスト化する”という処理が必要になりました。 「wp_list_pages()があるじゃないか!」とも思いますが、ただリストが欲しいだけではありません。 適切な形でhtml出力するにあたって細かく制御する必要がありました。 ツリー型の構造を得たいなら再帰の出番です。 諸事情で新しく関数を書き足すことは避けたい状況でしたので、手続き的に書きたいなと思いました。 そうなると無名関数しかないよなーと思い、安易に手を出したら軽くハマってしまったのでその備忘録になります。 (PHPの無名関数は5.3からサポートされているようですよ)

最初に書いたもの

$family = function ($children) use (&$contents) {
    if (count($children) == 0) {
        return;
    } else {
        foreach ($children as $child) {
            // 前処理
            $nextChildren = get_children(array(
                'post_parent' => $child->ID,
                'post_type' => 'page',
            ));
            $family($nextChildren);
            // 後処理
        }
    }
};

$family($firstChildren);

(見づらいですが) 出力したいhtmlテキストの形式が若干特殊なため、最初に該当ページの最上位ページを拾ってからその直下のページだけを$firstChildrenとして取得しています。 それを無名関数に投げて再帰的に呼び出し、前処理と後処理の中で$contentsにhtmlテキストを格納していく、というものです。 変数のキャプチャの仕様はいろんな言語で違うため、真っ先に調べてuseを使うんだなーとぼんやり理解しました。 無名関数も関数の1つであるからか変数のスコープが外側と異なるため、$contentsには参照を渡さなければ処理が値に反映されません。 しかし、これで動いてくれることを期待しましたが見事に実行を停止してしまいます。

なぜなのか

無名関数の中と外ではまったく別のスコープであると予想します。 $familyという変数に無名関数を渡して、その中で$family()を再帰的に呼び出しています。 この無名関数は$familyという変数があることをまったく知りません。 スコープの外の存在は$contentsのように丁寧に教えてやらなければ見つけてくれないのです。 これが原因でした。

せいかい

$family = function ($children) use (&$family, &$contents) {
    if (count($children) == 0) {
        return;
    } else {
        foreach ($children as $child) {
            // 前処理
            $nextChildren = get_children(array(
                'post_parent' => $child->ID,
                'post_type' => 'page',
            ));
            $family($nextChildren);
            // 後処理
        }
    }
};

$family($firstChildren);

ちゃんと無名関数を渡す変数もuseでキャプチャしてあげましょう。 無名関数の中で自分を表すキーワード(クラスで言うthis)なんかが書ければ、 無名関数を渡される変数名が関数の定義の中の変数名に縛られないので、自由度が増すのですが… ありませんか?