2f3594f231e30ede9de868460fceb73b8841b8e2
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Tests / Unit / Utility / ArrayUtilityTest.php
1 <?php
2 namespace TYPO3\CMS\Core\Tests\Unit\Utility;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Utility\ArrayUtility;
18 use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
19 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
20
21 /**
22 * Test case
23 */
24 class ArrayUtilityTest extends UnitTestCase
25 {
26 ///////////////////////
27 // Tests concerning filterByValueRecursive
28 ///////////////////////
29 /**
30 * Data provider for filterByValueRecursiveCorrectlyFiltersArray
31 *
32 * Every array splits into:
33 * - String value to search for
34 * - Input array
35 * - Expected result array
36 */
37 public function filterByValueRecursive()
38 {
39 return [
40 'empty search array' => [
41 'banana',
42 [],
43 []
44 ],
45 'empty string as needle' => [
46 '',
47 [
48 '',
49 'apple'
50 ],
51 [
52 ''
53 ]
54 ],
55 'flat array searching for string' => [
56 'banana',
57 [
58 'apple',
59 'banana'
60 ],
61 [
62 1 => 'banana'
63 ]
64 ],
65 'flat array searching for string with two matches' => [
66 'banana',
67 [
68 'foo' => 'apple',
69 'firstbanana' => 'banana',
70 'secondbanana' => 'banana'
71 ],
72 [
73 'firstbanana' => 'banana',
74 'secondbanana' => 'banana'
75 ]
76 ],
77 'multi dimensional array searching for string with multiple matches' => [
78 'banana',
79 [
80 'foo' => 'apple',
81 'firstbanana' => 'banana',
82 'grape' => [
83 'foo2' => 'apple2',
84 'secondbanana' => 'banana',
85 'foo3' => []
86 ],
87 'bar' => 'orange'
88 ],
89 [
90 'firstbanana' => 'banana',
91 'grape' => [
92 'secondbanana' => 'banana'
93 ]
94 ]
95 ],
96 'multi dimensional array searching for integer with multiple matches' => [
97 42,
98 [
99 'foo' => 23,
100 'bar' => 42,
101 [
102 'foo' => 23,
103 'bar' => 42
104 ]
105 ],
106 [
107 'bar' => 42,
108 [
109 'bar' => 42
110 ]
111 ]
112 ],
113 'flat array searching for boolean TRUE' => [
114 true,
115 [
116 23 => false,
117 42 => true
118 ],
119 [
120 42 => true
121 ]
122 ],
123 'multi dimensional array searching for boolean FALSE' => [
124 false,
125 [
126 23 => false,
127 42 => true,
128 'foo' => [
129 23 => false,
130 42 => true
131 ]
132 ],
133 [
134 23 => false,
135 'foo' => [
136 23 => false
137 ]
138 ]
139 ],
140 'flat array searching for array' => [
141 [
142 'foo' => 'bar'
143 ],
144 [
145 'foo' => 'bar',
146 'foobar' => [
147 'foo' => 'bar'
148 ]
149 ],
150 [
151 'foobar' => [
152 'foo' => 'bar'
153 ]
154 ]
155 ]
156 ];
157 }
158
159 /**
160 * @test
161 * @dataProvider filterByValueRecursive
162 * @param array $needle
163 * @param array $haystack
164 * @param array $expectedResult
165 */
166 public function filterByValueRecursiveCorrectlyFiltersArray($needle, $haystack, $expectedResult)
167 {
168 $this->assertEquals(
169 $expectedResult,
170 ArrayUtility::filterByValueRecursive($needle, $haystack)
171 );
172 }
173
174 /**
175 * @test
176 */
177 public function filterByValueRecursiveMatchesReferencesToSameObject()
178 {
179 $instance = new \stdClass();
180 $this->assertEquals(
181 [$instance],
182 ArrayUtility::filterByValueRecursive($instance, [$instance])
183 );
184 }
185
186 /**
187 * @test
188 */
189 public function filterByValueRecursiveDoesNotMatchDifferentInstancesOfSameClass()
190 {
191 $this->assertEquals(
192 [],
193 ArrayUtility::filterByValueRecursive(new \stdClass(), [new \stdClass()])
194 );
195 }
196
197 ///////////////////////
198 // Tests concerning isValidPath
199 ///////////////////////
200 /**
201 * @test
202 */
203 public function isValidPathReturnsTrueIfPathExists()
204 {
205 $this->assertTrue(ArrayUtility::isValidPath(['foo' => 'bar'], 'foo'));
206 }
207
208 /**
209 * @test
210 */
211 public function isValidPathReturnsFalseIfPathDoesNotExist()
212 {
213 $this->assertFalse(ArrayUtility::isValidPath(['foo' => 'bar'], 'bar'));
214 }
215
216 ///////////////////////
217 // Tests concerning getValueByPath
218 ///////////////////////
219 /**
220 * @test
221 */
222 public function getValueByPathThrowsExceptionIfPathIsNotString()
223 {
224 $this->expectException(\InvalidArgumentException::class);
225 $this->expectExceptionCode(1476557628);
226
227 ArrayUtility::getValueByPath([], 123);
228 }
229
230 /**
231 * @test
232 */
233 public function getValueByPathThrowsExceptionIfPathIsEmpty()
234 {
235 $this->expectException(\RuntimeException::class);
236 $this->expectExceptionCode(1341397767);
237
238 ArrayUtility::getValueByPath([], '');
239 }
240
241 /**
242 * @test
243 */
244 public function getValueByPathReturnsFirstIndexIfPathIsZero()
245 {
246 $this->assertSame('foo', ArrayUtility::getValueByPath(['foo'], '0'));
247 }
248
249 /**
250 * @test
251 */
252 public function getValueByPathReturnsFirstIndexIfPathSegmentIsZero()
253 {
254 $this->assertSame('bar', ArrayUtility::getValueByPath(['foo' => ['bar']], 'foo/0'));
255 }
256
257 /**
258 * Data provider for getValueByPathThrowsExceptionIfPathNotExists
259 * Every array splits into:
260 * - Array to get value from
261 * - String path
262 * - Expected result
263 * @return array
264 */
265 public function getValueByPathInvalidPathDataProvider()
266 {
267 return [
268 'not existing index' => [
269 [
270 'foo' => ['foo']
271 ],
272 'foo/1',
273 false
274 ],
275 'not existing path 1' => [
276 [
277 'foo' => []
278 ],
279 'foo/bar/baz',
280 false
281 ],
282 'not existing path 2' => [
283 [
284 'foo' => [
285 'baz' => 42
286 ],
287 'bar' => []
288 ],
289 'foo/bar/baz',
290 false
291 ],
292 // Negative test: This could be improved and the test moved to
293 // the valid data provider if the method supports this
294 'doubletick encapsulated quoted doubletick does not work' => [
295 [
296 '"foo"bar"' => [
297 'baz' => 42
298 ],
299 'bar' => []
300 ],
301 '"foo\\"bar"/baz',
302 42
303 ],
304 // Negative test: Method could be improved here
305 'path with doubletick does not work' => [
306 [
307 'fo"o' => [
308 'bar' => 42
309 ]
310 ],
311 'fo"o/foobar',
312 42
313 ]
314 ];
315 }
316
317 /**
318 * @test
319 * @dataProvider getValueByPathInvalidPathDataProvider
320 * @param array $array
321 * @param string $path
322 */
323 public function getValueByPathThrowsExceptionIfPathNotExists(array $array, $path)
324 {
325 $this->expectException(\RuntimeException::class);
326 $this->expectExceptionCode(1341397869);
327 ArrayUtility::getValueByPath($array, $path);
328 }
329
330 /**
331 * @test
332 * @dataProvider getValueByPathInvalidPathDataProvider
333 * @param array $array
334 * @param string $path
335 */
336 public function getValueByPathThrowsSpecificExceptionIfPathNotExists(array $array, string $path)
337 {
338 $this->expectException(MissingArrayPathException::class);
339 $this->expectExceptionCode(1341397869);
340 ArrayUtility::getValueByPath($array, $path);
341 }
342
343 /**
344 * Data provider for getValueByPathReturnsCorrectValue
345 * Every array splits into:
346 * - Array to get value from
347 * - String path
348 * - Expected result
349 */
350 public function getValueByPathValidDataProvider()
351 {
352 $testObject = new \stdClass();
353 $testObject->foo = 'foo';
354 $testObject->bar = 'bar';
355 return [
356 'integer in multi level array' => [
357 [
358 'foo' => [
359 'bar' => [
360 'baz' => 42
361 ],
362 'bar2' => []
363 ]
364 ],
365 'foo/bar/baz',
366 42
367 ],
368 'zero integer in multi level array' => [
369 [
370 'foo' => [
371 'bar' => [
372 'baz' => 0
373 ]
374 ]
375 ],
376 'foo/bar/baz',
377 0
378 ],
379 'NULL value in multi level array' => [
380 [
381 'foo' => [
382 'baz' => null
383 ]
384 ],
385 'foo/baz',
386 null
387 ],
388 'get string value' => [
389 [
390 'foo' => [
391 'baz' => 'this is a test string'
392 ]
393 ],
394 'foo/baz',
395 'this is a test string'
396 ],
397 'get boolean value: FALSE' => [
398 [
399 'foo' => [
400 'baz' => false
401 ]
402 ],
403 'foo/baz',
404 false
405 ],
406 'get boolean value: TRUE' => [
407 [
408 'foo' => [
409 'baz' => true
410 ]
411 ],
412 'foo/baz',
413 true
414 ],
415 'get object value' => [
416 [
417 'foo' => [
418 'baz' => $testObject
419 ]
420 ],
421 'foo/baz',
422 $testObject
423 ],
424 'enclosed path' => [
425 [
426 'foo/bar' => [
427 'foobar' => 42
428 ]
429 ],
430 '"foo/bar"/foobar',
431 42
432 ]
433 ];
434 }
435
436 /**
437 * @test
438 * @dataProvider getValueByPathValidDataProvider
439 * @param array $array
440 * @param string $path
441 * @param mixed $expectedResult
442 */
443 public function getValueByPathGetsCorrectValue(array $array, $path, $expectedResult)
444 {
445 $this->assertEquals($expectedResult, ArrayUtility::getValueByPath($array, $path));
446 }
447
448 /**
449 * @test
450 */
451 public function getValueByPathAcceptsDifferentDelimiter()
452 {
453 $input = [
454 'foo' => [
455 'bar' => [
456 'baz' => 42
457 ],
458 'bar2' => []
459 ]
460 ];
461 $searchPath = 'foo%bar%baz';
462 $expected = 42;
463 $delimiter = '%';
464 $this->assertEquals(
465 $expected,
466 ArrayUtility::getValueByPath($input, $searchPath, $delimiter)
467 );
468 }
469
470 ///////////////////////
471 // Tests concerning setValueByPath
472 ///////////////////////
473 /**
474 * @test
475 */
476 public function setValueByPathThrowsExceptionIfPathIsEmpty()
477 {
478 $this->expectException(\RuntimeException::class);
479 $this->expectExceptionCode(1341406194);
480
481 ArrayUtility::setValueByPath([], '', null);
482 }
483
484 /**
485 * @test
486 */
487 public function setValueByPathThrowsExceptionIfPathIsNotAString()
488 {
489 $this->expectException(\InvalidArgumentException::class);
490 $this->expectExceptionCode(1478781081);
491
492 ArrayUtility::setValueByPath([], 123, null);
493 }
494
495 /**
496 * @test
497 */
498 public function setValueByPathThrowsExceptionIfPathSegmentIsEmpty()
499 {
500 $this->expectException(\RuntimeException::class);
501 $this->expectExceptionCode(1341406846);
502
503 ArrayUtility::setValueByPath(['foo' => 'bar'], '/foo', 'value');
504 }
505
506 /**
507 * @test
508 */
509 public function setValueByPathCanUseZeroAsPathSegment()
510 {
511 $this->assertSame(['foo' => ['value']], ArrayUtility::setValueByPath(['foo' => []], 'foo/0', 'value'));
512 }
513
514 /**
515 * @test
516 */
517 public function setValueByPathCanUseZeroAsPath()
518 {
519 $this->assertSame(['value', 'bar'], ArrayUtility::setValueByPath(['foo', 'bar'], '0', 'value'));
520 }
521
522 /**
523 * Data provider for setValueByPathSetsCorrectValueDataProvider
524 *
525 * Every array splits into:
526 * - Array to set value in
527 * - String path
528 * - Value to set
529 * - Expected result
530 */
531 public function setValueByPathSetsCorrectValueDataProvider()
532 {
533 $testObject = new \stdClass();
534 $testObject->foo = 'foo';
535 $testObject->bar = 'bar';
536 return [
537 'set integer value: 42' => [
538 [
539 'foo' => [
540 'bar' => [
541 'baz' => 0
542 ]
543 ]
544 ],
545 'foo/bar/baz',
546 42,
547 [
548 'foo' => [
549 'bar' => [
550 'baz' => 42
551 ]
552 ]
553 ]
554 ],
555 'set integer value: 0' => [
556 [
557 'foo' => [
558 'bar' => [
559 'baz' => 42
560 ]
561 ]
562 ],
563 'foo/bar/baz',
564 0,
565 [
566 'foo' => [
567 'bar' => [
568 'baz' => 0
569 ]
570 ]
571 ]
572 ],
573 'set null value' => [
574 [
575 'foo' => [
576 'bar' => [
577 'baz' => 42
578 ]
579 ]
580 ],
581 'foo/bar/baz',
582 null,
583 [
584 'foo' => [
585 'bar' => [
586 'baz' => null
587 ]
588 ]
589 ]
590 ],
591 'set array value' => [
592 [
593 'foo' => [
594 'bar' => [
595 'baz' => 42
596 ]
597 ]
598 ],
599 'foo/bar/baz',
600 [
601 'foo' => 123
602 ],
603 [
604 'foo' => [
605 'bar' => [
606 'baz' => [
607 'foo' => 123
608 ]
609 ]
610 ]
611 ]
612 ],
613 'set boolean value: FALSE' => [
614 [
615 'foo' => [
616 'bar' => [
617 'baz' => true
618 ]
619 ]
620 ],
621 'foo/bar/baz',
622 false,
623 [
624 'foo' => [
625 'bar' => [
626 'baz' => false
627 ]
628 ]
629 ]
630 ],
631 'set boolean value: TRUE' => [
632 [
633 'foo' => [
634 'bar' => [
635 'baz' => null
636 ]
637 ]
638 ],
639 'foo/bar/baz',
640 true,
641 [
642 'foo' => [
643 'bar' => [
644 'baz' => true
645 ]
646 ]
647 ]
648 ],
649 'set object value' => [
650 [
651 'foo' => [
652 'bar' => [
653 'baz' => null
654 ]
655 ]
656 ],
657 'foo/bar/baz',
658 $testObject,
659 [
660 'foo' => [
661 'bar' => [
662 'baz' => $testObject
663 ]
664 ]
665 ]
666 ],
667 'multi keys in array' => [
668 [
669 'foo' => [
670 'bar' => [
671 'baz' => 'value'
672 ],
673 'bar2' => [
674 'baz' => 'value'
675 ]
676 ]
677 ],
678 'foo/bar2/baz',
679 'newValue',
680 [
681 'foo' => [
682 'bar' => [
683 'baz' => 'value'
684 ],
685 'bar2' => [
686 'baz' => 'newValue'
687 ]
688 ]
689 ]
690 ]
691 ];
692 }
693
694 /**
695 * @test
696 * @dataProvider setValueByPathSetsCorrectValueDataProvider
697 * @param array $array
698 * @param string $path
699 * @param string $value
700 * @param array $expectedResult
701 */
702 public function setValueByPathSetsCorrectValue(array $array, $path, $value, $expectedResult)
703 {
704 $this->assertEquals(
705 $expectedResult,
706 ArrayUtility::setValueByPath($array, $path, $value)
707 );
708 }
709
710 /**********************
711 /* Tests concerning removeByPath
712 ***********************/
713
714 /**
715 * @test
716 */
717 public function removeByPathThrowsExceptionIfPathIsEmpty()
718 {
719 $this->expectException(\RuntimeException::class);
720 $this->expectExceptionCode(1371757718);
721
722 ArrayUtility::removeByPath([], '');
723 }
724
725 /**
726 * @test
727 */
728 public function removeByPathThrowsExceptionIfPathIsNotAString()
729 {
730 $this->expectException(\RuntimeException::class);
731 $this->expectExceptionCode(1371757719);
732
733 ArrayUtility::removeByPath([], ['foo']);
734 }
735
736 /**
737 * @test
738 */
739 public function removeByPathThrowsExceptionWithEmptyPathSegment()
740 {
741 $inputArray = [
742 'foo' => [
743 'bar' => 42,
744 ]
745 ];
746
747 $this->expectException(\RuntimeException::class);
748 $this->expectExceptionCode(1371757720);
749
750 ArrayUtility::removeByPath($inputArray, 'foo//bar');
751 }
752
753 /**
754 * @test
755 */
756 public function removeByPathRemovesFirstIndexWithZeroAsPathSegment()
757 {
758 $inputArray = [
759 'foo' => ['bar']
760 ];
761
762 $this->assertSame(['foo' => []], ArrayUtility::removeByPath($inputArray, 'foo/0'));
763 }
764
765 /**
766 * @test
767 */
768 public function removeByPathRemovesFirstIndexWithZeroAsPath()
769 {
770 $inputArray = ['bar'];
771
772 $this->assertSame([], ArrayUtility::removeByPath($inputArray, '0'));
773 }
774
775 /**
776 * @test
777 */
778 public function removeByPathThrowsExceptionIfPathDoesNotExistInArray()
779 {
780 $inputArray = [
781 'foo' => [
782 'bar' => 42,
783 ]
784 ];
785
786 $this->expectException(\RuntimeException::class);
787 $this->expectExceptionCode(1371758436);
788
789 ArrayUtility::removeByPath($inputArray, 'foo/baz');
790 }
791
792 /**
793 * @test
794 */
795 public function removeByPathAcceptsGivenDelimiter()
796 {
797 $inputArray = [
798 'foo' => [
799 'toRemove' => 42,
800 'keep' => 23
801 ],
802 ];
803 $path = 'foo.toRemove';
804 $expected = [
805 'foo' => [
806 'keep' => 23,
807 ],
808 ];
809 $this->assertEquals(
810 $expected,
811 ArrayUtility::removeByPath($inputArray, $path, '.')
812 );
813 }
814
815 /**
816 * Data provider for removeByPathRemovesCorrectPath
817 */
818 public function removeByPathRemovesCorrectPathDataProvider()
819 {
820 return [
821 'single value' => [
822 [
823 'foo' => [
824 'toRemove' => 42,
825 'keep' => 23
826 ],
827 ],
828 'foo/toRemove',
829 [
830 'foo' => [
831 'keep' => 23,
832 ],
833 ],
834 ],
835 'whole array' => [
836 [
837 'foo' => [
838 'bar' => 42
839 ],
840 ],
841 'foo',
842 [],
843 ],
844 'sub array' => [
845 [
846 'foo' => [
847 'keep' => 23,
848 'toRemove' => [
849 'foo' => 'bar',
850 ],
851 ],
852 ],
853 'foo/toRemove',
854 [
855 'foo' => [
856 'keep' => 23,
857 ],
858 ],
859 ],
860 ];
861 }
862
863 /**
864 * @test
865 * @dataProvider removeByPathRemovesCorrectPathDataProvider
866 * @param array $array
867 * @param string $path
868 * @param array $expectedResult
869 */
870 public function removeByPathRemovesCorrectPath(array $array, $path, $expectedResult)
871 {
872 $this->assertEquals(
873 $expectedResult,
874 ArrayUtility::removeByPath($array, $path)
875 );
876 }
877
878 ///////////////////////
879 // Tests concerning sortByKeyRecursive
880 ///////////////////////
881 /**
882 * @test
883 */
884 public function sortByKeyRecursiveCheckIfSortingIsCorrect()
885 {
886 $unsortedArray = [
887 'z' => null,
888 'a' => null,
889 'd' => [
890 'c' => null,
891 'b' => null,
892 'd' => null,
893 'a' => null
894 ]
895 ];
896 $expectedResult = [
897 'a' => null,
898 'd' => [
899 'a' => null,
900 'b' => null,
901 'c' => null,
902 'd' => null
903 ],
904 'z' => null
905 ];
906 $this->assertSame($expectedResult, ArrayUtility::sortByKeyRecursive($unsortedArray));
907 }
908
909 ///////////////////////
910 // Tests concerning sortArraysByKey
911 ///////////////////////
912 /**
913 * Data provider for sortArraysByKeyCheckIfSortingIsCorrect
914 */
915 public function sortArraysByKeyCheckIfSortingIsCorrectDataProvider()
916 {
917 return [
918 'assoc array index' => [
919 [
920 '22' => [
921 'uid' => '22',
922 'title' => 'c',
923 'dummy' => 2
924 ],
925 '24' => [
926 'uid' => '24',
927 'title' => 'a',
928 'dummy' => 3
929 ],
930 '23' => [
931 'uid' => '23',
932 'title' => 'b',
933 'dummy' => 4
934 ],
935 ],
936 'title',
937 true,
938 [
939 '24' => [
940 'uid' => '24',
941 'title' => 'a',
942 'dummy' => 3
943 ],
944 '23' => [
945 'uid' => '23',
946 'title' => 'b',
947 'dummy' => 4
948 ],
949 '22' => [
950 'uid' => '22',
951 'title' => 'c',
952 'dummy' => 2
953 ],
954 ],
955 ],
956 'numeric array index' => [
957 [
958 22 => [
959 'uid' => '22',
960 'title' => 'c',
961 'dummy' => 2
962 ],
963 24 => [
964 'uid' => '24',
965 'title' => 'a',
966 'dummy' => 3
967 ],
968 23 => [
969 'uid' => '23',
970 'title' => 'b',
971 'dummy' => 4
972 ],
973 ],
974 'title',
975 true,
976 [
977 24 => [
978 'uid' => '24',
979 'title' => 'a',
980 'dummy' => 3
981 ],
982 23 => [
983 'uid' => '23',
984 'title' => 'b',
985 'dummy' => 4
986 ],
987 22 => [
988 'uid' => '22',
989 'title' => 'c',
990 'dummy' => 2
991 ],
992 ],
993 ],
994 'numeric array index DESC' => [
995 [
996 23 => [
997 'uid' => '23',
998 'title' => 'b',
999 'dummy' => 4
1000 ],
1001 22 => [
1002 'uid' => '22',
1003 'title' => 'c',
1004 'dummy' => 2
1005 ],
1006 24 => [
1007 'uid' => '24',
1008 'title' => 'a',
1009 'dummy' => 3
1010 ],
1011 ],
1012 'title',
1013 false,
1014 [
1015 22 => [
1016 'uid' => '22',
1017 'title' => 'c',
1018 'dummy' => 2
1019 ],
1020 23 => [
1021 'uid' => '23',
1022 'title' => 'b',
1023 'dummy' => 4
1024 ],
1025 24 => [
1026 'uid' => '24',
1027 'title' => 'a',
1028 'dummy' => 3
1029 ],
1030 ],
1031 ],
1032 ];
1033 }
1034
1035 /**
1036 * @test
1037 * @dataProvider sortArraysByKeyCheckIfSortingIsCorrectDataProvider
1038 * @param array $array
1039 * @param string $key
1040 * @param bool $ascending
1041 * @param array $expectedResult
1042 */
1043 public function sortArraysByKeyCheckIfSortingIsCorrect(array $array, $key, $ascending, $expectedResult)
1044 {
1045 $sortedArray = ArrayUtility::sortArraysByKey($array, $key, $ascending);
1046 $this->assertSame($expectedResult, $sortedArray);
1047 }
1048
1049 /**
1050 * @test
1051 */
1052 public function sortArraysByKeyThrowsExceptionForNonExistingKey()
1053 {
1054 $this->expectException(\RuntimeException::class);
1055 $this->expectExceptionCode(1373727309);
1056
1057 ArrayUtility::sortArraysByKey([['a'], ['a']], 'dummy');
1058 }
1059
1060 ///////////////////////
1061 // Tests concerning arrayExport
1062 ///////////////////////
1063 /**
1064 * @test
1065 */
1066 public function arrayExportReturnsFormattedMultidimensionalArray()
1067 {
1068 $array = [
1069 'foo' => [
1070 'bar' => 42,
1071 'bar2' => [
1072 'baz' => 'val\'ue',
1073 'baz2' => true,
1074 'baz3' => false,
1075 'baz4' => []
1076 ]
1077 ],
1078 'baz' => 23,
1079 'foobar' => null,
1080 'qux' => 0.1,
1081 'qux2' => 0.000000001,
1082 ];
1083 $expected =
1084 '[' . LF .
1085 ' \'foo\' => [' . LF .
1086 ' \'bar\' => 42,' . LF .
1087 ' \'bar2\' => [' . LF .
1088 ' \'baz\' => \'val\\\'ue\',' . LF .
1089 ' \'baz2\' => true,' . LF .
1090 ' \'baz3\' => false,' . LF .
1091 ' \'baz4\' => [],' . LF .
1092 ' ],' . LF .
1093 ' ],' . LF .
1094 ' \'baz\' => 23,' . LF .
1095 ' \'foobar\' => null,' . LF .
1096 ' \'qux\' => 0.1,' . LF .
1097 ' \'qux2\' => 1.0E-9,' . LF .
1098 ']';
1099 $this->assertSame($expected, ArrayUtility::arrayExport($array));
1100 }
1101
1102 /**
1103 * @test
1104 */
1105 public function arrayExportThrowsExceptionIfObjectShouldBeExported()
1106 {
1107 $array = [
1108 'foo' => [
1109 'bar' => new \stdClass()
1110 ]
1111 ];
1112
1113 $this->expectException(\RuntimeException::class);
1114 $this->expectExceptionCode(1342294987);
1115
1116 ArrayUtility::arrayExport($array);
1117 }
1118
1119 /**
1120 * @test
1121 */
1122 public function arrayExportReturnsNumericArrayKeys()
1123 {
1124 $array = [
1125 'foo' => 'string key',
1126 23 => 'integer key',
1127 '42' => 'string key representing integer'
1128 ];
1129 $expected =
1130 '[' . LF .
1131 ' \'foo\' => \'string key\',' . LF .
1132 ' 23 => \'integer key\',' . LF .
1133 ' 42 => \'string key representing integer\',' . LF .
1134 ']';
1135 $this->assertSame($expected, ArrayUtility::arrayExport($array));
1136 }
1137
1138 /**
1139 * @test
1140 */
1141 public function arrayExportReturnsNoKeyIndexForConsecutiveCountedArrays()
1142 {
1143 $array = [
1144 0 => 'zero',
1145 1 => 'one',
1146 2 => 'two'
1147 ];
1148 $expected =
1149 '[' . LF .
1150 ' \'zero\',' . LF .
1151 ' \'one\',' . LF .
1152 ' \'two\',' . LF .
1153 ']';
1154 $this->assertSame($expected, ArrayUtility::arrayExport($array));
1155 }
1156
1157 /**
1158 * @test
1159 */
1160 public function arrayExportReturnsKeyIndexForNonConsecutiveCountedArrays()
1161 {
1162 $array = [
1163 0 => 'zero',
1164 1 => 'one',
1165 3 => 'three',
1166 4 => 'four'
1167 ];
1168 $expected =
1169 '[' . LF .
1170 ' 0 => \'zero\',' . LF .
1171 ' 1 => \'one\',' . LF .
1172 ' 3 => \'three\',' . LF .
1173 ' 4 => \'four\',' . LF .
1174 ']';
1175 $this->assertSame($expected, ArrayUtility::arrayExport($array));
1176 }
1177
1178 ///////////////////////
1179 // Tests concerning flatten
1180 ///////////////////////
1181
1182 /**
1183 * @return array
1184 */
1185 public function flattenCalculatesExpectedResultDataProvider()
1186 {
1187 return [
1188 'plain array' => [
1189 [
1190 'first' => 1,
1191 'second' => 2
1192 ],
1193 [
1194 'first' => 1,
1195 'second' => 2
1196 ]
1197 ],
1198 'plain array with faulty dots' => [
1199 [
1200 'first.' => 1,
1201 'second.' => 2
1202 ],
1203 [
1204 'first' => 1,
1205 'second' => 2
1206 ]
1207 ],
1208 'nested array of 2 levels' => [
1209 [
1210 'first.' => [
1211 'firstSub' => 1
1212 ],
1213 'second.' => [
1214 'secondSub' => 2
1215 ]
1216 ],
1217 [
1218 'first.firstSub' => 1,
1219 'second.secondSub' => 2
1220 ]
1221 ],
1222 'nested array of 2 levels with faulty dots' => [
1223 [
1224 'first.' => [
1225 'firstSub.' => 1
1226 ],
1227 'second.' => [
1228 'secondSub.' => 2
1229 ]
1230 ],
1231 [
1232 'first.firstSub' => 1,
1233 'second.secondSub' => 2
1234 ]
1235 ],
1236 'nested array of 3 levels' => [
1237 [
1238 'first.' => [
1239 'firstSub.' => [
1240 'firstSubSub' => 1
1241 ]
1242 ],
1243 'second.' => [
1244 'secondSub.' => [
1245 'secondSubSub' => 2
1246 ]
1247 ]
1248 ],
1249 [
1250 'first.firstSub.firstSubSub' => 1,
1251 'second.secondSub.secondSubSub' => 2
1252 ]
1253 ],
1254 'nested array of 3 levels with faulty dots' => [
1255 [
1256 'first.' => [
1257 'firstSub.' => [
1258 'firstSubSub.' => 1
1259 ]
1260 ],
1261 'second.' => [
1262 'secondSub.' => [
1263 'secondSubSub.' => 2
1264 ]
1265 ]
1266 ],
1267 [
1268 'first.firstSub.firstSubSub' => 1,
1269 'second.secondSub.secondSubSub' => 2
1270 ]
1271 ]
1272 ];
1273 }
1274
1275 /**
1276 * @test
1277 * @param array $array
1278 * @param array $expected
1279 * @dataProvider flattenCalculatesExpectedResultDataProvider
1280 */
1281 public function flattenCalculatesExpectedResult(array $array, array $expected)
1282 {
1283 $this->assertEquals($expected, ArrayUtility::flatten($array));
1284 }
1285
1286 ///////////////////////
1287 // Tests concerning intersectRecursive
1288 ///////////////////////
1289
1290 /**
1291 * @return array
1292 */
1293 public function intersectRecursiveCalculatesExpectedResultDataProvider()
1294 {
1295 $sameObject = new \stdClass();
1296 return [
1297 // array($source, $mask, $expected)
1298 'empty array is returned if source is empty array' => [
1299 [],
1300 [
1301 'foo' => 'bar',
1302 ],
1303 [],
1304 ],
1305 'empty array is returned if mask is empty' => [
1306 [
1307 'foo' => 'bar',
1308 ],
1309 [],
1310 [],
1311 ],
1312 'key is kept on first level if exists in mask' => [
1313 [
1314 'foo' => 42,
1315 ],
1316 [
1317 'foo' => 42,
1318 ],
1319 [
1320 'foo' => 42,
1321 ],
1322 ],
1323 'value of key in source is kept if mask has different value' => [
1324 [
1325 'foo' => 42,
1326 ],
1327 [
1328 'foo' => new \stdClass(),
1329 ],
1330 [
1331 'foo' => 42,
1332 ],
1333 ],
1334 'key is kept on first level if according mask value is NULL' => [
1335 [
1336 'foo' => 42,
1337 ],
1338 [
1339 'foo' => null,
1340 ],
1341 [
1342 'foo' => 42,
1343 ],
1344 ],
1345 'null in source value is kept' => [
1346 [
1347 'foo' => null,
1348 ],
1349 [
1350 'foo' => 'bar',
1351 ],
1352 [
1353 'foo' => null,
1354 ]
1355 ],
1356 'mask does not add new keys' => [
1357 [
1358 'foo' => 42,
1359 ],
1360 [
1361 'foo' => 23,
1362 'bar' => [
1363 4711
1364 ],
1365 ],
1366 [
1367 'foo' => 42,
1368 ],
1369 ],
1370 'mask does not overwrite simple values with arrays' => [
1371 [
1372 'foo' => 42,
1373 ],
1374 [
1375 'foo' => [
1376 'bar' => 23,
1377 ],
1378 ],
1379 [
1380 'foo' => 42,
1381 ],
1382 ],
1383 'key is kept on first level if according mask value is array' => [
1384 [
1385 'foo' => 42,
1386 ],
1387 [
1388 'foo' => [
1389 'bar' => 23
1390 ],
1391 ],
1392 [
1393 'foo' => 42,
1394 ],
1395 ],
1396 'full array is kept if value is array and mask value is simple type' => [
1397 [
1398 'foo' => [
1399 'bar' => 23
1400 ],
1401 ],
1402 [
1403 'foo' => 42,
1404 ],
1405 [
1406 'foo' => [
1407 'bar' => 23
1408 ],
1409 ],
1410 ],
1411 'key handling is type agnostic' => [
1412 [
1413 42 => 'foo',
1414 ],
1415 [
1416 '42' => 'bar',
1417 ],
1418 [
1419 42 => 'foo',
1420 ],
1421 ],
1422 'value is same if value is object' => [
1423 [
1424 'foo' => $sameObject,
1425 ],
1426 [
1427 'foo' => 'something',
1428 ],
1429 [
1430 'foo' => $sameObject,
1431 ],
1432 ],
1433 'mask does not add simple value to result if key does not exist in source' => [
1434 [
1435 'foo' => '42',
1436 ],
1437 [
1438 'foo' => '42',
1439 'bar' => 23
1440 ],
1441 [
1442 'foo' => '42',
1443 ],
1444 ],
1445 'array of source is kept if value of mask key exists but is no array' => [
1446 [
1447 'foo' => '42',
1448 'bar' => [
1449 'baz' => 23
1450 ],
1451 ],
1452 [
1453 'foo' => 'value is not significant',
1454 'bar' => null,
1455 ],
1456 [
1457 'foo' => '42',
1458 'bar' => [
1459 'baz' => 23
1460 ],
1461 ],
1462 ],
1463 'sub arrays are kept if mask has according sub array key and is similar array' => [
1464 [
1465 'first1' => 42,
1466 'first2' => [
1467 'second1' => 23,
1468 'second2' => 4711,
1469 ],
1470 ],
1471 [
1472 'first1' => 42,
1473 'first2' => [
1474 'second1' => 'exists but different',
1475 ],
1476 ],
1477 [
1478 'first1' => 42,
1479 'first2' => [
1480 'second1' => 23,
1481 ],
1482 ],
1483 ],
1484 ];
1485 }
1486
1487 /**
1488 * @test
1489 * @param array $source
1490 * @param array $mask
1491 * @param array $expected
1492 * @dataProvider intersectRecursiveCalculatesExpectedResultDataProvider
1493 */
1494 public function intersectRecursiveCalculatesExpectedResult(array $source, array $mask, array $expected)
1495 {
1496 $this->assertSame($expected, ArrayUtility::intersectRecursive($source, $mask));
1497 }
1498
1499 ///////////////////////
1500 // Tests concerning renumberKeysToAvoidLeapsIfKeysAreAllNumeric
1501 ///////////////////////
1502 /**
1503 * @return array
1504 */
1505 public function renumberKeysToAvoidLeapsIfKeysAreAllNumericDataProvider()
1506 {
1507 return [
1508 'empty array is returned if source is empty array' => [
1509 [],
1510 []
1511 ],
1512 'returns self if array is already numerically keyed' => [
1513 [1, 2, 3],
1514 [1, 2, 3]
1515 ],
1516 'returns correctly if keys are numeric, but contains a leap' => [
1517 [0 => 'One', 1 => 'Two', 3 => 'Three'],
1518 [0 => 'One', 1 => 'Two', 2 => 'Three'],
1519 ],
1520 'returns correctly even though keys are strings but still numeric' => [
1521 ['0' => 'One', '1' => 'Two', '3' => 'Three'],
1522 [0 => 'One', 1 => 'Two', 2 => 'Three'],
1523 ],
1524 'returns correctly if just a single keys is not numeric' => [
1525 [0 => 'Zero', '1' => 'One', 'Two' => 'Two'],
1526 [0 => 'Zero', '1' => 'One', 'Two' => 'Two'],
1527 ],
1528 'returns unchanged if keys end with a dot' => [
1529 ['2.' => 'Two', '1.' => 'One', '0.' => 'Zero'],
1530 ['2.' => 'Two', '1.' => 'One', '0.' => 'Zero'],
1531 ],
1532 'return self with nested numerically keyed array' => [
1533 [
1534 'One',
1535 'Two',
1536 'Three',
1537 [
1538 'sub.One',
1539 'sub.Two',
1540 ]
1541 ],
1542 [
1543 'One',
1544 'Two',
1545 'Three',
1546 [
1547 'sub.One',
1548 'sub.Two',
1549 ]
1550 ]
1551 ],
1552 'returns correctly with nested numerically keyed array with leaps' => [
1553 [
1554 'One',
1555 'Two',
1556 'Three',
1557 [
1558 0 => 'sub.One',
1559 2 => 'sub.Two',
1560 ]
1561 ],
1562 [
1563 'One',
1564 'Two',
1565 'Three',
1566 [
1567 'sub.One',
1568 'sub.Two',
1569 ]
1570 ]
1571 ],
1572 'returns correctly with nested string-keyed array' => [
1573 [
1574 'One',
1575 'Two',
1576 'Three',
1577 [
1578 'one' => 'sub.One',
1579 'two' => 'sub.Two',
1580 ]
1581 ],
1582 [
1583 'One',
1584 'Two',
1585 'Three',
1586 [
1587 'one' => 'sub.One',
1588 'two' => 'sub.Two',
1589 ]
1590 ]
1591 ],
1592 'returns correctly with deeply nested arrays' => [
1593 [
1594 'One',
1595 'Two',
1596 [
1597 'one' => 1,
1598 'two' => 2,
1599 'three' => [
1600 2 => 'SubSubOne',
1601 5 => 'SubSubTwo',
1602 9 => [0, 1, 2],
1603 []
1604 ]
1605 ]
1606 ],
1607 [
1608 'One',
1609 'Two',
1610 [
1611 'one' => 1,
1612 'two' => 2,
1613 'three' => [
1614 'SubSubOne',
1615 'SubSubTwo',
1616 [0, 1, 2],
1617 []
1618 ]
1619 ]
1620 ]
1621 ]
1622 ];
1623 }
1624
1625 /**
1626 * @test
1627 * @param array $inputArray
1628 * @param array $expected
1629 * @dataProvider renumberKeysToAvoidLeapsIfKeysAreAllNumericDataProvider
1630 */
1631 public function renumberKeysToAvoidLeapsIfKeysAreAllNumericReturnsExpectedOrder(array $inputArray, array $expected)
1632 {
1633 $this->assertEquals($expected, ArrayUtility::renumberKeysToAvoidLeapsIfKeysAreAllNumeric($inputArray));
1634 }
1635
1636 /**
1637 * @return array
1638 */
1639 public function mergeRecursiveWithOverruleCalculatesExpectedResultDataProvider()
1640 {
1641 return [
1642 'Override array can reset string to array' => [
1643 [
1644 'first' => [
1645 'second' => 'foo',
1646 ],
1647 ],
1648 [
1649 'first' => [
1650 'second' => ['third' => 'bar'],
1651 ],
1652 ],
1653 true,
1654 true,
1655 true,
1656 [
1657 'first' => [
1658 'second' => ['third' => 'bar'],
1659 ],
1660 ],
1661 ],
1662 'Override array does not reset array to string (weird!)' => [
1663 [
1664 'first' => [],
1665 ],
1666 [
1667 'first' => 'foo',
1668 ],
1669 true,
1670 true,
1671 true,
1672 [
1673 'first' => [], // This is rather unexpected, naive expectation: first => 'foo'
1674 ],
1675 ],
1676 'Override array does override string with null' => [
1677 [
1678 'first' => 'foo',
1679 ],
1680 [
1681 'first' => null,
1682 ],
1683 true,
1684 true,
1685 true,
1686 [
1687 'first' => null,
1688 ],
1689 ],
1690 'Override array does override null with string' => [
1691 [
1692 'first' => null,
1693 ],
1694 [
1695 'first' => 'foo',
1696 ],
1697 true,
1698 true,
1699 true,
1700 [
1701 'first' => 'foo',
1702 ],
1703 ],
1704 'Override array does override null with empty string' => [
1705 [
1706 'first' => null,
1707 ],
1708 [
1709 'first' => '',
1710 ],
1711 true,
1712 true,
1713 true,
1714 [
1715 'first' => '',
1716 ],
1717 ],
1718 'Override array does not override string with NULL if requested' => [
1719 [
1720 'first' => 'foo',
1721 ],
1722 [
1723 'first' => null,
1724 ],
1725 true,
1726 false, // no include empty values
1727 true,
1728 [
1729 'first' => 'foo',
1730 ],
1731 ],
1732 'Override array does override null with null' => [
1733 [
1734 'first' => null,
1735 ],
1736 [
1737 'first' => null,
1738 ],
1739 true,
1740 true,
1741 true,
1742 [
1743 'first' => '',
1744 ],
1745 ],
1746 'Override array can __UNSET values' => [
1747 [
1748 'first' => [
1749 'second' => 'second',
1750 'third' => 'third',
1751 ],
1752 'fifth' => [],
1753 ],
1754 [
1755 'first' => [
1756 'second' => 'overrule',
1757 'third' => '__UNSET',
1758 'fourth' => 'overrile',
1759 ],
1760 'fifth' => '__UNSET',
1761 ],
1762 true,
1763 true,
1764 true,
1765 [
1766 'first' => [
1767 'second' => 'overrule',
1768 'fourth' => 'overrile',
1769 ],
1770 ],
1771 ],
1772 'Override can add keys' => [
1773 [
1774 'first' => 'foo',
1775 ],
1776 [
1777 'second' => 'bar',
1778 ],
1779 true,
1780 true,
1781 true,
1782 [
1783 'first' => 'foo',
1784 'second' => 'bar',
1785 ],
1786 ],
1787 'Override does not add key if __UNSET' => [
1788 [
1789 'first' => 'foo',
1790 ],
1791 [
1792 'second' => '__UNSET',
1793 ],
1794 true,
1795 true,
1796 true,
1797 [
1798 'first' => 'foo',
1799 ],
1800 ],
1801 'Override does not add key if not requested' => [
1802 [
1803 'first' => 'foo',
1804 ],
1805 [
1806 'second' => 'bar',
1807 ],
1808 false, // no add keys
1809 true,
1810 true,
1811 [
1812 'first' => 'foo',
1813 ],
1814 ],
1815 'Override does not add key if not requested with add include empty values' => [
1816 [
1817 'first' => 'foo',
1818 ],
1819 [
1820 'second' => 'bar',
1821 ],
1822 false, // no add keys
1823 false, // no include empty values
1824 true,
1825 [
1826 'first' => 'foo',
1827 ],
1828 ],
1829 'Override does not override string with empty string if requested' => [
1830 [
1831 'first' => 'foo',
1832 ],
1833 [
1834 'first' => '',
1835 ],
1836 true,
1837 false, // no include empty values
1838 true,
1839 [
1840 'first' => 'foo',
1841 ],
1842 ],
1843 'Override array does merge instead of __UNSET if requested (weird!)' => [
1844 [
1845 'first' => [
1846 'second' => 'second',
1847 'third' => 'third',
1848 ],
1849 'fifth' => [],
1850 ],
1851 [
1852 'first' => [
1853 'second' => 'overrule',
1854 'third' => '__UNSET',
1855 'fourth' => 'overrile',
1856 ],
1857 'fifth' => '__UNSET',
1858 ],
1859 true,
1860 true,
1861 false,
1862 [
1863 'first' => [
1864 'second' => 'overrule',
1865 'third' => '__UNSET', // overruled
1866 'fourth' => 'overrile',
1867 ],
1868 'fifth' => [], // not overruled with string here, naive expectation: 'fifth' => '__UNSET'
1869 ],
1870 ],
1871 ];
1872 }
1873
1874 /**
1875 * @test
1876 * @dataProvider mergeRecursiveWithOverruleCalculatesExpectedResultDataProvider
1877 * @param array $input1 Input 1
1878 * @param array $input2 Input 2
1879 * @param bool $addKeys TRUE if should add keys, else FALSE
1880 * @param bool $includeEmptyValues TRUE if should include empty values, else FALSE
1881 * @param bool $enableUnsetFeature TRUE if should enable unset feature, else FALSE
1882 * @param array $expected expected array
1883 */
1884 public function mergeRecursiveWithOverruleCalculatesExpectedResult($input1, $input2, $addKeys, $includeEmptyValues, $enableUnsetFeature, $expected)
1885 {
1886 ArrayUtility::mergeRecursiveWithOverrule($input1, $input2, $addKeys, $includeEmptyValues, $enableUnsetFeature);
1887 $this->assertEquals($expected, $input1);
1888 }
1889
1890 //////////////////////////////////
1891 // Tests concerning removeArrayEntryByValue
1892 //////////////////////////////////
1893 /**
1894 * @test
1895 */
1896 public function checkRemoveArrayEntryByValueRemovesEntriesFromOneDimensionalArray()
1897 {
1898 $inputArray = [
1899 '0' => 'test1',
1900 '1' => 'test2',
1901 '2' => 'test3',
1902 '3' => 'test2'
1903 ];
1904 $compareValue = 'test2';
1905 $expectedResult = [
1906 '0' => 'test1',
1907 '2' => 'test3'
1908 ];
1909 $actualResult = ArrayUtility::removeArrayEntryByValue($inputArray, $compareValue);
1910 $this->assertEquals($expectedResult, $actualResult);
1911 }
1912
1913 /**
1914 * @test
1915 */
1916 public function checkRemoveArrayEntryByValueRemovesEntriesFromMultiDimensionalArray()
1917 {
1918 $inputArray = [
1919 '0' => 'foo',
1920 '1' => [
1921 '10' => 'bar'
1922 ],
1923 '2' => 'bar'
1924 ];
1925 $compareValue = 'bar';
1926 $expectedResult = [
1927 '0' => 'foo',
1928 '1' => []
1929 ];
1930 $actualResult = ArrayUtility::removeArrayEntryByValue($inputArray, $compareValue);
1931 $this->assertEquals($expectedResult, $actualResult);
1932 }
1933
1934 /**
1935 * @test
1936 */
1937 public function checkRemoveArrayEntryByValueRemovesEntryWithEmptyString()
1938 {
1939 $inputArray = [
1940 '0' => 'foo',
1941 '1' => '',
1942 '2' => 'bar'
1943 ];
1944 $compareValue = '';
1945 $expectedResult = [
1946 '0' => 'foo',
1947 '2' => 'bar'
1948 ];
1949 $actualResult = ArrayUtility::removeArrayEntryByValue($inputArray, $compareValue);
1950 $this->assertEquals($expectedResult, $actualResult);
1951 }
1952
1953 //////////////////////////////////
1954 // Tests concerning keepItemsInArray
1955 //////////////////////////////////
1956 /**
1957 * @test
1958 * @dataProvider keepItemsInArrayWorksWithOneArgumentDataProvider
1959 * @param mixed $search The items which are allowed/kept in the array
1960 * @param array $array target array
1961 * @param array $expected expected array
1962 */
1963 public function keepItemsInArrayWorksWithOneArgument($search, $array, $expected)
1964 {
1965 $this->assertEquals($expected, ArrayUtility::keepItemsInArray($array, $search));
1966 }
1967
1968 /**
1969 * Data provider for keepItemsInArrayWorksWithOneArgument
1970 *
1971 * @return array
1972 */
1973 public function keepItemsInArrayWorksWithOneArgumentDataProvider()
1974 {
1975 $array = [
1976 0 => 0,
1977 'one' => 'one',
1978 'two' => 'two',
1979 'three' => 'three'
1980 ];
1981 return [
1982 'Empty argument will match "all" elements' => [null, $array, $array],
1983 'No match' => ['four', $array, []],
1984 'One match' => ['two', $array, ['two' => 'two']],
1985 'Multiple matches' => ['two,one', $array, ['one' => 'one', 'two' => 'two']],
1986 'Argument can be an array' => [['three'], $array, ['three' => 'three']]
1987 ];
1988 }
1989
1990 /**
1991 * Shows the example from the doc comment where
1992 * a function is used to reduce the sub arrays to one item which
1993 * is then used for the matching.
1994 *
1995 * @test
1996 */
1997 public function keepItemsInArrayCanUseClosure()
1998 {
1999 $array = [
2000 'aa' => ['first', 'second'],
2001 'bb' => ['third', 'fourth'],
2002 'cc' => ['fifth', 'sixth']
2003 ];
2004 $expected = ['bb' => ['third', 'fourth']];
2005 $keepItems = 'third';
2006 $match = ArrayUtility::keepItemsInArray(
2007 $array,
2008 $keepItems,
2009 function ($value) {
2010 return $value[0];
2011 }
2012 );
2013 $this->assertEquals($expected, $match);
2014 }
2015
2016 //////////////////////////////////
2017 // Tests concerning remapArrayKeys
2018 //////////////////////////////////
2019 /**
2020 * @test
2021 */
2022 public function remapArrayKeysExchangesKeysWithGivenMapping()
2023 {
2024 $array = [
2025 'one' => 'one',
2026 'two' => 'two',
2027 'three' => 'three'
2028 ];
2029 $keyMapping = [
2030 'one' => '1',
2031 'two' => '2'
2032 ];
2033 $expected = [
2034 '1' => 'one',
2035 '2' => 'two',
2036 'three' => 'three'
2037 ];
2038 ArrayUtility::remapArrayKeys($array, $keyMapping);
2039 $this->assertEquals($expected, $array);
2040 }
2041
2042 //////////////////////////////////////
2043 // Tests concerning arrayDiffAssocRecursive
2044 //////////////////////////////////////
2045 /**
2046 * @test
2047 */
2048 public function arrayDiffAssocRecursiveHandlesOneDimensionalArrays()
2049 {
2050 $array1 = [
2051 'key1' => 'value1',
2052 'key2' => 'value2',
2053 'key3' => 'value3'
2054 ];
2055 $array2 = [
2056 'key1' => 'value1',
2057 'key3' => 'value3'
2058 ];
2059 $expectedResult = [
2060 'key2' => 'value2'
2061 ];
2062 $actualResult = ArrayUtility::arrayDiffAssocRecursive($array1, $array2);
2063 $this->assertEquals($expectedResult, $actualResult);
2064 }
2065
2066 /**
2067 * @test
2068 */
2069 public function arrayDiffAssocRecursiveHandlesMultiDimensionalArrays()
2070 {
2071 $array1 = [
2072 'key1' => 'value1',
2073 'key2' => [
2074 'key21' => 'value21',
2075 'key22' => 'value22',
2076 'key23' => [
2077 'key231' => 'value231',
2078 'key232' => 'value232'
2079 ]
2080 ]
2081 ];
2082 $array2 = [
2083 'key1' => 'value1',
2084 'key2' => [
2085 'key21' => 'value21',
2086 'key23' => [
2087 'key231' => 'value231'
2088 ]
2089 ]
2090 ];
2091 $expectedResult = [
2092 'key2' => [
2093 'key22' => 'value22',
2094 'key23' => [
2095 'key232' => 'value232'
2096 ]
2097 ]
2098 ];
2099 $actualResult = ArrayUtility::arrayDiffAssocRecursive($array1, $array2);
2100 $this->assertEquals($expectedResult, $actualResult);
2101 }
2102
2103 /**
2104 * @test
2105 */
2106 public function arrayDiffAssocRecursiveHandlesMixedArrays()
2107 {
2108 $array1 = [
2109 'key1' => [
2110 'key11' => 'value11',
2111 'key12' => 'value12'
2112 ],
2113 'key2' => 'value2',
2114 'key3' => 'value3'
2115 ];
2116 $array2 = [
2117 'key1' => 'value1',
2118 'key2' => [
2119 'key21' => 'value21'
2120 ]
2121 ];
2122 $expectedResult = [
2123 'key3' => 'value3'
2124 ];
2125 $actualResult = ArrayUtility::arrayDiffAssocRecursive($array1, $array2);
2126 $this->assertEquals($expectedResult, $actualResult);
2127 }
2128
2129 //////////////////////////////////////
2130 // Tests concerning naturalKeySortRecursive
2131 //////////////////////////////////////
2132
2133 /**
2134 * @test
2135 */
2136 public function naturalKeySortRecursiveSortsOneDimensionalArrayByNaturalOrder()
2137 {
2138 $testArray = [
2139 'bb' => 'bb',
2140 'ab' => 'ab',
2141 '123' => '123',
2142 'aaa' => 'aaa',
2143 'abc' => 'abc',
2144 '23' => '23',
2145 'ba' => 'ba',
2146 'bad' => 'bad',
2147 '2' => '2',
2148 'zap' => 'zap',
2149 '210' => '210'
2150 ];
2151 $expectedResult = [
2152 '2',
2153 '23',
2154 '123',
2155 '210',
2156 'aaa',
2157 'ab',
2158 'abc',
2159 'ba',
2160 'bad',
2161 'bb',
2162 'zap'
2163 ];
2164 ArrayUtility::naturalKeySortRecursive($testArray);
2165 $this->assertEquals($expectedResult, array_values($testArray));
2166 }
2167
2168 /**
2169 * @test
2170 */
2171 public function naturalKeySortRecursiveSortsMultiDimensionalArrayByNaturalOrder()
2172 {
2173 $testArray = [
2174 '2' => '2',
2175 'bb' => 'bb',
2176 'ab' => 'ab',
2177 '23' => '23',
2178 'aaa' => [
2179 'bb' => 'bb',
2180 'ab' => 'ab',
2181 '123' => '123',
2182 'aaa' => 'aaa',
2183 '2' => '2',
2184 'abc' => 'abc',
2185 'ba' => 'ba',
2186 '23' => '23',
2187 'bad' => [
2188 'bb' => 'bb',
2189 'ab' => 'ab',
2190 '123' => '123',
2191 'aaa' => 'aaa',
2192 'abc' => 'abc',
2193 '23' => '23',
2194 'ba' => 'ba',
2195 'bad' => 'bad',
2196 '2' => '2',
2197 'zap' => 'zap',
2198 '210' => '210'
2199 ],
2200 '210' => '210',
2201 'zap' => 'zap'
2202 ],
2203 'abc' => 'abc',
2204 'ba' => 'ba',
2205 '210' => '210',
2206 'bad' => 'bad',
2207 '123' => '123',
2208 'zap' => 'zap'
2209 ];
2210 $expectedResult = [
2211 '2',
2212 '23',
2213 '123',
2214 '210',
2215 'aaa',
2216 'ab',
2217 'abc',
2218 'ba',
2219 'bad',
2220 'bb',
2221 'zap'
2222 ];
2223 ArrayUtility::naturalKeySortRecursive($testArray);
2224 $this->assertEquals($expectedResult, array_values(array_keys($testArray['aaa']['bad'])));
2225 $this->assertEquals($expectedResult, array_values(array_keys($testArray['aaa'])));
2226 $this->assertEquals($expectedResult, array_values(array_keys($testArray)));
2227 }
2228
2229 /**
2230 * Data provider for filterAndSortByNumericKeysBehavesCorrectlyForAcceptAnyKeysIsTrue
2231 *
2232 * @return array
2233 */
2234 public function filterAndSortByNumericKeysWithAcceptAnyKey()
2235 {
2236 return [
2237 'ordered list of plain numeric keys' => [
2238 'input' => [
2239 '10' => 'foo',
2240 '20' => 'bar',
2241 ],
2242 'expected' => [
2243 10,
2244 20,
2245 ],
2246 ],
2247 'unordered list of plain numeric keys' => [
2248 'input' => [
2249 '20' => 'bar',
2250 '10' => 'foo',
2251 ],
2252 'expected' => [
2253 10,
2254 20,
2255 ],
2256 ],
2257 'list of string keys' => [
2258 'input' => [
2259 '10.' => [
2260 'wrap' => 'foo',
2261 ],
2262 '20.' => [
2263 'wrap' => 'bar',
2264 ],
2265 ],
2266 'expected' => [
2267 10,
2268 20,
2269 ],
2270 ],
2271 'list of mixed keys' => [
2272 'input' => [
2273 '10' => 'foo',
2274 '20.' => [
2275 'wrap' => 'bar',
2276 ],
2277 ],
2278 'expected' => [
2279 10,
2280 20,
2281 ],
2282 ],
2283 'list of mixed keys with one not interpreted as integer' => [
2284 'input' => [
2285 '10' => 'foo',
2286 'bla20.' => [
2287 'wrap' => 'bar',
2288 ],
2289 ],
2290 'expected' => [
2291 0,
2292 10,
2293 ],
2294 ],
2295 'list of mixed keys with more than one not interpreted as integer' => [
2296 'input' => [
2297 '10' => 'foo',
2298 'bla20.' => [
2299 'wrap' => 'bar',
2300 ],
2301 'bla21.' => [
2302 'wrap' => 'foobar',
2303 ],
2304 ],
2305 'expected' => [
2306 0,
2307 10,
2308 ],
2309 ],
2310 ];
2311 }
2312
2313 /**
2314 * @test
2315 * @dataProvider filterAndSortByNumericKeysWithAcceptAnyKey
2316 *
2317 * @param array $input
2318 * @param array $expected
2319 */
2320 public function filterAndSortByNumericKeysBehavesCorrectlyForAcceptAnyKeysIsTrue($input, $expected)
2321 {
2322 $result = ArrayUtility::filterAndSortByNumericKeys($input, true);
2323 $this->assertEquals($result, $expected);
2324 }
2325
2326 /**
2327 * Data provider for filterAndSortByNumericKeysBehavesCorrectlyForAcceptAnyKeysIsFalse
2328 *
2329 * @return array
2330 */
2331 public function filterAndSortByNumericKeysWithoutAcceptAnyKey()
2332 {
2333 return [
2334 'ordered list of plain numeric keys' => [
2335 'input' => [
2336 '10' => 'foo',
2337 '20' => 'bar',
2338 ],
2339 'expected' => [
2340 10,
2341 20,
2342 ],
2343 ],
2344 'unordered list of plain numeric keys' => [
2345 'input' => [
2346 '20' => 'bar',
2347 '10' => 'foo',
2348 ],
2349 'expected' => [
2350 10,
2351 20,
2352 ],
2353 ],
2354 'list of string keys' => [
2355 'input' => [
2356 '10.' => [
2357 'wrap' => 'foo',
2358 ],
2359 '20.' => [
2360 'wrap' => 'bar',
2361 ],
2362 ],
2363 'expected' => [],
2364 ],
2365 'list of mixed keys' => [
2366 'input' => [
2367 '10' => 'foo',
2368 '20.' => [
2369 'wrap' => 'bar',
2370 ],
2371 ],
2372 'expected' => [
2373 10,
2374 ],
2375 ],
2376 ];
2377 }
2378
2379 /**
2380 * @test
2381 * @dataProvider filterAndSortByNumericKeysWithoutAcceptAnyKey
2382 *
2383 * @param array $input
2384 * @param array $expected
2385 */
2386 public function filterAndSortByNumericKeysBehavesCorrectlyForAcceptAnyKeysIsFalse($input, $expected)
2387 {
2388 $result = ArrayUtility::filterAndSortByNumericKeys($input);
2389 $this->assertEquals($result, $expected);
2390 }
2391
2392 /**
2393 * dataProvider for sortArrayWithIntegerKeys
2394 *
2395 * @return array
2396 */
2397 public function sortArrayWithIntegerKeysDataProvider()
2398 {
2399 return [
2400 [
2401 [
2402 '20' => 'test1',
2403 '11' => 'test2',
2404 '16' => 'test3',
2405 ],
2406 [
2407 '11' => 'test2',
2408 '16' => 'test3',
2409 '20' => 'test1',
2410 ]
2411 ],
2412 [
2413 [
2414 '20' => 'test1',
2415 '16.5' => 'test2',
2416 '16' => 'test3',
2417 ],
2418 [
2419 '20' => 'test1',
2420 '16.5' => 'test2',
2421 '16' => 'test3',
2422 ]
2423 ],
2424 [
2425 [
2426 '20' => 'test20',
2427 'somestring' => 'teststring',
2428 '16' => 'test16',
2429 ],
2430 [
2431 '20' => 'test20',
2432 'somestring' => 'teststring',
2433 '16' => 'test16',
2434 ]
2435 ],
2436 ];
2437 }
2438
2439 /**
2440 * @test
2441 *
2442 * @param array $arrayToSort
2443 * @param array $expectedArray
2444 *
2445 * @dataProvider sortArrayWithIntegerKeysDataProvider
2446 */
2447 public function sortArrayWithIntegerKeysSortsNumericArrays(array $arrayToSort, array $expectedArray)
2448 {
2449 $sortedArray = ArrayUtility::sortArrayWithIntegerKeys($arrayToSort);
2450 $this->assertSame($sortedArray, $expectedArray);
2451 }
2452
2453 /**
2454 * @test
2455 */
2456 public function assertAllArrayKeysAreValidThrowsExceptionOnNotAllowedArrayKeys()
2457 {
2458 $this->expectException(\InvalidArgumentException::class);
2459 $this->expectExceptionCode(1325697085);
2460
2461 $arrayToTest = [
2462 'roger' => '',
2463 'francine' => '',
2464 'stan' => '',
2465 ];
2466
2467 $allowedArrayKeys = [
2468 'roger',
2469 'francine',
2470 ];
2471
2472 ArrayUtility::assertAllArrayKeysAreValid($arrayToTest, $allowedArrayKeys);
2473 }
2474
2475 /**
2476 * @test
2477 */
2478 public function assertAllArrayKeysAreValidReturnsNullOnAllowedArrayKeys()
2479 {
2480 $arrayToTest = [
2481 'roger' => '',
2482 'francine' => '',
2483 'stan' => '',
2484 ];
2485
2486 $allowedArrayKeys = [
2487 'roger',
2488 'francine',
2489 'stan',
2490 ];
2491
2492 ArrayUtility::assertAllArrayKeysAreValid($arrayToTest, $allowedArrayKeys);
2493 }
2494
2495 /**
2496 * @test
2497 */
2498 public function sortArrayWithIntegerKeysRecursiveExpectSorting()
2499 {
2500 $input = [
2501 20 => 'b',
2502 10 => 'a',
2503 40 => 'd',
2504 30 => 'c',
2505 50 => [
2506 20 => 'a',
2507 10 => 'b',
2508 ],
2509 ];
2510
2511 $expected = [
2512 10 => 'a',
2513 20 => 'b',
2514 30 => 'c',
2515 40 => 'd',
2516 50 => [
2517 10 => 'b',
2518 20 => 'a',
2519 ],
2520 ];
2521
2522 $this->assertSame($expected, ArrayUtility::sortArrayWithIntegerKeysRecursive($input));
2523 }
2524
2525 /**
2526 * @test
2527 */
2528 public function sortArrayWithIntegerKeysRecursiveExpectNoSorting()
2529 {
2530 $input = [
2531 'b' => 'b',
2532 10 => 'a',
2533 40 => 'd',
2534 30 => 'c',
2535 ];
2536
2537 $expected = [
2538 'b' => 'b',
2539 10 => 'a',
2540 40 => 'd',
2541 30 => 'c',
2542 ];
2543
2544 $this->assertSame($expected, ArrayUtility::sortArrayWithIntegerKeysRecursive($input));
2545 }
2546
2547 /**
2548 * @test
2549 */
2550 public function reIndexNumericArrayKeysRecursiveExpectReindexing()
2551 {
2552 $input = [
2553 20 => 'b',
2554 10 => 'a',
2555 40 => 'd',
2556 30 => 'c',
2557 50 => [
2558 20 => 'a',
2559 10 => 'b',
2560 ],
2561 ];
2562
2563 $expected = [
2564 0 => 'b',
2565 1 => 'a',
2566 2 => 'd',
2567 3 => 'c',
2568 4 => [
2569 0 => 'a',
2570 1 => 'b',
2571 ],
2572 ];
2573
2574 $this->assertSame($expected, ArrayUtility::reIndexNumericArrayKeysRecursive($input));
2575 }
2576
2577 /**
2578 * @test
2579 */
2580 public function reIndexNumericArrayKeysRecursiveExpectNoReindexing()
2581 {
2582 $input = [
2583 'a' => 'b',
2584 10 => 'a',
2585 40 => 'd',
2586 30 => 'c',
2587 50 => [
2588 20 => 'a',
2589 10 => 'b',
2590 ],
2591 ];
2592
2593 $expected = [
2594 'a' => 'b',
2595 10 => 'a',
2596 40 => 'd',
2597 30 => 'c',
2598 50 => [
2599 0 => 'a',
2600 1 => 'b',
2601 ],
2602 ];
2603
2604 $this->assertSame($expected, ArrayUtility::reIndexNumericArrayKeysRecursive($input));
2605 }
2606
2607 /**
2608 * @test
2609 */
2610 public function removeNullValuesRecursiveExpectRemoval()
2611 {
2612 $input = [
2613 'a' => 'a',
2614 'b' => [
2615 'c' => null,
2616 'd' => 'd',
2617 ],
2618 ];
2619
2620 $expected = [
2621 'a' => 'a',
2622 'b' => [
2623 'd' => 'd',
2624 ],
2625 ];
2626
2627 $this->assertSame($expected, ArrayUtility::removeNullValuesRecursive($input));
2628 }
2629
2630 /**
2631 * @test
2632 */
2633 public function stripTagsFromValuesRecursiveExpectRemoval()
2634 {
2635 $input = [
2636 'a' => 'a',
2637 'b' => [
2638 'c' => '<b>i am evil</b>',
2639 'd' => 'd',
2640 ],
2641 ];
2642
2643 $expected = [
2644 'a' => 'a',
2645 'b' => [
2646 'c' => 'i am evil',
2647 'd' => 'd',
2648 ],
2649 ];
2650
2651 $this->assertSame($expected, ArrayUtility::stripTagsFromValuesRecursive($input));
2652 }
2653
2654 /**
2655 * @test
2656 */
2657 public function convertBooleanStringsToBooleanRecursiveExpectConverting()
2658 {
2659 $input = [
2660 'a' => 'a',
2661 'b' => [
2662 'c' => 'true',
2663 'd' => 'd',
2664 ],
2665 ];
2666
2667 $expected = [
2668 'a' => 'a',
2669 'b' => [
2670 'c' => true,
2671 'd' => 'd',
2672 ],
2673 ];
2674
2675 $this->assertSame($expected, ArrayUtility::convertBooleanStringsToBooleanRecursive($input));
2676 }
2677
2678 /**
2679 * Data provider for arrayFilterRecursiveFiltersFalseElements
2680 * @return array
2681 */
2682 public function filterRecursiveFiltersFalseElementsDataProvider()
2683 {
2684 return [
2685 'filter all values which will be false when converted to boolean' => [
2686 // input
2687 [
2688 true,
2689 false,
2690 'foo1' => [
2691 'bar' => [
2692 'baz' => [
2693 '1',
2694 null,
2695 '',
2696 ],
2697 '' => 1,
2698 'bbd' => 0,
2699 ]
2700 ],
2701 'foo2' => 'foo',
2702 'foo3' => '',
2703 'foo4' => [
2704 'z' => 'bar',
2705 'bar' => 0,
2706 'baz' => [
2707 'foo' => [
2708 'bar' => '',
2709 'boo' => [],
2710 'bamboo' => 5,
2711 'fooAndBoo' => [0],
2712 ]
2713 ],
2714 ],
2715 ],
2716 // expected
2717 [
2718 true,
2719 'foo1' => [
2720 'bar' => [
2721 'baz' => [
2722 '1',
2723 ],
2724 '' => 1,
2725 ]
2726 ],
2727 'foo2' => 'foo',
2728 'foo4' => [
2729 'z' => 'bar',
2730 'baz' => [
2731 'foo' => [
2732 'bamboo' => 5,
2733 'fooAndBoo' => [],
2734 ]
2735 ],
2736 ],
2737 ],
2738 ],
2739 ];
2740 }
2741
2742 /**
2743 * @test
2744 * @dataProvider filterRecursiveFiltersFalseElementsDataProvider
2745 * @param array $input
2746 * @param array $expectedResult
2747 */
2748 public function filterRecursiveFiltersFalseElements(array $input, array $expectedResult)
2749 {
2750 // If no callback is supplied, all entries of array equal to FALSE (see converting to boolean) will be removed.
2751 $result = ArrayUtility::filterRecursive($input);
2752 $this->assertEquals($expectedResult, $result);
2753 }
2754
2755 /**
2756 * Data provider for filterRecursiveCallbackFiltersEmptyElementsWithoutIntegerByCallback
2757 * @return array
2758 */
2759 public function filterRecursiveCallbackFiltersEmptyElementsWithoutIntegerZeroByCallbackDataProvider()
2760 {
2761 return [
2762 'filter empty values, keep zero integers' => [
2763 // input
2764 [
2765 true,
2766 false,
2767 'foo1' => [
2768 'bar' => [
2769 'baz' => [
2770 '1',
2771 null,
2772 '',
2773 ],
2774 '' => 1,
2775 'bbd' => 0,
2776 ]
2777 ],
2778 'foo2' => 'foo',
2779 'foo3' => '',
2780 'foo4' => [
2781 'z' => 'bar',
2782 'bar' => 0,
2783 'baz' => [
2784 'foo' => [
2785 'bar' => '',
2786 'boo' => [],
2787 'bamboo' => 5,
2788 'fooAndBoo' => [0],
2789 ]
2790 ],
2791 ],
2792 ],
2793 // expected
2794 [
2795 true,
2796 false,
2797 'foo1' => [
2798 'bar' => [
2799 'baz' => [
2800 '1',
2801 ],
2802 '' => 1,
2803 'bbd' => 0,
2804 ]
2805 ],
2806 'foo2' => 'foo',
2807 'foo4' => [
2808 'z' => 'bar',
2809 'bar' => 0,
2810 'baz' => [
2811 'foo' => [
2812 'bamboo' => 5,
2813 'fooAndBoo' => [0],
2814 ]
2815 ],
2816 ],
2817 ],
2818 ],
2819 ];
2820 }
2821
2822 /**
2823 * @test
2824 * @dataProvider filterRecursiveCallbackFiltersEmptyElementsWithoutIntegerZeroByCallbackDataProvider
2825 * @param array $input
2826 * @param array $expectedResult
2827 */
2828 public function filterRecursiveCallbackFiltersEmptyElementsWithoutIntegerByCallback(array $input, array $expectedResult)
2829 {
2830 // callback filters empty strings, array and null but keeps zero integers
2831 $result = ArrayUtility::filterRecursive(
2832 $input,
2833 function ($item) {
2834 return $item !== '' && $item !== [] && $item !== null;
2835 }
2836 );
2837 $this->assertEquals($expectedResult, $result);
2838 }
2839 }