friendship ended with social-app. php is my new best friend
at main 14 kB view raw
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 array_combine, array_intersect_key, array_is_list, array_key_exists, array_key_first, array_key_last, array_keys, array_reverse, array_search, array_slice, array_walk_recursive, count, func_num_args, in_array, is_array, is_int, is_object, key, preg_split, range; 15use const PHP_VERSION_ID, PREG_GREP_INVERT, PREG_SPLIT_DELIM_CAPTURE, PREG_SPLIT_NO_EMPTY; 16 17 18/** 19 * Array tools library. 20 */ 21class Arrays 22{ 23 use Nette\StaticClass; 24 25 /** 26 * Returns item from array. If it does not exist, it throws an exception, unless a default value is set. 27 * @template T 28 * @param array<T> $array 29 * @param array-key|array-key[] $key 30 * @param ?T $default 31 * @return ?T 32 * @throws Nette\InvalidArgumentException if item does not exist and default value is not provided 33 */ 34 public static function get(array $array, string|int|array $key, mixed $default = null): mixed 35 { 36 foreach (is_array($key) ? $key : [$key] as $k) { 37 if (is_array($array) && array_key_exists($k, $array)) { 38 $array = $array[$k]; 39 } else { 40 if (func_num_args() < 3) { 41 throw new Nette\InvalidArgumentException("Missing item '$k'."); 42 } 43 44 return $default; 45 } 46 } 47 48 return $array; 49 } 50 51 52 /** 53 * Returns reference to array item. If the index does not exist, new one is created with value null. 54 * @template T 55 * @param array<T> $array 56 * @param array-key|array-key[] $key 57 * @return ?T 58 * @throws Nette\InvalidArgumentException if traversed item is not an array 59 */ 60 public static function &getRef(array &$array, string|int|array $key): mixed 61 { 62 foreach (is_array($key) ? $key : [$key] as $k) { 63 if (is_array($array) || $array === null) { 64 $array = &$array[$k]; 65 } else { 66 throw new Nette\InvalidArgumentException('Traversed item is not an array.'); 67 } 68 } 69 70 return $array; 71 } 72 73 74 /** 75 * Recursively merges two fields. It is useful, for example, for merging tree structures. It behaves as 76 * the + operator for array, ie. it adds a key/value pair from the second array to the first one and retains 77 * the value from the first array in the case of a key collision. 78 * @template T1 79 * @template T2 80 * @param array<T1> $array1 81 * @param array<T2> $array2 82 * @return array<T1|T2> 83 */ 84 public static function mergeTree(array $array1, array $array2): array 85 { 86 $res = $array1 + $array2; 87 foreach (array_intersect_key($array1, $array2) as $k => $v) { 88 if (is_array($v) && is_array($array2[$k])) { 89 $res[$k] = self::mergeTree($v, $array2[$k]); 90 } 91 } 92 93 return $res; 94 } 95 96 97 /** 98 * Returns zero-indexed position of given array key. Returns null if key is not found. 99 */ 100 public static function getKeyOffset(array $array, string|int $key): ?int 101 { 102 return Helpers::falseToNull(array_search(self::toKey($key), array_keys($array), strict: true)); 103 } 104 105 106 /** 107 * @deprecated use getKeyOffset() 108 */ 109 public static function searchKey(array $array, $key): ?int 110 { 111 return self::getKeyOffset($array, $key); 112 } 113 114 115 /** 116 * Tests an array for the presence of value. 117 */ 118 public static function contains(array $array, mixed $value): bool 119 { 120 return in_array($value, $array, true); 121 } 122 123 124 /** 125 * Returns the first item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null. 126 * @template K of int|string 127 * @template V 128 * @param array<K, V> $array 129 * @param ?callable(V, K, array<K, V>): bool $predicate 130 * @return ?V 131 */ 132 public static function first(array $array, ?callable $predicate = null, ?callable $else = null): mixed 133 { 134 $key = self::firstKey($array, $predicate); 135 return $key === null 136 ? ($else ? $else() : null) 137 : $array[$key]; 138 } 139 140 141 /** 142 * Returns the last item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null. 143 * @template K of int|string 144 * @template V 145 * @param array<K, V> $array 146 * @param ?callable(V, K, array<K, V>): bool $predicate 147 * @return ?V 148 */ 149 public static function last(array $array, ?callable $predicate = null, ?callable $else = null): mixed 150 { 151 $key = self::lastKey($array, $predicate); 152 return $key === null 153 ? ($else ? $else() : null) 154 : $array[$key]; 155 } 156 157 158 /** 159 * Returns the key of first item (matching the specified predicate if given) or null if there is no such item. 160 * @template K of int|string 161 * @template V 162 * @param array<K, V> $array 163 * @param ?callable(V, K, array<K, V>): bool $predicate 164 * @return ?K 165 */ 166 public static function firstKey(array $array, ?callable $predicate = null): int|string|null 167 { 168 if (!$predicate) { 169 return array_key_first($array); 170 } 171 foreach ($array as $k => $v) { 172 if ($predicate($v, $k, $array)) { 173 return $k; 174 } 175 } 176 return null; 177 } 178 179 180 /** 181 * Returns the key of last item (matching the specified predicate if given) or null if there is no such item. 182 * @template K of int|string 183 * @template V 184 * @param array<K, V> $array 185 * @param ?callable(V, K, array<K, V>): bool $predicate 186 * @return ?K 187 */ 188 public static function lastKey(array $array, ?callable $predicate = null): int|string|null 189 { 190 return $predicate 191 ? self::firstKey(array_reverse($array, preserve_keys: true), $predicate) 192 : array_key_last($array); 193 } 194 195 196 /** 197 * Inserts the contents of the $inserted array into the $array immediately after the $key. 198 * If $key is null (or does not exist), it is inserted at the beginning. 199 */ 200 public static function insertBefore(array &$array, string|int|null $key, array $inserted): void 201 { 202 $offset = $key === null ? 0 : (int) self::getKeyOffset($array, $key); 203 $array = array_slice($array, 0, $offset, preserve_keys: true) 204 + $inserted 205 + array_slice($array, $offset, count($array), preserve_keys: true); 206 } 207 208 209 /** 210 * Inserts the contents of the $inserted array into the $array before the $key. 211 * If $key is null (or does not exist), it is inserted at the end. 212 */ 213 public static function insertAfter(array &$array, string|int|null $key, array $inserted): void 214 { 215 if ($key === null || ($offset = self::getKeyOffset($array, $key)) === null) { 216 $offset = count($array) - 1; 217 } 218 219 $array = array_slice($array, 0, $offset + 1, preserve_keys: true) 220 + $inserted 221 + array_slice($array, $offset + 1, count($array), preserve_keys: true); 222 } 223 224 225 /** 226 * Renames key in array. 227 */ 228 public static function renameKey(array &$array, string|int $oldKey, string|int $newKey): bool 229 { 230 $offset = self::getKeyOffset($array, $oldKey); 231 if ($offset === null) { 232 return false; 233 } 234 235 $val = &$array[$oldKey]; 236 $keys = array_keys($array); 237 $keys[$offset] = $newKey; 238 $array = array_combine($keys, $array); 239 $array[$newKey] = &$val; 240 return true; 241 } 242 243 244 /** 245 * Returns only those array items, which matches a regular expression $pattern. 246 * @param string[] $array 247 * @return string[] 248 */ 249 public static function grep( 250 array $array, 251 #[Language('RegExp')] 252 string $pattern, 253 bool|int $invert = false, 254 ): array 255 { 256 $flags = $invert ? PREG_GREP_INVERT : 0; 257 return Strings::pcre('preg_grep', [$pattern, $array, $flags]); 258 } 259 260 261 /** 262 * Transforms multidimensional array to flat array. 263 */ 264 public static function flatten(array $array, bool $preserveKeys = false): array 265 { 266 $res = []; 267 $cb = $preserveKeys 268 ? function ($v, $k) use (&$res): void { $res[$k] = $v; } 269 : function ($v) use (&$res): void { $res[] = $v; }; 270 array_walk_recursive($array, $cb); 271 return $res; 272 } 273 274 275 /** 276 * Checks if the array is indexed in ascending order of numeric keys from zero, a.k.a list. 277 * @return ($value is list ? true : false) 278 */ 279 public static function isList(mixed $value): bool 280 { 281 return is_array($value) && ( 282 PHP_VERSION_ID < 80100 283 ? !$value || array_keys($value) === range(0, count($value) - 1) 284 : array_is_list($value) 285 ); 286 } 287 288 289 /** 290 * Reformats table to associative tree. Path looks like 'field|field[]field->field=field'. 291 * @param string|string[] $path 292 */ 293 public static function associate(array $array, $path): array|\stdClass 294 { 295 $parts = is_array($path) 296 ? $path 297 : preg_split('#(\[\]|->|=|\|)#', $path, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); 298 299 if (!$parts || $parts === ['->'] || $parts[0] === '=' || $parts[0] === '|') { 300 throw new Nette\InvalidArgumentException("Invalid path '$path'."); 301 } 302 303 $res = $parts[0] === '->' ? new \stdClass : []; 304 305 foreach ($array as $rowOrig) { 306 $row = (array) $rowOrig; 307 $x = &$res; 308 309 for ($i = 0; $i < count($parts); $i++) { 310 $part = $parts[$i]; 311 if ($part === '[]') { 312 $x = &$x[]; 313 314 } elseif ($part === '=') { 315 if (isset($parts[++$i])) { 316 $x = $row[$parts[$i]]; 317 $row = null; 318 } 319 } elseif ($part === '->') { 320 if (isset($parts[++$i])) { 321 if ($x === null) { 322 $x = new \stdClass; 323 } 324 325 $x = &$x->{$row[$parts[$i]]}; 326 } else { 327 $row = is_object($rowOrig) ? $rowOrig : (object) $row; 328 } 329 } elseif ($part !== '|') { 330 $x = &$x[(string) $row[$part]]; 331 } 332 } 333 334 if ($x === null) { 335 $x = $row; 336 } 337 } 338 339 return $res; 340 } 341 342 343 /** 344 * Normalizes array to associative array. Replace numeric keys with their values, the new value will be $filling. 345 */ 346 public static function normalize(array $array, mixed $filling = null): array 347 { 348 $res = []; 349 foreach ($array as $k => $v) { 350 $res[is_int($k) ? $v : $k] = is_int($k) ? $filling : $v; 351 } 352 353 return $res; 354 } 355 356 357 /** 358 * Returns and removes the value of an item from an array. If it does not exist, it throws an exception, 359 * or returns $default, if provided. 360 * @template T 361 * @param array<T> $array 362 * @param ?T $default 363 * @return ?T 364 * @throws Nette\InvalidArgumentException if item does not exist and default value is not provided 365 */ 366 public static function pick(array &$array, string|int $key, mixed $default = null): mixed 367 { 368 if (array_key_exists($key, $array)) { 369 $value = $array[$key]; 370 unset($array[$key]); 371 return $value; 372 373 } elseif (func_num_args() < 3) { 374 throw new Nette\InvalidArgumentException("Missing item '$key'."); 375 376 } else { 377 return $default; 378 } 379 } 380 381 382 /** 383 * Tests whether at least one element in the array passes the test implemented by the provided function. 384 * @template K of int|string 385 * @template V 386 * @param array<K, V> $array 387 * @param callable(V, K, array<K, V>): bool $predicate 388 */ 389 public static function some(iterable $array, callable $predicate): bool 390 { 391 foreach ($array as $k => $v) { 392 if ($predicate($v, $k, $array)) { 393 return true; 394 } 395 } 396 397 return false; 398 } 399 400 401 /** 402 * Tests whether all elements in the array pass the test implemented by the provided function. 403 * @template K of int|string 404 * @template V 405 * @param array<K, V> $array 406 * @param callable(V, K, array<K, V>): bool $predicate 407 */ 408 public static function every(iterable $array, callable $predicate): bool 409 { 410 foreach ($array as $k => $v) { 411 if (!$predicate($v, $k, $array)) { 412 return false; 413 } 414 } 415 416 return true; 417 } 418 419 420 /** 421 * Returns a new array containing all key-value pairs matching the given $predicate. 422 * @template K of int|string 423 * @template V 424 * @param array<K, V> $array 425 * @param callable(V, K, array<K, V>): bool $predicate 426 * @return array<K, V> 427 */ 428 public static function filter(array $array, callable $predicate): array 429 { 430 $res = []; 431 foreach ($array as $k => $v) { 432 if ($predicate($v, $k, $array)) { 433 $res[$k] = $v; 434 } 435 } 436 return $res; 437 } 438 439 440 /** 441 * Returns an array containing the original keys and results of applying the given transform function to each element. 442 * @template K of int|string 443 * @template V 444 * @template R 445 * @param array<K, V> $array 446 * @param callable(V, K, array<K, V>): R $transformer 447 * @return array<K, R> 448 */ 449 public static function map(iterable $array, callable $transformer): array 450 { 451 $res = []; 452 foreach ($array as $k => $v) { 453 $res[$k] = $transformer($v, $k, $array); 454 } 455 456 return $res; 457 } 458 459 460 /** 461 * Returns an array containing new keys and values generated by applying the given transform function to each element. 462 * If the function returns null, the element is skipped. 463 * @template K of int|string 464 * @template V 465 * @template ResK of int|string 466 * @template ResV 467 * @param array<K, V> $array 468 * @param callable(V, K, array<K, V>): ?array{ResK, ResV} $transformer 469 * @return array<ResK, ResV> 470 */ 471 public static function mapWithKeys(array $array, callable $transformer): array 472 { 473 $res = []; 474 foreach ($array as $k => $v) { 475 $pair = $transformer($v, $k, $array); 476 if ($pair) { 477 $res[$pair[0]] = $pair[1]; 478 } 479 } 480 481 return $res; 482 } 483 484 485 /** 486 * Invokes all callbacks and returns array of results. 487 * @param callable[] $callbacks 488 */ 489 public static function invoke(iterable $callbacks, ...$args): array 490 { 491 $res = []; 492 foreach ($callbacks as $k => $cb) { 493 $res[$k] = $cb(...$args); 494 } 495 496 return $res; 497 } 498 499 500 /** 501 * Invokes method on every object in an array and returns array of results. 502 * @param object[] $objects 503 */ 504 public static function invokeMethod(iterable $objects, string $method, ...$args): array 505 { 506 $res = []; 507 foreach ($objects as $k => $obj) { 508 $res[$k] = $obj->$method(...$args); 509 } 510 511 return $res; 512 } 513 514 515 /** 516 * Copies the elements of the $array array to the $object object and then returns it. 517 * @template T of object 518 * @param T $object 519 * @return T 520 */ 521 public static function toObject(iterable $array, object $object): object 522 { 523 foreach ($array as $k => $v) { 524 $object->$k = $v; 525 } 526 527 return $object; 528 } 529 530 531 /** 532 * Converts value to array key. 533 */ 534 public static function toKey(mixed $value): int|string 535 { 536 return key([$value => null]); 537 } 538 539 540 /** 541 * Returns copy of the $array where every item is converted to string 542 * and prefixed by $prefix and suffixed by $suffix. 543 * @param string[] $array 544 * @return string[] 545 */ 546 public static function wrap(array $array, string $prefix = '', string $suffix = ''): array 547 { 548 $res = []; 549 foreach ($array as $k => $v) { 550 $res[$k] = $prefix . $v . $suffix; 551 } 552 553 return $res; 554 } 555}