NestedArray.php 13 KB

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