annotate core/lib/Drupal/Component/Utility/NestedArray.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 1fec387a4317
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\Component\Utility;
Chris@0 4
Chris@0 5 /**
Chris@0 6 * Provides helpers to perform operations on nested arrays and array keys of variable depth.
Chris@0 7 *
Chris@0 8 * @ingroup utility
Chris@0 9 */
Chris@0 10 class NestedArray {
Chris@0 11
Chris@0 12 /**
Chris@0 13 * Retrieves a value from a nested array with variable depth.
Chris@0 14 *
Chris@0 15 * This helper function should be used when the depth of the array element
Chris@0 16 * being retrieved may vary (that is, the number of parent keys is variable).
Chris@0 17 * It is primarily used for form structures and renderable arrays.
Chris@0 18 *
Chris@0 19 * Without this helper function the only way to get a nested array value with
Chris@0 20 * variable depth in one line would be using eval(), which should be avoided:
Chris@0 21 * @code
Chris@0 22 * // Do not do this! Avoid eval().
Chris@0 23 * // May also throw a PHP notice, if the variable array keys do not exist.
Chris@0 24 * eval('$value = $array[\'' . implode("']['", $parents) . "'];");
Chris@0 25 * @endcode
Chris@0 26 *
Chris@0 27 * Instead, use this helper function:
Chris@0 28 * @code
Chris@0 29 * $value = NestedArray::getValue($form, $parents);
Chris@0 30 * @endcode
Chris@0 31 *
Chris@0 32 * A return value of NULL is ambiguous, and can mean either that the requested
Chris@0 33 * key does not exist, or that the actual value is NULL. If it is required to
Chris@0 34 * know whether the nested array key actually exists, pass a third argument
Chris@0 35 * that is altered by reference:
Chris@0 36 * @code
Chris@0 37 * $key_exists = NULL;
Chris@0 38 * $value = NestedArray::getValue($form, $parents, $key_exists);
Chris@0 39 * if ($key_exists) {
Chris@0 40 * // Do something with $value.
Chris@0 41 * }
Chris@0 42 * @endcode
Chris@0 43 *
Chris@0 44 * However if the number of array parent keys is static, the value should
Chris@0 45 * always be retrieved directly rather than calling this function.
Chris@0 46 * For instance:
Chris@0 47 * @code
Chris@0 48 * $value = $form['signature_settings']['signature'];
Chris@0 49 * @endcode
Chris@0 50 *
Chris@0 51 * @param array $array
Chris@0 52 * The array from which to get the value.
Chris@0 53 * @param array $parents
Chris@0 54 * An array of parent keys of the value, starting with the outermost key.
Chris@0 55 * @param bool $key_exists
Chris@0 56 * (optional) If given, an already defined variable that is altered by
Chris@0 57 * reference.
Chris@0 58 *
Chris@0 59 * @return mixed
Chris@0 60 * The requested nested value. Possibly NULL if the value is NULL or not all
Chris@0 61 * nested parent keys exist. $key_exists is altered by reference and is a
Chris@0 62 * Boolean that indicates whether all nested parent keys exist (TRUE) or not
Chris@0 63 * (FALSE). This allows to distinguish between the two possibilities when
Chris@0 64 * NULL is returned.
Chris@0 65 *
Chris@0 66 * @see NestedArray::setValue()
Chris@0 67 * @see NestedArray::unsetValue()
Chris@0 68 */
Chris@0 69 public static function &getValue(array &$array, array $parents, &$key_exists = NULL) {
Chris@0 70 $ref = &$array;
Chris@0 71 foreach ($parents as $parent) {
Chris@14 72 if (is_array($ref) && (isset($ref[$parent]) || array_key_exists($parent, $ref))) {
Chris@0 73 $ref = &$ref[$parent];
Chris@0 74 }
Chris@0 75 else {
Chris@0 76 $key_exists = FALSE;
Chris@0 77 $null = NULL;
Chris@0 78 return $null;
Chris@0 79 }
Chris@0 80 }
Chris@0 81 $key_exists = TRUE;
Chris@0 82 return $ref;
Chris@0 83 }
Chris@0 84
Chris@0 85 /**
Chris@0 86 * Sets a value in a nested array with variable depth.
Chris@0 87 *
Chris@0 88 * This helper function should be used when the depth of the array element you
Chris@0 89 * are changing may vary (that is, the number of parent keys is variable). It
Chris@0 90 * is primarily used for form structures and renderable arrays.
Chris@0 91 *
Chris@0 92 * Example:
Chris@0 93 * @code
Chris@0 94 * // Assume you have a 'signature' element somewhere in a form. It might be:
Chris@0 95 * $form['signature_settings']['signature'] = array(
Chris@0 96 * '#type' => 'text_format',
Chris@0 97 * '#title' => t('Signature'),
Chris@0 98 * );
Chris@0 99 * // Or, it might be further nested:
Chris@0 100 * $form['signature_settings']['user']['signature'] = array(
Chris@0 101 * '#type' => 'text_format',
Chris@0 102 * '#title' => t('Signature'),
Chris@0 103 * );
Chris@0 104 * @endcode
Chris@0 105 *
Chris@0 106 * To deal with the situation, the code needs to figure out the route to the
Chris@0 107 * element, given an array of parents that is either
Chris@0 108 * @code array('signature_settings', 'signature') @endcode
Chris@0 109 * in the first case or
Chris@0 110 * @code array('signature_settings', 'user', 'signature') @endcode
Chris@0 111 * in the second case.
Chris@0 112 *
Chris@0 113 * Without this helper function the only way to set the signature element in
Chris@0 114 * one line would be using eval(), which should be avoided:
Chris@0 115 * @code
Chris@0 116 * // Do not do this! Avoid eval().
Chris@0 117 * eval('$form[\'' . implode("']['", $parents) . '\'] = $element;');
Chris@0 118 * @endcode
Chris@0 119 *
Chris@0 120 * Instead, use this helper function:
Chris@0 121 * @code
Chris@0 122 * NestedArray::setValue($form, $parents, $element);
Chris@0 123 * @endcode
Chris@0 124 *
Chris@0 125 * However if the number of array parent keys is static, the value should
Chris@0 126 * always be set directly rather than calling this function. For instance,
Chris@0 127 * for the first example we could just do:
Chris@0 128 * @code
Chris@0 129 * $form['signature_settings']['signature'] = $element;
Chris@0 130 * @endcode
Chris@0 131 *
Chris@0 132 * @param array $array
Chris@0 133 * A reference to the array to modify.
Chris@0 134 * @param array $parents
Chris@0 135 * An array of parent keys, starting with the outermost key.
Chris@0 136 * @param mixed $value
Chris@0 137 * The value to set.
Chris@0 138 * @param bool $force
Chris@0 139 * (optional) If TRUE, the value is forced into the structure even if it
Chris@0 140 * requires the deletion of an already existing non-array parent value. If
Chris@0 141 * FALSE, PHP throws an error if trying to add into a value that is not an
Chris@0 142 * array. Defaults to FALSE.
Chris@0 143 *
Chris@0 144 * @see NestedArray::unsetValue()
Chris@0 145 * @see NestedArray::getValue()
Chris@0 146 */
Chris@0 147 public static function setValue(array &$array, array $parents, $value, $force = FALSE) {
Chris@0 148 $ref = &$array;
Chris@0 149 foreach ($parents as $parent) {
Chris@0 150 // PHP auto-creates container arrays and NULL entries without error if $ref
Chris@0 151 // is NULL, but throws an error if $ref is set, but not an array.
Chris@0 152 if ($force && isset($ref) && !is_array($ref)) {
Chris@0 153 $ref = [];
Chris@0 154 }
Chris@0 155 $ref = &$ref[$parent];
Chris@0 156 }
Chris@0 157 $ref = $value;
Chris@0 158 }
Chris@0 159
Chris@0 160 /**
Chris@0 161 * Unsets a value in a nested array with variable depth.
Chris@0 162 *
Chris@0 163 * This helper function should be used when the depth of the array element you
Chris@0 164 * are changing may vary (that is, the number of parent keys is variable). It
Chris@0 165 * is primarily used for form structures and renderable arrays.
Chris@0 166 *
Chris@0 167 * Example:
Chris@0 168 * @code
Chris@0 169 * // Assume you have a 'signature' element somewhere in a form. It might be:
Chris@0 170 * $form['signature_settings']['signature'] = array(
Chris@0 171 * '#type' => 'text_format',
Chris@0 172 * '#title' => t('Signature'),
Chris@0 173 * );
Chris@0 174 * // Or, it might be further nested:
Chris@0 175 * $form['signature_settings']['user']['signature'] = array(
Chris@0 176 * '#type' => 'text_format',
Chris@0 177 * '#title' => t('Signature'),
Chris@0 178 * );
Chris@0 179 * @endcode
Chris@0 180 *
Chris@0 181 * To deal with the situation, the code needs to figure out the route to the
Chris@0 182 * element, given an array of parents that is either
Chris@0 183 * @code array('signature_settings', 'signature') @endcode
Chris@0 184 * in the first case or
Chris@0 185 * @code array('signature_settings', 'user', 'signature') @endcode
Chris@0 186 * in the second case.
Chris@0 187 *
Chris@0 188 * Without this helper function the only way to unset the signature element in
Chris@0 189 * one line would be using eval(), which should be avoided:
Chris@0 190 * @code
Chris@0 191 * // Do not do this! Avoid eval().
Chris@0 192 * eval('unset($form[\'' . implode("']['", $parents) . '\']);');
Chris@0 193 * @endcode
Chris@0 194 *
Chris@0 195 * Instead, use this helper function:
Chris@0 196 * @code
Chris@0 197 * NestedArray::unset_nested_value($form, $parents, $element);
Chris@0 198 * @endcode
Chris@0 199 *
Chris@0 200 * However if the number of array parent keys is static, the value should
Chris@0 201 * always be set directly rather than calling this function. For instance, for
Chris@0 202 * the first example we could just do:
Chris@0 203 * @code
Chris@0 204 * unset($form['signature_settings']['signature']);
Chris@0 205 * @endcode
Chris@0 206 *
Chris@0 207 * @param array $array
Chris@0 208 * A reference to the array to modify.
Chris@0 209 * @param array $parents
Chris@0 210 * An array of parent keys, starting with the outermost key and including
Chris@0 211 * the key to be unset.
Chris@0 212 * @param bool $key_existed
Chris@0 213 * (optional) If given, an already defined variable that is altered by
Chris@0 214 * reference.
Chris@0 215 *
Chris@0 216 * @see NestedArray::setValue()
Chris@0 217 * @see NestedArray::getValue()
Chris@0 218 */
Chris@0 219 public static function unsetValue(array &$array, array $parents, &$key_existed = NULL) {
Chris@0 220 $unset_key = array_pop($parents);
Chris@0 221 $ref = &self::getValue($array, $parents, $key_existed);
Chris@14 222 if ($key_existed && is_array($ref) && (isset($ref[$unset_key]) || array_key_exists($unset_key, $ref))) {
Chris@0 223 $key_existed = TRUE;
Chris@0 224 unset($ref[$unset_key]);
Chris@0 225 }
Chris@0 226 else {
Chris@0 227 $key_existed = FALSE;
Chris@0 228 }
Chris@0 229 }
Chris@0 230
Chris@0 231 /**
Chris@0 232 * Determines whether a nested array contains the requested keys.
Chris@0 233 *
Chris@0 234 * This helper function should be used when the depth of the array element to
Chris@0 235 * be checked may vary (that is, the number of parent keys is variable). See
Chris@0 236 * NestedArray::setValue() for details. It is primarily used for form
Chris@0 237 * structures and renderable arrays.
Chris@0 238 *
Chris@0 239 * If it is required to also get the value of the checked nested key, use
Chris@0 240 * NestedArray::getValue() instead.
Chris@0 241 *
Chris@0 242 * If the number of array parent keys is static, this helper function is
Chris@0 243 * unnecessary and the following code can be used instead:
Chris@0 244 * @code
Chris@0 245 * $value_exists = isset($form['signature_settings']['signature']);
Chris@0 246 * $key_exists = array_key_exists('signature', $form['signature_settings']);
Chris@0 247 * @endcode
Chris@0 248 *
Chris@0 249 * @param array $array
Chris@0 250 * The array with the value to check for.
Chris@0 251 * @param array $parents
Chris@0 252 * An array of parent keys of the value, starting with the outermost key.
Chris@0 253 *
Chris@0 254 * @return bool
Chris@0 255 * TRUE if all the parent keys exist, FALSE otherwise.
Chris@0 256 *
Chris@0 257 * @see NestedArray::getValue()
Chris@0 258 */
Chris@0 259 public static function keyExists(array $array, array $parents) {
Chris@0 260 // Although this function is similar to PHP's array_key_exists(), its
Chris@0 261 // arguments should be consistent with getValue().
Chris@0 262 $key_exists = NULL;
Chris@0 263 self::getValue($array, $parents, $key_exists);
Chris@0 264 return $key_exists;
Chris@0 265 }
Chris@0 266
Chris@0 267 /**
Chris@0 268 * Merges multiple arrays, recursively, and returns the merged array.
Chris@0 269 *
Chris@0 270 * This function is similar to PHP's array_merge_recursive() function, but it
Chris@0 271 * handles non-array values differently. When merging values that are not both
Chris@0 272 * arrays, the latter value replaces the former rather than merging with it.
Chris@0 273 *
Chris@0 274 * Example:
Chris@0 275 * @code
Chris@0 276 * $link_options_1 = array('fragment' => 'x', 'attributes' => array('title' => t('X'), 'class' => array('a', 'b')));
Chris@0 277 * $link_options_2 = array('fragment' => 'y', 'attributes' => array('title' => t('Y'), 'class' => array('c', 'd')));
Chris@0 278 *
Chris@0 279 * // This results in array('fragment' => array('x', 'y'), 'attributes' => array('title' => array(t('X'), t('Y')), 'class' => array('a', 'b', 'c', 'd'))).
Chris@0 280 * $incorrect = array_merge_recursive($link_options_1, $link_options_2);
Chris@0 281 *
Chris@0 282 * // This results in array('fragment' => 'y', 'attributes' => array('title' => t('Y'), 'class' => array('a', 'b', 'c', 'd'))).
Chris@0 283 * $correct = NestedArray::mergeDeep($link_options_1, $link_options_2);
Chris@0 284 * @endcode
Chris@0 285 *
Chris@0 286 * @param array ...
Chris@0 287 * Arrays to merge.
Chris@0 288 *
Chris@0 289 * @return array
Chris@0 290 * The merged array.
Chris@0 291 *
Chris@0 292 * @see NestedArray::mergeDeepArray()
Chris@0 293 */
Chris@0 294 public static function mergeDeep() {
Chris@0 295 return self::mergeDeepArray(func_get_args());
Chris@0 296 }
Chris@0 297
Chris@0 298 /**
Chris@0 299 * Merges multiple arrays, recursively, and returns the merged array.
Chris@0 300 *
Chris@0 301 * This function is equivalent to NestedArray::mergeDeep(), except the
Chris@0 302 * input arrays are passed as a single array parameter rather than a variable
Chris@0 303 * parameter list.
Chris@0 304 *
Chris@0 305 * The following are equivalent:
Chris@0 306 * - NestedArray::mergeDeep($a, $b);
Chris@0 307 * - NestedArray::mergeDeepArray(array($a, $b));
Chris@0 308 *
Chris@0 309 * The following are also equivalent:
Chris@0 310 * - call_user_func_array('NestedArray::mergeDeep', $arrays_to_merge);
Chris@0 311 * - NestedArray::mergeDeepArray($arrays_to_merge);
Chris@0 312 *
Chris@0 313 * @param array $arrays
Chris@0 314 * An arrays of arrays to merge.
Chris@0 315 * @param bool $preserve_integer_keys
Chris@0 316 * (optional) If given, integer keys will be preserved and merged instead of
Chris@0 317 * appended. Defaults to FALSE.
Chris@0 318 *
Chris@0 319 * @return array
Chris@0 320 * The merged array.
Chris@0 321 *
Chris@0 322 * @see NestedArray::mergeDeep()
Chris@0 323 */
Chris@0 324 public static function mergeDeepArray(array $arrays, $preserve_integer_keys = FALSE) {
Chris@0 325 $result = [];
Chris@0 326 foreach ($arrays as $array) {
Chris@0 327 foreach ($array as $key => $value) {
Chris@0 328 // Renumber integer keys as array_merge_recursive() does unless
Chris@0 329 // $preserve_integer_keys is set to TRUE. Note that PHP automatically
Chris@0 330 // converts array keys that are integer strings (e.g., '1') to integers.
Chris@0 331 if (is_int($key) && !$preserve_integer_keys) {
Chris@0 332 $result[] = $value;
Chris@0 333 }
Chris@0 334 // Recurse when both values are arrays.
Chris@0 335 elseif (isset($result[$key]) && is_array($result[$key]) && is_array($value)) {
Chris@0 336 $result[$key] = self::mergeDeepArray([$result[$key], $value], $preserve_integer_keys);
Chris@0 337 }
Chris@0 338 // Otherwise, use the latter value, overriding any previous value.
Chris@0 339 else {
Chris@0 340 $result[$key] = $value;
Chris@0 341 }
Chris@0 342 }
Chris@0 343 }
Chris@0 344 return $result;
Chris@0 345 }
Chris@0 346
Chris@0 347 /**
Chris@0 348 * Filters a nested array recursively.
Chris@0 349 *
Chris@0 350 * @param array $array
Chris@0 351 * The filtered nested array.
Chris@0 352 * @param callable|null $callable
Chris@0 353 * The callable to apply for filtering.
Chris@0 354 *
Chris@0 355 * @return array
Chris@0 356 * The filtered array.
Chris@0 357 */
Chris@0 358 public static function filter(array $array, callable $callable = NULL) {
Chris@0 359 $array = is_callable($callable) ? array_filter($array, $callable) : array_filter($array);
Chris@0 360 foreach ($array as &$element) {
Chris@0 361 if (is_array($element)) {
Chris@0 362 $element = static::filter($element, $callable);
Chris@0 363 }
Chris@0 364 }
Chris@0 365
Chris@0 366 return $array;
Chris@0 367 }
Chris@0 368
Chris@0 369 }