1 <?php
2 /**
3 * Part of the Joomla Framework Utilities Package
4 *
5 * @copyright Copyright (C) 2005 - 2016 Open Source Matters, Inc. All rights reserved.
6 * @license GNU General Public License version 2 or later; see LICENSE
7 */
8
9 namespace Joomla\Utilities;
10
11 use Joomla\String\StringHelper;
12
13 /**
14 * ArrayHelper is an array utility class for doing all sorts of odds and ends with arrays.
15 *
16 * @since 1.0
17 */
18 final class ArrayHelper
19 {
20 /**
21 * Private constructor to prevent instantiation of this class
22 *
23 * @since 1.0
24 */
25 private function __construct()
26 {
27 }
28
29 /**
30 * Function to convert array to integer values
31 *
32 * @param array $array The source array to convert
33 * @param mixed $default A default value (int|array) to assign if $array is not an array
34 *
35 * @return array
36 *
37 * @since 1.0
38 */
39 public static function toInteger($array, $default = null)
40 {
41 if (is_array($array))
42 {
43 return array_map('intval', $array);
44 }
45
46 if ($default === null)
47 {
48 return array();
49 }
50
51 if (is_array($default))
52 {
53 return static::toInteger($default, null);
54 }
55
56 return array((int) $default);
57 }
58
59 /**
60 * Utility function to map an array to a stdClass object.
61 *
62 * @param array $array The array to map.
63 * @param string $class Name of the class to create
64 * @param boolean $recursive Convert also any array inside the main array
65 *
66 * @return object
67 *
68 * @since 1.0
69 */
70 public static function toObject(array $array, $class = 'stdClass', $recursive = true)
71 {
72 $obj = new $class;
73
74 foreach ($array as $k => $v)
75 {
76 if ($recursive && is_array($v))
77 {
78 $obj->$k = static::toObject($v, $class);
79 }
80 else
81 {
82 $obj->$k = $v;
83 }
84 }
85
86 return $obj;
87 }
88
89 /**
90 * Utility function to map an array to a string.
91 *
92 * @param array $array The array to map.
93 * @param string $inner_glue The glue (optional, defaults to '=') between the key and the value.
94 * @param string $outer_glue The glue (optional, defaults to ' ') between array elements.
95 * @param boolean $keepOuterKey True if final key should be kept.
96 *
97 * @return string
98 *
99 * @since 1.0
100 */
101 public static function toString(array $array, $inner_glue = '=', $outer_glue = ' ', $keepOuterKey = false)
102 {
103 $output = array();
104
105 foreach ($array as $key => $item)
106 {
107 if (is_array($item))
108 {
109 if ($keepOuterKey)
110 {
111 $output[] = $key;
112 }
113
114 // This is value is an array, go and do it again!
115 $output[] = static::toString($item, $inner_glue, $outer_glue, $keepOuterKey);
116 }
117 else
118 {
119 $output[] = $key . $inner_glue . '"' . $item . '"';
120 }
121 }
122
123 return implode($outer_glue, $output);
124 }
125
126 /**
127 * Utility function to map an object to an array
128 *
129 * @param object $p_obj The source object
130 * @param boolean $recurse True to recurse through multi-level objects
131 * @param string $regex An optional regular expression to match on field names
132 *
133 * @return array
134 *
135 * @since 1.0
136 */
137 public static function fromObject($p_obj, $recurse = true, $regex = null)
138 {
139 if (is_object($p_obj) || is_array($p_obj))
140 {
141 return self::arrayFromObject($p_obj, $recurse, $regex);
142 }
143
144 return array();
145 }
146
147 /**
148 * Utility function to map an object or array to an array
149 *
150 * @param mixed $item The source object or array
151 * @param boolean $recurse True to recurse through multi-level objects
152 * @param string $regex An optional regular expression to match on field names
153 *
154 * @return array
155 *
156 * @since 1.0
157 */
158 private static function arrayFromObject($item, $recurse, $regex)
159 {
160 if (is_object($item))
161 {
162 $result = array();
163
164 foreach (get_object_vars($item) as $k => $v)
165 {
166 if (!$regex || preg_match($regex, $k))
167 {
168 if ($recurse)
169 {
170 $result[$k] = self::arrayFromObject($v, $recurse, $regex);
171 }
172 else
173 {
174 $result[$k] = $v;
175 }
176 }
177 }
178
179 return $result;
180 }
181
182 if (is_array($item))
183 {
184 $result = array();
185
186 foreach ($item as $k => $v)
187 {
188 $result[$k] = self::arrayFromObject($v, $recurse, $regex);
189 }
190
191 return $result;
192 }
193
194 return $item;
195 }
196
197 /**
198 * Extracts a column from an array of arrays or objects
199 *
200 * @param array $array The source array
201 * @param string $valueCol The index of the column or name of object property to be used as value
202 * It may also be NULL to return complete arrays or objects (this is
203 * useful together with <var>$keyCol</var> to reindex the array).
204 * @param string $keyCol The index of the column or name of object property to be used as key
205 *
206 * @return array Column of values from the source array
207 *
208 * @since 1.0
209 * @see http://php.net/manual/en/language.types.array.php
210 * @see http://php.net/manual/en/function.array-column.php
211 */
212 public static function getColumn(array $array, $valueCol, $keyCol = null)
213 {
214 $result = array();
215
216 foreach ($array as $item)
217 {
218 // Convert object to array
219 $subject = is_object($item) ? static::fromObject($item) : $item;
220
221 /*
222 * We process arrays (and objects already converted to array)
223 * Only if the value column (if required) exists in this item
224 */
225 if (is_array($subject) && (!isset($valueCol) || isset($subject[$valueCol])))
226 {
227 // Use whole $item if valueCol is null, else use the value column.
228 $value = isset($valueCol) ? $subject[$valueCol] : $item;
229
230 // Array keys can only be integer or string. Casting will occur as per the PHP Manual.
231 if (isset($keyCol) && isset($subject[$keyCol]) && is_scalar($subject[$keyCol]))
232 {
233 $key = $subject[$keyCol];
234 $result[$key] = $value;
235 }
236 else
237 {
238 $result[] = $value;
239 }
240 }
241 }
242
243 return $result;
244 }
245
246 /**
247 * Utility function to return a value from a named array or a specified default
248 *
249 * @param array|\ArrayAccess $array A named array or object that implements ArrayAccess
250 * @param string $name The key to search for
251 * @param mixed $default The default value to give if no key found
252 * @param string $type Return type for the variable (INT, FLOAT, STRING, WORD, BOOLEAN, ARRAY)
253 *
254 * @return mixed
255 *
256 * @since 1.0
257 * @throws \InvalidArgumentException
258 */
259 public static function getValue($array, $name, $default = null, $type = '')
260 {
261 if (!is_array($array) && !($array instanceof \ArrayAccess))
262 {
263 throw new \InvalidArgumentException('The object must be an array or an object that implements ArrayAccess');
264 }
265
266 $result = null;
267
268 if (isset($array[$name]))
269 {
270 $result = $array[$name];
271 }
272
273 // Handle the default case
274 if (is_null($result))
275 {
276 $result = $default;
277 }
278
279 // Handle the type constraint
280 switch (strtoupper($type))
281 {
282 case 'INT':
283 case 'INTEGER':
284 // Only use the first integer value
285 @preg_match('/-?[0-9]+/', $result, $matches);
286 $result = @(int) $matches[0];
287 break;
288
289 case 'FLOAT':
290 case 'DOUBLE':
291 // Only use the first floating point value
292 @preg_match('/-?[0-9]+(\.[0-9]+)?/', $result, $matches);
293 $result = @(float) $matches[0];
294 break;
295
296 case 'BOOL':
297 case 'BOOLEAN':
298 $result = (bool) $result;
299 break;
300
301 case 'ARRAY':
302 if (!is_array($result))
303 {
304 $result = array($result);
305 }
306 break;
307
308 case 'STRING':
309 $result = (string) $result;
310 break;
311
312 case 'WORD':
313 $result = (string) preg_replace('#\W#', '', $result);
314 break;
315
316 case 'NONE':
317 default:
318 // No casting necessary
319 break;
320 }
321
322 return $result;
323 }
324
325 /**
326 * Takes an associative array of arrays and inverts the array keys to values using the array values as keys.
327 *
328 * Example:
329 * $input = array(
330 * 'New' => array('1000', '1500', '1750'),
331 * 'Used' => array('3000', '4000', '5000', '6000')
332 * );
333 * $output = ArrayHelper::invert($input);
334 *
335 * Output would be equal to:
336 * $output = array(
337 * '1000' => 'New',
338 * '1500' => 'New',
339 * '1750' => 'New',
340 * '3000' => 'Used',
341 * '4000' => 'Used',
342 * '5000' => 'Used',
343 * '6000' => 'Used'
344 * );
345 *
346 * @param array $array The source array.
347 *
348 * @return array
349 *
350 * @since 1.0
351 */
352 public static function invert(array $array)
353 {
354 $return = array();
355
356 foreach ($array as $base => $values)
357 {
358 if (!is_array($values))
359 {
360 continue;
361 }
362
363 foreach ($values as $key)
364 {
365 // If the key isn't scalar then ignore it.
366 if (is_scalar($key))
367 {
368 $return[$key] = $base;
369 }
370 }
371 }
372
373 return $return;
374 }
375
376 /**
377 * Method to determine if an array is an associative array.
378 *
379 * @param array $array An array to test.
380 *
381 * @return boolean
382 *
383 * @since 1.0
384 */
385 public static function isAssociative($array)
386 {
387 if (is_array($array))
388 {
389 foreach (array_keys($array) as $k => $v)
390 {
391 if ($k !== $v)
392 {
393 return true;
394 }
395 }
396 }
397
398 return false;
399 }
400
401 /**
402 * Pivots an array to create a reverse lookup of an array of scalars, arrays or objects.
403 *
404 * @param array $source The source array.
405 * @param string $key Where the elements of the source array are objects or arrays, the key to pivot on.
406 *
407 * @return array An array of arrays pivoted either on the value of the keys, or an individual key of an object or array.
408 *
409 * @since 1.0
410 */
411 public static function pivot(array $source, $key = null)
412 {
413 $result = array();
414 $counter = array();
415
416 foreach ($source as $index => $value)
417 {
418 // Determine the name of the pivot key, and its value.
419 if (is_array($value))
420 {
421 // If the key does not exist, ignore it.
422 if (!isset($value[$key]))
423 {
424 continue;
425 }
426
427 $resultKey = $value[$key];
428 $resultValue = $source[$index];
429 }
430 elseif (is_object($value))
431 {
432 // If the key does not exist, ignore it.
433 if (!isset($value->$key))
434 {
435 continue;
436 }
437
438 $resultKey = $value->$key;
439 $resultValue = $source[$index];
440 }
441 else
442 {
443 // Just a scalar value.
444 $resultKey = $value;
445 $resultValue = $index;
446 }
447
448 // The counter tracks how many times a key has been used.
449 if (empty($counter[$resultKey]))
450 {
451 // The first time around we just assign the value to the key.
452 $result[$resultKey] = $resultValue;
453 $counter[$resultKey] = 1;
454 }
455 elseif ($counter[$resultKey] == 1)
456 {
457 // If there is a second time, we convert the value into an array.
458 $result[$resultKey] = array(
459 $result[$resultKey],
460 $resultValue,
461 );
462 $counter[$resultKey]++;
463 }
464 else
465 {
466 // After the second time, no need to track any more. Just append to the existing array.
467 $result[$resultKey][] = $resultValue;
468 }
469 }
470
471 unset($counter);
472
473 return $result;
474 }
475
476 /**
477 * Utility function to sort an array of objects on a given field
478 *
479 * @param array $a An array of objects
480 * @param mixed $k The key (string) or an array of keys to sort on
481 * @param mixed $direction Direction (integer) or an array of direction to sort in [1 = Ascending] [-1 = Descending]
482 * @param mixed $caseSensitive Boolean or array of booleans to let sort occur case sensitive or insensitive
483 * @param mixed $locale Boolean or array of booleans to let sort occur using the locale language or not
484 *
485 * @return array
486 *
487 * @since 1.0
488 */
489 public static function sortObjects(array $a, $k, $direction = 1, $caseSensitive = true, $locale = false)
490 {
491 if (!is_array($locale) || !is_array($locale[0]))
492 {
493 $locale = array($locale);
494 }
495
496 $sortCase = (array) $caseSensitive;
497 $sortDirection = (array) $direction;
498 $key = (array) $k;
499 $sortLocale = $locale;
500
501 usort(
502 $a, function ($a, $b) use ($sortCase, $sortDirection, $key, $sortLocale)
503 {
504 for ($i = 0, $count = count($key); $i < $count; $i++)
505 {
506 if (isset($sortDirection[$i]))
507 {
508 $direction = $sortDirection[$i];
509 }
510
511 if (isset($sortCase[$i]))
512 {
513 $caseSensitive = $sortCase[$i];
514 }
515
516 if (isset($sortLocale[$i]))
517 {
518 $locale = $sortLocale[$i];
519 }
520
521 $va = $a->{$key[$i]};
522 $vb = $b->{$key[$i]};
523
524 if ((is_bool($va) || is_numeric($va)) && (is_bool($vb) || is_numeric($vb)))
525 {
526 $cmp = $va - $vb;
527 }
528 elseif ($caseSensitive)
529 {
530 $cmp = StringHelper::strcmp($va, $vb, $locale);
531 }
532 else
533 {
534 $cmp = StringHelper::strcasecmp($va, $vb, $locale);
535 }
536
537 if ($cmp > 0)
538 {
539 return $direction;
540 }
541
542 if ($cmp < 0)
543 {
544 return -$direction;
545 }
546 }
547
548 return 0;
549 }
550 );
551
552 return $a;
553 }
554
555 /**
556 * Multidimensional array safe unique test
557 *
558 * @param array $array The array to make unique.
559 *
560 * @return array
561 *
562 * @see http://php.net/manual/en/function.array-unique.php
563 * @since 1.0
564 */
565 public static function arrayUnique(array $array)
566 {
567 $array = array_map('serialize', $array);
568 $array = array_unique($array);
569 $array = array_map('unserialize', $array);
570
571 return $array;
572 }
573
574 /**
575 * An improved array_search that allows for partial matching of strings values in associative arrays.
576 *
577 * @param string $needle The text to search for within the array.
578 * @param array $haystack Associative array to search in to find $needle.
579 * @param boolean $caseSensitive True to search case sensitive, false otherwise.
580 *
581 * @return mixed Returns the matching array $key if found, otherwise false.
582 *
583 * @since 1.0
584 */
585 public static function arraySearch($needle, array $haystack, $caseSensitive = true)
586 {
587 foreach ($haystack as $key => $value)
588 {
589 $searchFunc = ($caseSensitive) ? 'strpos' : 'stripos';
590
591 if ($searchFunc($value, $needle) === 0)
592 {
593 return $key;
594 }
595 }
596
597 return false;
598 }
599
600 /**
601 * Method to recursively convert data to a one dimension array.
602 *
603 * @param array|object $array The array or object to convert.
604 * @param string $separator The key separator.
605 * @param string $prefix Last level key prefix.
606 *
607 * @return array
608 *
609 * @since 1.3.0
610 */
611 public static function flatten($array, $separator = '.', $prefix = '')
612 {
613 if ($array instanceof \Traversable)
614 {
615 $array = iterator_to_array($array);
616 }
617 elseif (is_object($array))
618 {
619 $array = get_object_vars($array);
620 }
621
622 foreach ($array as $k => $v)
623 {
624 $key = $prefix ? $prefix . $separator . $k : $k;
625
626 if (is_object($v) || is_array($v))
627 {
628 $array = array_merge($array, static::flatten($v, $separator, $key));
629 }
630 else
631 {
632 $array[$key] = $v;
633 }
634 }
635
636 return $array;
637 }
638 }
639