-
-
Notifications
You must be signed in to change notification settings - Fork 188
/
Arrays.php
343 lines (327 loc) · 14 KB
/
Arrays.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
<?php
namespace Neos\Utility;
/*
* This file is part of the Neos.Utility.Arrays package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/
/**
* Some array functions to help with common tasks
*
*/
abstract class Arrays
{
/**
* Explodes a $string delimited by $delimiter and passes each item in the array through intval().
* Corresponds to explode(), but with conversion to integers for all values.
*
* @param string $delimiter Delimiter string to explode with
* @param string $string The string to explode
* @return array Exploded values, all converted to integers
*/
public static function integerExplode(string $delimiter, string $string): array
{
$chunksArr = explode($delimiter, $string);
foreach ($chunksArr as $key => $value) {
$chunks[$key] = (int)$value;
}
reset($chunks);
return $chunks;
}
/**
* Explodes a string and trims all values for whitespace in the ends.
* If $onlyNonEmptyValues is set, then all blank ('') values are removed.
*
* @param string $delimiter Delimiter string to explode with
* @param string $string The string to explode
* @param boolean $onlyNonEmptyValues If disabled, even empty values (='') will be set in output
* @return array Exploded values
*/
public static function trimExplode(string $delimiter, string $string, bool $onlyNonEmptyValues = true): array
{
$chunksArr = explode($delimiter, $string);
$newChunksArr = [];
foreach ($chunksArr as $value) {
if ($onlyNonEmptyValues === false || strcmp('', trim($value))) {
$newChunksArr[] = trim($value);
}
}
reset($newChunksArr);
return $newChunksArr;
}
/**
* Merges two arrays recursively and "binary safe" (integer keys are overridden as well), overruling similar values
* in the first array ($firstArray) with the values of the second array ($secondArray) in case of identical keys,
* ie. keeping the values of the second.
*
* @param array $firstArray First array
* @param array $secondArray Second array, overruling the first array
* @param boolean $dontAddNewKeys If set, keys that are NOT found in $firstArray (first array) will not be set. Thus only existing value can/will be overruled from second array.
* @param boolean $emptyValuesOverride If set (which is the default), values from $secondArray will overrule if they are empty (according to PHP's empty() function)
* @return array Resulting array where $secondArray values has overruled $firstArray values
*/
public static function arrayMergeRecursiveOverrule(array $firstArray, array $secondArray, bool $dontAddNewKeys = false, bool $emptyValuesOverride = true): array
{
$data = [&$firstArray, $secondArray];
$entryCount = 1;
for ($i = 0; $i < $entryCount; $i++) {
$firstArrayInner = &$data[$i * 2];
$secondArrayInner = $data[$i * 2 + 1];
foreach ($secondArrayInner as $key => $value) {
if (isset($firstArrayInner[$key]) && is_array($firstArrayInner[$key])) {
if ((!$emptyValuesOverride || $value !== []) && is_array($value)) {
$data[] = &$firstArrayInner[$key];
$data[] = $value;
$entryCount++;
} else {
$firstArrayInner[$key] = $value;
}
} else {
if ($dontAddNewKeys) {
if (array_key_exists($key, $firstArrayInner) && ($emptyValuesOverride || !empty($value))) {
$firstArrayInner[$key] = $value;
}
} else {
if ($emptyValuesOverride || !empty($value)) {
$firstArrayInner[$key] = $value;
} elseif (!isset($firstArrayInner[$key]) && $value === []) {
$firstArrayInner[$key] = $value;
}
}
}
}
}
reset($firstArray);
return $firstArray;
}
/**
* Merges two arrays recursively and "binary safe" (integer keys are overridden as well), overruling similar values in the first array ($firstArray) with the values of the second array ($secondArray)
* In case of identical keys, ie. keeping the values of the second. The given $toArray closure will be used if one of the two array keys contains an array and the other not. It should return an array.
*
* @param array $firstArray First array
* @param array $secondArray Second array, overruling the first array
* @param \Closure $toArray The given callable will get a value that is not an array and has to return an array.
* This is to allow custom merging of simple types with (sub) arrays
* @param \Closure|null $overrideFirst The given callable will determine whether the value of the first array should be overridden.
* It should have the following signature $callable($key, ?array $firstValue = null, ?array $secondValue = null): bool
* @return array Resulting array where $secondArray values has overruled $firstArray values
*/
public static function arrayMergeRecursiveOverruleWithCallback(array $firstArray, array $secondArray, \Closure $toArray, ?\Closure $overrideFirst = null): array
{
$data = [&$firstArray, $secondArray];
$entryCount = 1;
for ($i = 0; $i < $entryCount; $i++) {
$firstArrayInner = &$data[$i * 2];
$secondArrayInner = $data[$i * 2 + 1];
foreach ($secondArrayInner as $key => $value) {
if (!isset($firstArrayInner[$key]) || (!is_array($firstArrayInner[$key]) && !is_array($value))) {
$firstArrayInner[$key] = $value;
} else {
if (!is_array($value)) {
$value = $toArray($value);
}
if (!is_array($firstArrayInner[$key])) {
$firstArrayInner[$key] = $toArray($firstArrayInner[$key]);
}
if (is_array($firstArrayInner[$key]) && is_array($value)) {
if ($overrideFirst !== null && $overrideFirst($key, $firstArrayInner[$key], $value)) {
$firstArrayInner[$key] = $value;
}
$data[] = &$firstArrayInner[$key];
$data[] = $value;
$entryCount++;
} else {
$firstArrayInner[$key] = $value;
}
}
}
}
reset($firstArray);
return $firstArray;
}
/**
* Returns true if the given array contains elements of varying types
*
* @param array $array
* @return boolean
*/
public static function containsMultipleTypes(array $array): bool
{
if (count($array) > 0) {
reset($array);
$previousType = gettype(current($array));
next($array);
foreach ($array as $value) {
if ($previousType !== gettype($value)) {
return true;
}
}
}
return false;
}
/**
* Replacement for array_reduce that allows any type for $initial (instead
* of only integer)
*
* @param array $array the array to reduce
* @param string $function the reduce function with the same order of parameters as in the native array_reduce (i.e. accumulator first, then current array element)
* @param mixed $initial the initial accumulator value
* @return mixed
*/
public static function array_reduce(array $array, string $function, $initial = null)
{
$accumulator = $initial;
foreach ($array as $value) {
$accumulator = $function($accumulator, $value);
}
return $accumulator;
}
/**
* Returns the value of a nested array by following the specifed path.
*
* @param array &$array The array to traverse as a reference
* @param array|string $path The path to follow. Either a simple array of keys or a string in the format 'foo.bar.baz'
* @return mixed The value found, NULL if the path didn't exist (note there is no way to distinguish between a found NULL value and "path not found")
* @throws \InvalidArgumentException
*/
public static function getValueByPath(array &$array, $path)
{
if (is_string($path)) {
$path = explode('.', $path);
} elseif (!is_array($path)) {
throw new \InvalidArgumentException('getValueByPath() expects $path to be string or array, "' . gettype($path) . '" given.', 1304950007);
}
$key = array_shift($path);
if (isset($array[$key])) {
if (count($path) > 0) {
return (is_array($array[$key])) ? self::getValueByPath($array[$key], $path) : null;
} else {
return $array[$key];
}
} else {
return null;
}
}
/**
* Sets the given value in a nested array or object by following the specified path.
*
* @param array|\ArrayAccess $subject The array or ArrayAccess instance to work on
* @param array|string $path The path to follow. Either a simple array of keys or a string in the format 'foo.bar.baz'
* @param mixed $value The value to set
* @return array|\ArrayAccess The modified array or object
* @throws \InvalidArgumentException
*/
public static function setValueByPath($subject, $path, $value)
{
if (!is_array($subject) && !($subject instanceof \ArrayAccess)) {
throw new \InvalidArgumentException('setValueByPath() expects $subject to be array or an object implementing \ArrayAccess, "' . (is_object($subject) ? get_class($subject) : gettype($subject)) . '" given.', 1306424308);
}
if (is_string($path)) {
$path = explode('.', $path);
} elseif (!is_array($path)) {
throw new \InvalidArgumentException('setValueByPath() expects $path to be string or array, "' . gettype($path) . '" given.', 1305111499);
}
$key = array_shift($path);
if (count($path) === 0) {
$subject[$key] = $value;
} else {
if (!isset($subject[$key]) || !is_array($subject[$key])) {
$subject[$key] = [];
}
$subject[$key] = self::setValueByPath($subject[$key], $path, $value);
}
return $subject;
}
/**
* Unsets an element/part of a nested array by following the specified path.
*
* @param array $array The array
* @param array|string $path The path to follow. Either a simple array of keys or a string in the format 'foo.bar.baz'
* @return array The modified array
* @throws \InvalidArgumentException
*/
public static function unsetValueByPath(array $array, $path): array
{
if (is_string($path)) {
$path = explode('.', $path);
} elseif (!is_array($path)) {
throw new \InvalidArgumentException('unsetValueByPath() expects $path to be string or array, "' . gettype($path) . '" given.', 1305111513);
}
$key = array_shift($path);
if (count($path) === 0) {
unset($array[$key]);
} else {
if (!isset($array[$key]) || !is_array($array[$key])) {
return $array;
}
$array[$key] = self::unsetValueByPath($array[$key], $path);
}
return $array;
}
/**
* Sorts multidimensional arrays by recursively calling ksort on its elements.
*
* @param array $array the array to sort
* @param integer $sortFlags may be used to modify the sorting behavior using these values (see http://www.php.net/manual/en/function.sort.php)
* @return boolean true on success, false on failure
* @see asort()
*/
public static function sortKeysRecursively(array &$array, int $sortFlags = null): bool
{
foreach ($array as &$value) {
if (is_array($value)) {
if (self::sortKeysRecursively($value, $sortFlags) === false) {
return false;
}
}
}
return ksort($array, $sortFlags);
}
/**
* Recursively convert an object hierarchy into an associative array.
*
* @param mixed $subject An object or array of objects
* @return array The subject represented as an array
* @throws \InvalidArgumentException
*/
public static function convertObjectToArray($subject): array
{
if (!is_object($subject) && !is_array($subject)) {
throw new \InvalidArgumentException('convertObjectToArray expects either array or object as input, ' . gettype($subject) . ' given.', 1287059709);
}
if (is_object($subject)) {
$subject = (array)$subject;
}
foreach ($subject as $key => $value) {
if (is_array($value) || is_object($value)) {
$subject[$key] = self::convertObjectToArray($value);
}
}
return $subject;
}
/**
* Recursively removes empty array elements.
*
* @param array $array
* @return array the modified array
*/
public static function removeEmptyElementsRecursively(array $array): array
{
$result = $array;
foreach ($result as $key => $value) {
if (is_array($value)) {
$result[$key] = self::removeEmptyElementsRecursively($value);
if ($result[$key] === []) {
unset($result[$key]);
}
} elseif ($value === null) {
unset($result[$key]);
}
}
return $result;
}
}