Arrays.php 10 KB

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