comparison core/lib/Drupal/Component/Utility/NestedArray.php @ 0:4c8ae668cc8c

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