NestedArray.php 12.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11
<?php

/**
 * @file
 * Contains Drupal\Component\Utility\NestedArray.
 */

namespace Drupal\Component\Utility;

/**
 * Provides helpers to perform operations on nested arrays and array keys of variable depth.
12 13
 *
 * @ingroup utility
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
 */
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
   *
   * The return value will be NULL, regardless of whether the actual value is
   * NULL or whether the requested key does not exist. 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'] = array(
   *   '#type' => 'text_format',
   *   '#title' => t('Signature'),
   * );
   * // Or, it might be further nested:
   * $form['signature_settings']['user']['signature'] = array(
   *   '#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 array('signature_settings', 'signature') @endcode
   * in the first case or
   * @code array('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, PHP throws an error 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 ($force && isset($ref) && !is_array($ref)) {
        $ref = array();
      }
      $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'] = array(
   *   '#type' => 'text_format',
   *   '#title' => t('Signature'),
   * );
   * // Or, it might be further nested:
   * $form['signature_settings']['user']['signature'] = array(
   *   '#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 array('signature_settings', 'signature') @endcode
   * in the first case or
   * @code array('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::unset_nested_value($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 = array('fragment' => 'x', 'attributes' => array('title' => t('X'), 'class' => array('a', 'b')));
   * $link_options_2 = array('fragment' => 'y', 'attributes' => array('title' => t('Y'), 'class' => array('c', 'd')));
   *
   * // This results in array('fragment' => array('x', 'y'), 'attributes' => array('title' => array(t('X'), t('Y')), 'class' => array('a', 'b', 'c', 'd'))).
   * $incorrect = array_merge_recursive($link_options_1, $link_options_2);
   *
   * // This results in array('fragment' => 'y', 'attributes' => array('title' => t('Y'), 'class' => array('a', 'b', 'c', 'd'))).
   * $correct = NestedArray::mergeDeep($link_options_1, $link_options_2);
   * @endcode
   *
   * @param array ...
   *   Arrays to merge.
   *
294 295 296 297
   * @param bool $preserve_integer_keys
   *   (optional) If given, integer keys will be preserved and merged instead of
   *   appended.
   *
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
   * @return array
   *   The merged array.
   *
   * @see NestedArray::mergeDeepArray()
   */
  public static function mergeDeep() {
    return self::mergeDeepArray(func_get_args());
  }

  /**
   * 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(array($a, $b));
   *
   * The following are also equivalent:
   * - call_user_func_array('NestedArray::mergeDeep', $arrays_to_merge);
   * - NestedArray::mergeDeepArray($arrays_to_merge);
   *
   * @see NestedArray::mergeDeep()
   */
324
  public static function mergeDeepArray(array $arrays, $preserve_integer_keys = FALSE) {
325 326 327
    $result = array();
    foreach ($arrays as $array) {
      foreach ($array as $key => $value) {
328 329 330 331
        // 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_integer($key) && !$preserve_integer_keys) {
332 333 334 335
          $result[] = $value;
        }
        // Recurse when both values are arrays.
        elseif (isset($result[$key]) && is_array($result[$key]) && is_array($value)) {
336
          $result[$key] = self::mergeDeepArray(array($result[$key], $value), $preserve_integer_keys);
337 338 339 340 341 342 343 344 345 346 347
        }
        // Otherwise, use the latter value, overriding any previous value.
        else {
          $result[$key] = $value;
        }
      }
    }
    return $result;
  }

}