PHP で入れ子の配列の中身を再帰的に変更する方法

PHP で入れ子の配列の中身を再帰的に変更する方法について説明してみます。

PHP でコードを書いているとときどきこれをやりたくなることがあるのですが、サッと実装できずに時間がかかることがあるのでまとめておきたいと思います。

課題

入れ子の配列 $form があり、その中にキーが #submit で、値が配列になっている要素が含まれている。 もし #submit の中に foo_form_submit という文字列の要素が含まれていれば、 bar_form_submit という要素を #submit に付け加えたい。

$form のイメージ:

$form = [
  '#submit' => [
    'foo_form_submit',
  ],
  'child1' => [],
  'child2' => [
    '#submit' => [
      'foo_form_submit',
    ],
  ],
  'child3' => [
    'grandchild1' => [
      '#submit' => [
        'foo_form_submit',
      ],
    ],
  ],
];

これを次のように変更したい。

$form_changed = [
  '#submit' => [
    'foo_form_submit',
    'bar_form_submit',
  ],
  'child1' => [],
  'child2' => [
    '#submit' => [
      'foo_form_submit',
      'bar_form_submit',
    ],
  ],
  'child3' => [
    'grandchild1' => [
      '#submit' => [
        'foo_form_submit',
        'bar_form_submit',
      ],
    ],
  ],
];

「再帰呼び出しを使った方法」と「 while ループを使った方法」の大きく 2 通りの方法があります。

再帰呼び出しアプローチ

まずは再帰呼び出しを使った方法で解いてみます。

再帰呼び出しを行う関数を定義します。


/** * 再帰的に要素を追加する */ function append_submit_element_recursively_1(array &$element) { if (!empty($element['#submit']) && is_array($element['#submit'])) { if (in_array('foo_form_submit', $element['#submit'], TRUE)) { $element['#submit'][] = 'bar_form_submit'; } } foreach ($element as &$child) { if (is_array($child)) { append_submit_element_recursively_1($child); } } }

かんたんなテストを書いて試してみます。


test(); /** * 再帰的な要素の追加を実施する */ function test() { $form = [ '#submit' => [ 'foo_form_submit', ], 'child1' => [], 'child2' => [ '#submit' => [ 'foo_form_submit', ], ], 'child3' => [ 'grandchild1' => [ '#submit' => [ 'foo_form_submit', ], ], ], ]; $expected = [ '#submit' => [ 'foo_form_submit', 'bar_form_submit', ], 'child1' => [], 'child2' => [ '#submit' => [ 'foo_form_submit', 'bar_form_submit', ], ], 'child3' => [ 'grandchild1' => [ '#submit' => [ 'foo_form_submit', 'bar_form_submit', ], ], ], ]; append_submit_element_recursively_1($form); assert($form == $expected); } /** * 再帰的に要素を追加する */ function append_submit_element_recursively_1(array &$element) { if (!empty($element['#submit']) && is_array($element['#submit'])) { if (in_array('foo_form_submit', $element['#submit'], TRUE)) { $element['#submit'][] = 'bar_form_submit'; } } foreach ($element as &$child) { if (is_array($child)) { append_submit_element_recursively_1($child); } } }

実行すると正しく動くことが確認できます。

ポイント:

  • 再帰関数の引数を参照渡しにする( append_submit_element_recursively_1() の引数を & 付きにする)
  • 小要素のループを参照渡しで回す( foreach ($element as &$child)$child& をつける)

関数の呼び出しとループのところを参照渡しにしておかないと、せっかく加えた変更が元の配列に反映されないので注意が必要です。

ちなみに、再帰呼び出しの部分を次のように書くことはできません。

    if (is_array($child)) {
      call_user_func(__FUNCTION__, $child);
    }

なぜなら call_user_func() は参照渡しができないためです。 たとえ対象の関数が引数を参照として受け取るように定義されていても、値渡しになってしまいます。 これはこれまでに何度かはまって驚いた覚えがあります。

while ループアプローチ

続いて、 while ループを使った方法を見てみます。

while ループで BFS を行う関数を定義します( DFS でも可)。


/** * 再帰的に要素を追加する( while ループ) */ function append_submit_element_recursively_2(array &$element) { $edges = [&$element]; while (count($edges) > 0) { reset($edges); $edge = &$edges[key($edges)]; array_shift($edges); if (!empty($edge['#submit']) && is_array($edge['#submit'])) { if (in_array('foo_form_submit', $edge['#submit'], TRUE)) { $edge['#submit'][] = 'bar_form_submit'; } } foreach ($edge as &$child) { if (is_array($child)) { $edges[] = &$child; } } } }

こちらも上の append_submit_element_recursively_1() と同様にテストを書いて実行すればうまく動くことが確認できます。

ポイント:

  • 再帰関数の引数を参照渡しにする( append_submit_element_recursively_2() の引数を & 付きにする)
  • 配列の要素を受け渡しするときにことごとく参照渡しにする( & をつけて代入する)

参照渡し( & )だらけでなんだか気持ち悪い感じがしますが、こうしないとせっかく加えた変更も元の配列に反映されません。 こちらは再帰呼び出しアプローチ以上に十分に注意して参照渡しを徹底する必要があるようです。

array_shift() は配列の最初の要素を pop するときに便利ですが、ここでは array_shift() を使って値を取得すると参照が失われてしまうため、 $edges から $edge を取り出すところでは $edge = array_shift(&edges) と書くことができません。 そのため、わざわざ参照を保った形で要素を取り出してから array_shift() で配列を変更しています。

以上、 PHP で入れ子の配列の中身を再帰的に変更する方法についてでした。