1<?php
2
3/**
4 * This file is part of the Nette Framework (https://nette.org)
5 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6 */
7
8declare(strict_types=1);
9
10namespace Nette\Utils;
11
12use JetBrains\PhpStorm\Language;
13use Nette;
14use function is_array, is_int, is_object, count;
15
16
17/**
18 * Array tools library.
19 */
20class Arrays
21{
22	use Nette\StaticClass;
23
24	/**
25	 * Returns item from array. If it does not exist, it throws an exception, unless a default value is set.
26	 * @template T
27	 * @param  array<T>  $array
28	 * @param  array-key|array-key[]  $key
29	 * @param  ?T  $default
30	 * @return ?T
31	 * @throws Nette\InvalidArgumentException if item does not exist and default value is not provided
32	 */
33	public static function get(array $array, string|int|array $key, mixed $default = null): mixed
34	{
35		foreach (is_array($key) ? $key : [$key] as $k) {
36			if (is_array($array) && array_key_exists($k, $array)) {
37				$array = $array[$k];
38			} else {
39				if (func_num_args() < 3) {
40					throw new Nette\InvalidArgumentException("Missing item '$k'.");
41				}
42
43				return $default;
44			}
45		}
46
47		return $array;
48	}
49
50
51	/**
52	 * Returns reference to array item. If the index does not exist, new one is created with value null.
53	 * @template T
54	 * @param  array<T>  $array
55	 * @param  array-key|array-key[]  $key
56	 * @return ?T
57	 * @throws Nette\InvalidArgumentException if traversed item is not an array
58	 */
59	public static function &getRef(array &$array, string|int|array $key): mixed
60	{
61		foreach (is_array($key) ? $key : [$key] as $k) {
62			if (is_array($array) || $array === null) {
63				$array = &$array[$k];
64			} else {
65				throw new Nette\InvalidArgumentException('Traversed item is not an array.');
66			}
67		}
68
69		return $array;
70	}
71
72
73	/**
74	 * Recursively merges two fields. It is useful, for example, for merging tree structures. It behaves as
75	 * the + operator for array, ie. it adds a key/value pair from the second array to the first one and retains
76	 * the value from the first array in the case of a key collision.
77	 * @template T1
78	 * @template T2
79	 * @param  array<T1>  $array1
80	 * @param  array<T2>  $array2
81	 * @return array<T1|T2>
82	 */
83	public static function mergeTree(array $array1, array $array2): array
84	{
85		$res = $array1 + $array2;
86		foreach (array_intersect_key($array1, $array2) as $k => $v) {
87			if (is_array($v) && is_array($array2[$k])) {
88				$res[$k] = self::mergeTree($v, $array2[$k]);
89			}
90		}
91
92		return $res;
93	}
94
95
96	/**
97	 * Returns zero-indexed position of given array key. Returns null if key is not found.
98	 */
99	public static function getKeyOffset(array $array, string|int $key): ?int
100	{
101		return Helpers::falseToNull(array_search(self::toKey($key), array_keys($array), true));
102	}
103
104
105	/**
106	 * @deprecated  use  getKeyOffset()
107	 */
108	public static function searchKey(array $array, $key): ?int
109	{
110		return self::getKeyOffset($array, $key);
111	}
112
113
114	/**
115	 * Tests an array for the presence of value.
116	 */
117	public static function contains(array $array, mixed $value): bool
118	{
119		return in_array($value, $array, true);
120	}
121
122
123	/**
124	 * Returns the first item from the array or null if array is empty.
125	 * @template T
126	 * @param  array<T>  $array
127	 * @return ?T
128	 */
129	public static function first(array $array): mixed
130	{
131		return count($array) ? reset($array) : null;
132	}
133
134
135	/**
136	 * Returns the last item from the array or null if array is empty.
137	 * @template T
138	 * @param  array<T>  $array
139	 * @return ?T
140	 */
141	public static function last(array $array): mixed
142	{
143		return count($array) ? end($array) : null;
144	}
145
146
147	/**
148	 * Inserts the contents of the $inserted array into the $array immediately after the $key.
149	 * If $key is null (or does not exist), it is inserted at the beginning.
150	 */
151	public static function insertBefore(array &$array, string|int|null $key, array $inserted): void
152	{
153		$offset = $key === null ? 0 : (int) self::getKeyOffset($array, $key);
154		$array = array_slice($array, 0, $offset, true)
155			+ $inserted
156			+ array_slice($array, $offset, count($array), true);
157	}
158
159
160	/**
161	 * Inserts the contents of the $inserted array into the $array before the $key.
162	 * If $key is null (or does not exist), it is inserted at the end.
163	 */
164	public static function insertAfter(array &$array, string|int|null $key, array $inserted): void
165	{
166		if ($key === null || ($offset = self::getKeyOffset($array, $key)) === null) {
167			$offset = count($array) - 1;
168		}
169
170		$array = array_slice($array, 0, $offset + 1, true)
171			+ $inserted
172			+ array_slice($array, $offset + 1, count($array), true);
173	}
174
175
176	/**
177	 * Renames key in array.
178	 */
179	public static function renameKey(array &$array, string|int $oldKey, string|int $newKey): bool
180	{
181		$offset = self::getKeyOffset($array, $oldKey);
182		if ($offset === null) {
183			return false;
184		}
185
186		$val = &$array[$oldKey];
187		$keys = array_keys($array);
188		$keys[$offset] = $newKey;
189		$array = array_combine($keys, $array);
190		$array[$newKey] = &$val;
191		return true;
192	}
193
194
195	/**
196	 * Returns only those array items, which matches a regular expression $pattern.
197	 * @param  string[]  $array
198	 * @return string[]
199	 */
200	public static function grep(
201		array $array,
202		#[Language('RegExp')]
203		string $pattern,
204		bool|int $invert = false,
205	): array
206	{
207		$flags = $invert ? PREG_GREP_INVERT : 0;
208		return Strings::pcre('preg_grep', [$pattern, $array, $flags]);
209	}
210
211
212	/**
213	 * Transforms multidimensional array to flat array.
214	 */
215	public static function flatten(array $array, bool $preserveKeys = false): array
216	{
217		$res = [];
218		$cb = $preserveKeys
219			? function ($v, $k) use (&$res): void { $res[$k] = $v; }
220		: function ($v) use (&$res): void { $res[] = $v; };
221		array_walk_recursive($array, $cb);
222		return $res;
223	}
224
225
226	/**
227	 * Checks if the array is indexed in ascending order of numeric keys from zero, a.k.a list.
228	 * @return ($value is list ? true : false)
229	 */
230	public static function isList(mixed $value): bool
231	{
232		return is_array($value) && (PHP_VERSION_ID < 80100
233			? !$value || array_keys($value) === range(0, count($value) - 1)
234			: array_is_list($value)
235		);
236	}
237
238
239	/**
240	 * Reformats table to associative tree. Path looks like 'field|field[]field->field=field'.
241	 * @param  string|string[]  $path
242	 */
243	public static function associate(array $array, $path): array|\stdClass
244	{
245		$parts = is_array($path)
246			? $path
247			: preg_split('#(\[\]|->|=|\|)#', $path, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
248
249		if (!$parts || $parts === ['->'] || $parts[0] === '=' || $parts[0] === '|') {
250			throw new Nette\InvalidArgumentException("Invalid path '$path'.");
251		}
252
253		$res = $parts[0] === '->' ? new \stdClass : [];
254
255		foreach ($array as $rowOrig) {
256			$row = (array) $rowOrig;
257			$x = &$res;
258
259			for ($i = 0; $i < count($parts); $i++) {
260				$part = $parts[$i];
261				if ($part === '[]') {
262					$x = &$x[];
263
264				} elseif ($part === '=') {
265					if (isset($parts[++$i])) {
266						$x = $row[$parts[$i]];
267						$row = null;
268					}
269				} elseif ($part === '->') {
270					if (isset($parts[++$i])) {
271						if ($x === null) {
272							$x = new \stdClass;
273						}
274
275						$x = &$x->{$row[$parts[$i]]};
276					} else {
277						$row = is_object($rowOrig) ? $rowOrig : (object) $row;
278					}
279				} elseif ($part !== '|') {
280					$x = &$x[(string) $row[$part]];
281				}
282			}
283
284			if ($x === null) {
285				$x = $row;
286			}
287		}
288
289		return $res;
290	}
291
292
293	/**
294	 * Normalizes array to associative array. Replace numeric keys with their values, the new value will be $filling.
295	 */
296	public static function normalize(array $array, mixed $filling = null): array
297	{
298		$res = [];
299		foreach ($array as $k => $v) {
300			$res[is_int($k) ? $v : $k] = is_int($k) ? $filling : $v;
301		}
302
303		return $res;
304	}
305
306
307	/**
308	 * Returns and removes the value of an item from an array. If it does not exist, it throws an exception,
309	 * or returns $default, if provided.
310	 * @template T
311	 * @param  array<T>  $array
312	 * @param  ?T  $default
313	 * @return ?T
314	 * @throws Nette\InvalidArgumentException if item does not exist and default value is not provided
315	 */
316	public static function pick(array &$array, string|int $key, mixed $default = null): mixed
317	{
318		if (array_key_exists($key, $array)) {
319			$value = $array[$key];
320			unset($array[$key]);
321			return $value;
322
323		} elseif (func_num_args() < 3) {
324			throw new Nette\InvalidArgumentException("Missing item '$key'.");
325
326		} else {
327			return $default;
328		}
329	}
330
331
332	/**
333	 * Tests whether at least one element in the array passes the test implemented by the
334	 * provided callback with signature `function ($value, $key, array $array): bool`.
335	 */
336	public static function some(iterable $array, callable $callback): bool
337	{
338		foreach ($array as $k => $v) {
339			if ($callback($v, $k, $array)) {
340				return true;
341			}
342		}
343
344		return false;
345	}
346
347
348	/**
349	 * Tests whether all elements in the array pass the test implemented by the provided function,
350	 * which has the signature `function ($value, $key, array $array): bool`.
351	 */
352	public static function every(iterable $array, callable $callback): bool
353	{
354		foreach ($array as $k => $v) {
355			if (!$callback($v, $k, $array)) {
356				return false;
357			}
358		}
359
360		return true;
361	}
362
363
364	/**
365	 * Calls $callback on all elements in the array and returns the array of return values.
366	 * The callback has the signature `function ($value, $key, array $array): bool`.
367	 */
368	public static function map(iterable $array, callable $callback): array
369	{
370		$res = [];
371		foreach ($array as $k => $v) {
372			$res[$k] = $callback($v, $k, $array);
373		}
374
375		return $res;
376	}
377
378
379	/**
380	 * Invokes all callbacks and returns array of results.
381	 * @param  callable[]  $callbacks
382	 */
383	public static function invoke(iterable $callbacks, ...$args): array
384	{
385		$res = [];
386		foreach ($callbacks as $k => $cb) {
387			$res[$k] = $cb(...$args);
388		}
389
390		return $res;
391	}
392
393
394	/**
395	 * Invokes method on every object in an array and returns array of results.
396	 * @param  object[]  $objects
397	 */
398	public static function invokeMethod(iterable $objects, string $method, ...$args): array
399	{
400		$res = [];
401		foreach ($objects as $k => $obj) {
402			$res[$k] = $obj->$method(...$args);
403		}
404
405		return $res;
406	}
407
408
409	/**
410	 * Copies the elements of the $array array to the $object object and then returns it.
411	 * @template T of object
412	 * @param  T  $object
413	 * @return T
414	 */
415	public static function toObject(iterable $array, object $object): object
416	{
417		foreach ($array as $k => $v) {
418			$object->$k = $v;
419		}
420
421		return $object;
422	}
423
424
425	/**
426	 * Converts value to array key.
427	 */
428	public static function toKey(mixed $value): int|string
429	{
430		return key([$value => null]);
431	}
432
433
434	/**
435	 * Returns copy of the $array where every item is converted to string
436	 * and prefixed by $prefix and suffixed by $suffix.
437	 * @param  string[]  $array
438	 * @return string[]
439	 */
440	public static function wrap(array $array, string $prefix = '', string $suffix = ''): array
441	{
442		$res = [];
443		foreach ($array as $k => $v) {
444			$res[$k] = $prefix . $v . $suffix;
445		}
446
447		return $res;
448	}
449}
450