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