1
\$\begingroup\$

I need to update a nested array of settings. There are several difficulties that I managed to solve, but I'm pretty sure the code can be written much simpler. I just don't know how, which is why I'm asking for a simpler solution.

Problem:

  • I have a nested array of settings in the database.
  • I have a form with fields that need to update the database.
  • The form contains checkboxes. When the form is sent, the empty checkbox fields don't get transmitted as keys in the form $input array. They are just missing.
  • I have settings in the database that don't get updated with the form and need to be preserved.

It is not possible to just use array_merge(). It doesn't work recursively. And, it will not update a checkbox setting from true to false, because the form $input array doesn't contain the key.

Also we must preserve all settings that are not being updated with the form.

What I've done:

  1. Create an array with all keys that are not part of the form.
  2. Merge that array recursively with the form $input. (This ensures that existing non-form keys get preserved with the next merge function. But, we are still missing the key for the empty checkboxes.)
  3. Created a function that iterates recursively through the input array and the database array. If keys on boths sides exist, the form input key overwrites the database key. If the form input key is missing, the database key is set to false.

Here is my working code. It contains a few helpers. It tests if the final processed array is equal to the desired output or not. So this should make it easier to jump in and test quickly.

$input_from_db = [
    'first' => [
        'one'   => '123123132', // form textfield textfield with text, change
        'two'   => '',          // form, empty textfield, don't change
        'three' => 0,           // form, checkbox, false, change to true
        'four'  => 1,           // form, checkbox, true, change to false
        'five'  => 1,           // form, checkbox, true, keep
        'six'   => 0,           // form, checkbox, false, keep
        'seven' => 'testdata'   // not in form, must be preserved
    ],
    'second' => [               // test some deep nested arrays too
        'one' => 0,             // change to true
        'two' => 1,             // change to false with empty key
        'three' => [
            'a' => 'uno',
            'b' => 'due'
        ]
    ],
    'third'  => '2', // not in form, must be preserved
    'fourth' => '3' // not in form, must be preserved
];

$input_from_form = [
    'first' => [
        'one'   => 'abababab',
        'two'   => '',
        'three' => 1,
        // four is missing from the form input because it was sent empty
        'five'  => 1,
        // six is missing from the form input because it was sent empty
        // seven is missing because it is no part of the form
    ],
    'second' => [ //test some nested array too
        'one' => 1,
        // form field, must come back with zero
        'three' => [
            // a is not in form, preserve
            'b' => 'due_new',
            // c is an empty form checkbox
        ]
    ],
    'third' => '2',
    // four not in form, must be preserved
];

$must_be_result = [
    'first' => [
        'one'   => 'abababab', // changed in the form to 'abababab'
        'two'   => '',
        'three' => 1,
        'four'  => 0, // because it IS in the form, but was unchecked, it must be present again, with value zero
        'five'  => 1,
        'six'   => 0,
        'seven' => 'testdata' // not in form, must be preserved
    ],
    'second' => [
        'one' => 1,
        'two' => 0,
        'three' => [
            'a' => 'uno',
            'b' => 'due_new',
            'c' => 0,
        ]
    ],
    'third' => '2',
    'fourth'  => '3'
];
        
// list of keys that the form will never return because they are not part of the form fields
function non_form_keys($array_existing): array
{
    return [
        'first' => [
            'seven' => $array_existing['first']['seven'],
        ], 
        'second' => [
            'three' => [
                'a' => 'uno'
            ]
        ],
        'third' => $array_existing['third'],
        'fourth' => $array_existing['fourth'],
    ];
}

function update_options($array_existing, $array_input)
{
    $output_array = [];

    foreach ($array_existing as $key => $value)
    {
        if(array_key_exists($key, $array_input)){
            if(is_array($value)){

                $output_array[$key] = update_options($value, $array_input[$key]);

            } else {
                $output_array[$key] = $array_input[$key];
            }
        } else {
            if(is_array($value)){
                $output_array[$key] = set_to_zero($value);
            } else {
                $output_array[$key] = 0;
            }
        }
    }

    return $output_array;
}

function set_to_zero($array){

    foreach ($array as $key => $value) {
        if(is_array($value)) {
            $array[$key] = set_to_zero($value);
        } else {
            $array[$key] = 0;
        }
    }

    return $array;
}

// this is just a test that helps comparing my computed array with
// with an array that contains the result I want to see 
function arrayRecursiveDiff($aArray1, $aArray2)
{
    $aReturn = array();

    foreach ($aArray1 as $mKey => $mValue) {
        if (array_key_exists($mKey, $aArray2)) {
            if (is_array($mValue)) {
                $aRecursiveDiff = arrayRecursiveDiff($mValue, $aArray2[$mKey]);
                if (count($aRecursiveDiff)) {
                    $aReturn[$mKey] = $aRecursiveDiff;
                }
            } else {
                if ($mValue != $aArray2[$mKey]) {
                    $aReturn[$mKey] = $mValue;
                }
            }
        } else {
            $aReturn[$mKey] = $mValue;
        }
    }
    return $aReturn;
}

// small helper to either print success or the 
// diff array
function check_result($array_diff)
{
    if(empty($array_diff)){
        echo 'success' . PHP_EOL; 
    } else {
        echo 'array diff' . PHP_EOL;
        print_r($array_diff);
    }
}

$input_from_form_updated = array_replace_recursive(non_form_keys($input_from_db), $input_from_form);

$result = update_options($input_from_db, $input_from_form_updated);

$array_diff = arrayRecursiveDiff($result, $must_be_result);

check_result($array_diff);
\$\endgroup\$
2
\$\begingroup\$
  • set_to_zero() is a "re-invention of the wheel". Just use the native array_walk_recursive() function to modify leafnodes.

    array_walk_recursive(
        $array,
        function(&$leafnode) {
            $leafnode = 0;
        }
    );
    

    Or from PHP7.4:

    array_walk_recursive($array, fn(&$leafnode) => $leafnode = 0);
    
  • To use the above in update_options(), I recommend using reference variables instead of declaring a new array to be returned.

  • You should employ consistent method naming styles. I prefer camel-case over snake_case.

  • empty($array_diff) can be safely trduced to !$array_diff because the variable is assured to be declared.

\$\endgroup\$
2
  • \$\begingroup\$ Is there a way to compare two nested arrays with array_walk_recursive()? \$\endgroup\$
    – alev
    Jan 13 at 4:37
  • \$\begingroup\$ No, because it does not keep track of where it is while iterating leafnodes. \$\endgroup\$ Jan 13 at 8:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.