NestedArray.php

Same filename in other branches
  1. 9 core/lib/Drupal/Component/Utility/NestedArray.php
  2. 8.9.x core/lib/Drupal/Component/Utility/NestedArray.php
  3. 10 core/lib/Drupal/Component/Utility/NestedArray.php

Namespace

Drupal\Component\Utility

File

core/lib/Drupal/Component/Utility/NestedArray.php

View source
<?php

namespace Drupal\Component\Utility;


/**
 * Provides helpers to perform operations on nested arrays and array keys of variable depth.
 *
 * @ingroup utility
 */
class NestedArray {
    
    /**
     * Retrieves a value from a nested array with variable depth.
     *
     * This helper function should be used when the depth of the array element
     * being retrieved may vary (that is, the number of parent keys is variable).
     * It is primarily used for form structures and renderable arrays.
     *
     * Without this helper function the only way to get a nested array value with
     * variable depth in one line would be using eval(), which should be avoided:
     * @code
     * // Do not do this! Avoid eval().
     * // May also throw a PHP notice, if the variable array keys do not exist.
     * eval('$value = $array[\'' . implode("']['", $parents) . "'];");
     * @endcode
     *
     * Instead, use this helper function:
     * @code
     * $value = NestedArray::getValue($form, $parents);
     * @endcode
     *
     * A return value of NULL is ambiguous, and can mean either that the requested
     * key does not exist, or that the actual value is NULL. If it is required to
     * know whether the nested array key actually exists, pass a third argument
     * that is altered by reference:
     * @code
     * $key_exists = NULL;
     * $value = NestedArray::getValue($form, $parents, $key_exists);
     * if ($key_exists) {
     *   // Do something with $value.
     * }
     * @endcode
     *
     * However if the number of array parent keys is static, the value should
     * always be retrieved directly rather than calling this function.
     * For instance:
     * @code
     * $value = $form['signature_settings']['signature'];
     * @endcode
     *
     * @param array $array
     *   The array from which to get the value.
     * @param array $parents
     *   An array of parent keys of the value, starting with the outermost key.
     * @param bool $key_exists
     *   (optional) If given, an already defined variable that is altered by
     *   reference.
     *
     * @return mixed
     *   The requested nested value. Possibly NULL if the value is NULL or not all
     *   nested parent keys exist. $key_exists is altered by reference and is a
     *   Boolean that indicates whether all nested parent keys exist (TRUE) or not
     *   (FALSE). This allows to distinguish between the two possibilities when
     *   NULL is returned.
     *
     * @see NestedArray::setValue()
     * @see NestedArray::unsetValue()
     */
    public static function &getValue(array &$array, array $parents, &$key_exists = NULL) {
        $ref =& $array;
        foreach ($parents as $parent) {
            if (is_array($ref) && \array_key_exists($parent, $ref)) {
                $ref =& $ref[$parent];
            }
            else {
                $key_exists = FALSE;
                $null = NULL;
                return $null;
            }
        }
        $key_exists = TRUE;
        return $ref;
    }
    
