【PHP】ネストした配列に対応可能できる再帰的な array_diff を作る

  • 2024年4月3日
  • 2024年4月3日
  • PHP

 PHP には array_diff という関数があります。array_diff は次のように使って配列の差分を得ることができます。
PHP: array_diff – Manual

<?php

var_dump(array_diff(
    ['a' => 'apple', 'b' => 'banana', 'c' => 'cherry'],
    ['a' => 0, 'b' => 'banana', 'd' => 'donut']
));
// array(2) { ["a"]=> string(5) "apple" ["c"]=> string(6) "cherry" }

 array_diff は便利なのですが、配列がネストしている場合には使えません。次のように警告が現れ、正しい結果が得られない場合があります。

<?php

var_dump(array_diff(
    ['a' => ['sub' => 'apple']],
    ['a' => ['sub' => 'orange']],
));
// PHP Warning:  Array to string conversion in xxx.php on line 3
// PHP Warning:  Array to string conversion in xxx.php on line 3
// array(0) {
// }

 そこで再帰的な array_diff を作る需要があります。実際に作った array_diff_recursive は次のようになります。

<?php

/**
 * 2つの配列の差分を再帰的に取得する。
 * 返り値の差分は $array1に存在する値 !== その値と同キーで$array2を参照した時の値 となるキーと値による連想配列。
 * 比較の際にはキーの順序は無視するが、返り値は$array1のキー順序を維持する。
 *
 * @param  array          $array1         比較対象1
 * @param  array          $array2         比較対象2
 * @param  callable|bool  $compareMethod  値の比較方法。trueならば厳密な比較、falseならば緩やかな比較、($a,$b)=>boolな関数ならそれを使う。関数には二つの値が等しいならばtrue、異なるならばfalseを返すことを期待する。デフォルトは厳密比較
 * @return array 配列の差分。$array1基準。同キーで異なる値は$array1の値を採用。$array1に存在し$array2に存在しないキーはそのまま採用。$array2に存在し$array1に存在しないキーは無視
 */
function array_diff_recursive(array $array1, array $array2, callable|bool $compareMethod = true): array
{
    if(is_callable($compareMethod)) {
        $isEqualFn = $compareMethod; // インスタンスの比較とか何か特別な比較をしたい場合に使う
    } else {
        // === か == かを決定
        $isEqualFn = $compareMethod ? fn($a, $b) => $a === $b : fn($a, $b) => $a == $b;
    }
    // 関数の決定を何度も行いたくないので、再帰呼び出し用の関数を内部で定義
    $_array_diff_recursive = function(array $array1, array $array2) use ($isEqualFn, &$_array_diff_recursive): array {
        $result = [];
        foreach($array1 as $key => $value) {
            // キーが$array2に存在しない場合、そのまま結果に追加
            if(!array_key_exists($key, $array2)) {
                $result[$key] = $value;
                continue;
            }

            // キーが$array2に存在し両方とも配列の場合、再帰的に奥を見に行って差分を得る
            if(is_array($value) && is_array($array2[$key])) {
                $diff = $_array_diff_recursive($value, $array2[$key]);
                if(!empty($diff)) {
                    $result[$key] = $diff;
                }
                continue;
            }
            // 配列でない場合、上で用意した比較関数で差異があるか確認
            if(!$isEqualFn($value, $array2[$key])) {
                $result[$key] = $value;
            }
        }
        return $result;
    };


    return $_array_diff_recursive($array1, $array2);
}

使い方兼テストコード
/** 失敗した時にだけvar_dumpする。ざっくりテスト関数 */
function var_dump_when_fail(bool $test_passed, mixed $v): void
{
    if(!$test_passed) {
        var_dump($v);
    }
}


var_dump('同じ値が異なるキーで存在する場合');
$res    = array_diff_recursive(
    ['a' => 'apple', 'b' => 'banana'],
    ['c' => 'apple', 'd' => 'date']
);
$expect = ['a' => 'apple', 'b' => 'banana'];
var_dump_when_fail($res === $expect, $res);

var_dump('深い配列でのキーの不一致');
$res    = array_diff_recursive(
    ['a' => ['sub1' => 'apple']],
    ['a' => ['sub2' => 'apple']]
);
$expect = ['a' => ['sub1' => 'apple']];
var_dump_when_fail($res === $expect, $res);

var_dump('深い配列での値の不一致');
$res    = array_diff_recursive(
    ['a' => ['sub' => 'apple']],
    ['a' => ['sub' => 'orange']]
);
$expect = ['a' => ['sub' => 'apple']];
var_dump_when_fail($res === $expect, $res);

var_dump('深い配列内で同じキーと値を持つが、別の要素が存在する場合');
$res    = array_diff_recursive(
    ['a' => ['sub' => 'apple', 'extra' => 'banana']],
    ['a' => ['sub' => 'apple']]
);
$expect = ['a' => ['extra' => 'banana']];
var_dump_when_fail($res === $expect, $res);