    /**
     * Sets a value in a nested array with variable depth.
     *
     * This helper function should be used when the depth of the array element you
     * are changing may vary (that is, the number of parent keys is variable). It
     * is primarily used for form structures and renderable arrays.
     *
     * Example:
     * @code
     * // Assume you have a 'signature' element somewhere in a form. It might be:
     * $form['signature_settings']['signature'] = [
     *   '#type' => 'text_format',
     *   '#title' => t('Signature'),
     * ];
     * // Or, it might be further nested:
     * $form['signature_settings']['user']['signature'] = [
     *   '#type' => 'text_format',
     *   '#title' => t('Signature'),
     * ];
     * @endcode
     *
     * To deal with the situation, the code needs to figure out the route to the
     * element, given an array of parents that is either
     * @code ['signature_settings', 'signature'] @endcode
     * in the first case or
     * @code ['signature_settings', 'user', 'signature'] @endcode
     * in the second case.
     *
     * Without this helper function the only way to set the signature element in
     * one line would be using eval(), which should be avoided:
     * @code
     * // Do not do this! Avoid eval().
     * eval('$form[\'' . implode("']['", $parents) . '\'] = $element;');
     * @endcode
     *
     * Instead, use this helper function:
     * @code
     * NestedArray::setValue($form, $parents, $element);
     * @endcode
     *
     * However if the number of array parent keys is static, the value should
     * always be set directly rather than calling this function. For instance,
     * for the first example we could just do:
     * @code
     * $form['signature_settings']['signature'] = $element;
     * @endcode
     *
     * @param array $array
     *   A reference to the array to modify.
     * @param array $parents
     *   An array of parent keys, starting with the outermost key.
     * @param mixed $value
     *   The value to set.
     * @param bool $force
     *   (optional) If TRUE, the value is forced into the structure even if it
     *   requires the deletion of an already existing non-array parent value. If
     *   FALSE, throws an exception if trying to add into a value that is not an
     *   array. Defaults to FALSE.
     *
     * @see NestedArray::unsetValue()
     * @see NestedArray::getValue()
     */
    public static function setValue(array &$array, array $parents, $value, $force = FALSE) {
        $ref =& $array;
        foreach ($parents as $parent) {
            // PHP auto-creates container arrays and NULL entries without error if $ref
            // is NULL, but throws an error if $ref is set, but not an array.
            if (isset($ref) && !is_array($ref)) {
                if (!$force) {
                    throw new \LogicException('Cannot create key "' . $parent . '" on non-array value.');
                }
                $ref = [];
            }
            $ref =& $ref[$parent];
        }
        $ref = $value;
    }
    
    /**
     * Unsets a value in a nested array with variable depth.
     *
     * This helper function should be used when the depth of the array element you
     * are changing may vary (that is, the number of parent keys is variable). It
     * is primarily used for form structures and renderable arrays.
     *
     * Example:
     * @code
     * // Assume you have a 'signature' element somewhere in a form. It might be:
     * $form['signature_settings']['signature'] = [
     *   '#type' => 'text_format',
     *   '#title' => t('Signature'),
     * ];
     * // Or, it might be further nested:
     * $form['signature_settings']['user']['signature'] = [
     *   '#type' => 'text_format',
     *   '#title' => t('Signature'),
     * ];
     * @endcode
     *
     * To deal with the situation, the code needs to figure out the route to the
     * element, given an array of parents that is either
     * @code ['signature_settings', 'signature'] @endcode
     * in the first case or
     * @code ['signature_settings', 'user', 'signature'] @endcode
     * in the second case.
     *
     * Without this helper function the only way to unset the signature element in
     * one line would be using eval(), which should be avoided:
     * @code
     * // Do not do this! Avoid eval().
     * eval('unset($form[\'' . implode("']['", $parents) . '\']);');
     * @endcode
     *
     * Instead, use this helper function:
     * @code
     * NestedArray::unsetValue($form, $parents, $element);
     * @endcode
     *
     * However if the number of array parent keys is static, the value should
     * always be set directly rather than calling this function. For instance, for
     * the first example we could just do:
     * @code
     * unset($form['signature_settings']['signature']);
     * @endcode
     *
     * @param array $array
     *   A reference to the array to modify.
     * @param array $parents
     *   An array of parent keys, starting with the outermost key and including
     *   the key to be unset.
     * @param bool $key_existed
     *   (optional) If given, an already defined variable that is altered by
     *   reference.
     *
     * @see NestedArray::setValue()
     * @see NestedArray::getValue()
     */
    public static function unsetValue(array &$array, array $parents, &$key_existed = NULL) {
        $unset_key = array_pop($parents);
        $ref =& self::getValue($array, $parents, $key_existed);
        if ($key_existed && is_array($ref) && \array_key_exists($unset_key, $ref)) {
            $key_existed = TRUE;
            unset($ref[$unset_key]);
        }
        else {
            $key_existed = FALSE;
        }
    }
    
    /**
     * Determines whether a nested array contains the requested keys.
     *
     * This helper function should be used when the depth of the array element to
     * be checked may vary (that is, the number of parent keys is variable). See
     * NestedArray::setValue() for details. It is primarily used for form
     * structures and renderable arrays.
     *
     * If it is required to also get the value of the checked nested key, use
     * NestedArray::getValue() instead.
     *
     * If the number of array parent keys is static, this helper function is
     * unnecessary and the following code can be used instead:
     * @code
     * $value_exists = isset($form['signature_settings']['signature']);
     * $key_exists = array_key_exists('signature', $form['signature_settings']);
     * @endcode
     *
     * @param array $array
     *   The array with the value to check for.
     * @param array $parents
     *   An array of parent keys of the value, starting with the outermost key.
     *
     * @return bool
     *   TRUE if all the parent keys exist, FALSE otherwise.
     *
     * @see NestedArray::getValue()
     */
    public static function keyExists(array $array, array $parents) {
        // Although this function is similar to PHP's array_key_exists(), its
        // arguments should be consistent with getValue().
        $key_exists = NULL;
        self::getValue($array, $parents, $key_exists);
        return $key_exists;
    }
    
    /**
     * Merges multiple arrays, recursively, and returns the merged array.
     *
     * This function is similar to PHP's array_merge_recursive() function, but it
     * handles non-array values differently. When merging values that are not both
     * arrays, the latter value replaces the former rather than merging with it.
     *
     * Example:
     * @code
     * $link_options_1 = ['fragment' => 'x', 'attributes' => ['title' => t('X'), 'class' => ['a', 'b']]];
     * $link_options_2 = ['fragment' => 'y', 'attributes' => ['title' => t('Y'), 'class' => ['c', 'd']]];
     *
     * // This results in ['fragment' => ['x', 'y'], 'attributes' => ['title' => [t('X'), t('Y')], 'class' => ['a', 'b', 'c', 'd']]].
     * $incorrect = array_merge_recursive($link_options_1, $link_options_2);
     *
     * // This results in ['fragment' => 'y', 'attributes' => ['title' => t('Y'), 'class' => ['a', 'b', 'c', 'd']]].
     * $correct = NestedArray::mergeDeep($link_options_1, $link_options_2);
     * @endcode
     *
     * @param array ...$arrays
     *   Arrays to merge.
     *
     * @return array
     *   The merged array.
     *
     * @see NestedArray::mergeDeepArray()
     */
    public static function mergeDeep(...$arrays) {
        return self::mergeDeepArray($arrays);
    }
    
    /**
     * Merges multiple arrays, recursively, and returns the merged array.
     *
     * This function is equivalent to NestedArray::mergeDeep(), except the
     * input arrays are passed as a single array parameter rather than a variable
     * parameter list.
     *
     * The following are equivalent:
     * - NestedArray::mergeDeep($a, $b);
     * - NestedArray::mergeDeepArray([$a, $b]);
     *
     * The following are also equivalent:
     * - call_user_func_array('NestedArray::mergeDeep', $arrays_to_merge);
     * - NestedArray::mergeDeepArray($arrays_to_merge);
     *
     * @param array $arrays
     *   An arrays of arrays to merge.
     * @param bool $preserve_integer_keys
     *   (optional) If given, integer keys will be preserved and merged instead of
     *   appended. Defaults to FALSE.
     *
     * @return array
     *   The merged array.
     *
     * @see NestedArray::mergeDeep()
     */
    public static function mergeDeepArray(array $arrays, $preserve_integer_keys = FALSE) {
        $result = [];
        foreach ($arrays as $array) {
            foreach ($array as $key => $value) {
                // Renumber integer keys as array_merge_recursive() does unless
                // $preserve_integer_keys is set to TRUE. Note that PHP automatically
                // converts array keys that are integer strings (e.g., '1') to integers.
                if (is_int($key) && !$preserve_integer_keys) {
                    $result[] = $value;
                }
                elseif (isset($result[$key]) && is_array($result[$key]) && is_array($value)) {
                    $result[$key] = self::mergeDeepArray([
                        $result[$key],
                        $value,
                    ], $preserve_integer_keys);
                }
                else {
                    $result[$key] = $value;
                }
            }
        }
        return $result;
    }
    
    /**
     * Filters a nested array recursively.
     *
     * @param array $array
     *   The filtered nested array.
     * @param callable|null $callable
     *   The callable to apply for filtering.
     *
     * @return array
     *   The filtered array.
     */
    public static function filter(array $array, ?callable $callable = NULL) {
        $array = is_callable($callable) ? array_filter($array, $callable) : array_filter($array);
        foreach ($array as &$element) {
            if (is_array($element)) {
                $element = static::filter($element, $callable);
            }
        }
        return $array;
    }

}

Classes

Title Deprecated Summary
NestedArray Provides helpers to perform operations on nested arrays and array keys of variable depth.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.