var_dump('配列の中に空の配列が存在する場合');
$res    = array_diff_recursive(
    ['a' => [], 'b' => 'value'],
    ['a' => ['sub' => 'value']]
);
$expect = ['b' => 'value'];
var_dump_when_fail($res === $expect, $res);

var_dump('空配列と非配列の比較');
$res    = array_diff_recursive(
    ['a' => [], 'b' => [], 'c' => [], 'd' => []],
    ['a' => 'value', 'c' => [], 'd' => ['sub' => 'value']],
);
$expect = ['a' => [], 'b' => []];
var_dump_when_fail($res === $expect, $res);

var_dump('同一キーであるが、一方が配列で他方が非配列の値を持つ場合');
$res    = array_diff_recursive(
    ['a' => ['sub' => 'apple'], 'b' => 'banana'],
    ['a' => 'apple', 'b' => ['sub' => 'banana']],
);
$expect = ['a' => ['sub' => 'apple'], 'b' => 'banana'];
var_dump_when_fail($res === $expect, $res);

var_dump('数値キーの扱い');
$res    = array_diff_recursive(
    [1, 2, 3, 4, 5],
    [4, 1, 3, 2, 5]
);
$expect = [0 => 1, 1 => 2, 3 => 4];
var_dump_when_fail($res === $expect, $res);

var_dump('キーの順序の違い');
$res    = array_diff_recursive(
    [1 => 'a', 2 => 'b', 3 => 'c'],
    [3 => 'c', 2 => 'b', 1 => 'a']
);
$expect = [];
var_dump_when_fail($res === $expect, $res);

var_dump('非数値キーでの配列と非配列の比較');
$res    = array_diff_recursive(
    ['key1' => ['sub1' => 'apple']],
    ['key1' => 'apple']
);
$expect = ['key1' => ['sub1' => 'apple']];
var_dump_when_fail($res === $expect, $res);

var_dump('異なる型について緩やかな比較');
$res    = array_diff_recursive(
    [
        'truthy' => ['a' => 1, 'b' => true, 'c' => '1'],
        'falsy'  => ['c' => [], 'd' => [], 'e' => null]
    ],
    [
        'truthy' => ['a' => '1', 'b' => 1, 'c' => true],
        'falsy'  => ['c' => 0, 'd' => null, 'e' => 0]
    ],
    false
);
$expect = ['falsy' => ['c' => []]];
var_dump_when_fail($res === $expect, $res);

var_dump('異なる型について厳密な比較');
$res    = array_diff_recursive(
    [
        'a'      => 1,
        'b'      => '1',
        'truthy' => ['a' => 1, 'b' => true, 'c' => '1'],
        'falsy'  => ['c' => [], 'd' => [], 'e' => null]
    ],
    [
        'a'      => '1',
        'b'      => 1,
        'truthy' => ['a' => '1', 'b' => 1, 'c' => true],
        'falsy'  => ['c' => 0, 'd' => null, 'e' => 0]
    ],
    true
);
$expect = [
    'a'      => 1,
    'b'      => '1',
    'truthy' => ['a' => 1, 'b' => true, 'c' => '1'],
    'falsy'  => ['c' => [], 'd' => [], 'e' => null]
];
var_dump_when_fail($res === $expect, $res);

var_dump('型の混在がある配列の比較(緩やかな比較)');
$res    = array_diff_recursive(
    ['a' => 1, 'b' => true, 'c' => '1'],
    ['a' => '1', 'b' => 1, 'c' => true],
    false
);
$expect = [];
var_dump_when_fail($res === $expect, $res);

var_dump('オブジェクトを使ったカスタム比較');
$makeObj = fn($v) => (object)['v' => $v];
$res     = array_diff_recursive(
    ['a' => $a = $makeObj(1), 'b' => $b = $makeObj(2)],
    ['a' => $makeObj('1'), 'b' => $makeObj(2)],
    fn($a, $b) => $a->v === $b->v
);
$expect  = ['a' => $a];
var_dump_when_fail($res === $expect, $res);


 array_diff_recursive は渡された配列Aの中を総ざらいし、その最中にもう一つの渡された配列Bを配列Aと同じキーで参照、比較することによって差異の検出を行います。この関数はやんごとなき事情によってオブジェクトが等しいかどうかを判定する必要がある場合にも対応しています(callable|boolの$compareMethodというのはもういくらかわかりやすい書き方ができる余地がありそうではありますが)。

 このコードの注意点として循環参照を全く考慮していない点があります。そうそう使うことはないでしょうがご注意ください。同じキーで同じ形の循環参照に入ると無限ループに入ります。

>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG