@@ -49,18 +49,22 @@ final class Query implements ArrayAccess, Iterator
49
49
50
50
/**
51
51
* @param string|mixed[] $query
52
+ * @throws Exception
52
53
*/
53
54
public function __construct (string |array $ query )
54
55
{
55
56
if (is_string ($ query )) {
56
- $ this ->string = $ this ->encode ($ query );
57
+ $ this ->string = $ this ->sanitizeAndEncode ($ query );
57
58
}
58
59
59
60
if (is_array ($ query )) {
60
- $ this ->array = $ query ;
61
+ $ this ->array = $ this -> sanitizeArray ( $ query) ;
61
62
}
62
63
}
63
64
65
+ /**
66
+ * @throws Exception
67
+ */
64
68
public static function fromString (string $ queryString ): self
65
69
{
66
70
return new Query ($ queryString );
@@ -69,6 +73,7 @@ public static function fromString(string $queryString): self
69
73
/**
70
74
* @param mixed[] $queryArray
71
75
* @return static
76
+ * @throws Exception
72
77
*/
73
78
public static function fromArray (array $ queryArray ): self
74
79
{
@@ -83,11 +88,7 @@ public function toString(): string
83
88
if ($ this ->string === null || $ this ->isDirty ) {
84
89
$ array = $ this ->toArray ();
85
90
86
- if (!$ this ->boolToInt ) {
87
- $ array = $ this ->boolsToString ($ array );
88
- }
89
-
90
- $ this ->string = http_build_query ($ array , '' , $ this ->separator , $ this ->spaceCharacterEncoding );
91
+ $ this ->string = $ this ->arrayToString ($ array );
91
92
}
92
93
93
94
return $ this ->string ;
@@ -96,9 +97,9 @@ public function toString(): string
96
97
/**
97
98
* @throws Exception
98
99
*/
99
- public function toStringWithUnencodedBrackets (): string
100
+ public function toStringWithUnencodedBrackets (? string $ query = null ): string
100
101
{
101
- return str_replace (['%5B ' , '%5D ' ], ['[ ' , '] ' ], $ this ->toString ());
102
+ return str_replace (['%5B ' , '%5D ' ], ['[ ' , '] ' ], $ query ?? $ this ->toString ());
102
103
}
103
104
104
105
/**
@@ -530,6 +531,89 @@ public function rewind(): void
530
531
}
531
532
}
532
533
534
+ /**
535
+ * @throws Exception
536
+ */
537
+ private function sanitizeAndEncode (string $ query ): string
538
+ {
539
+ $ query = $ this ->sanitize ($ query );
540
+
541
+ return $ this ->encode ($ query );
542
+ }
543
+
544
+ /**
545
+ * @throws Exception
546
+ */
547
+ private function sanitize (string $ query ): string
548
+ {
549
+ while (str_starts_with ($ query , '& ' )) {
550
+ $ query = substr ($ query , 1 );
551
+ }
552
+
553
+ while (str_ends_with ($ query , '& ' )) {
554
+ $ query = substr ($ query , 0 , strlen ($ query ) - 1 );
555
+ }
556
+
557
+ $ query = preg_replace ('/&+/ ' , '& ' , $ query ) ?? $ query ;
558
+
559
+ return $ this ->arrayToString ($ this ->stringToArray ($ query ));
560
+ }
561
+
562
+ /**
563
+ * @param mixed[] $query
564
+ * @return mixed[]
565
+ * @throws Exception
566
+ */
567
+ private function sanitizeArray (array $ query ): array
568
+ {
569
+ $ originalInputQuery = $ query ;
570
+
571
+ $ query = $ this ->boolValuesToStringInSanitizeArray ($ query );
572
+
573
+ $ query = $ this ->stringToArray ($ this ->arrayToString ($ query ));
574
+
575
+ return $ this ->revertBoolValuesInSanitizeArray ($ query , $ originalInputQuery );
576
+ }
577
+
578
+ /**
579
+ * @param mixed[] $query
580
+ * @return mixed[]
581
+ */
582
+ private function boolValuesToStringInSanitizeArray (array $ query ): array
583
+ {
584
+ foreach ($ query as $ key => $ value ) {
585
+ if (is_array ($ value )) {
586
+ $ query [$ key ] = $ this ->boolValuesToStringInSanitizeArray ($ value );
587
+ }
588
+
589
+ if (is_bool ($ value )) {
590
+ $ query [$ key ] = $ value ? 'true ' : 'false ' ;
591
+ }
592
+ }
593
+
594
+ return $ query ;
595
+ }
596
+
597
+ /**
598
+ * @param mixed[] $query
599
+ * @param mixed[] $originalQuery
600
+ * @return mixed[]
601
+ */
602
+ private function revertBoolValuesInSanitizeArray (array $ query , array $ originalQuery ): array
603
+ {
604
+ foreach ($ query as $ key => $ value ) {
605
+ if (is_array ($ value )) {
606
+ $ query [$ key ] = $ this ->revertBoolValuesInSanitizeArray ($ value , $ originalQuery [$ key ]);
607
+ }
608
+
609
+ if (in_array ($ value , ['true ' , 'false ' ], true ) && is_bool ($ originalQuery [$ key ])) {
610
+ $ query [$ key ] = $ value === 'true ' ;
611
+ }
612
+ }
613
+
614
+ return $ query ;
615
+ }
616
+
533
617
/**
534
618
* Correctly encode a query string
535
619
*
@@ -570,22 +654,77 @@ private function encodePercentCharacter(string $string): string
570
654
* @return mixed[]
571
655
* @throws Exception
572
656
*/
573
- private function fixKeysContainingDotsOrSpaces (): array
657
+ private function fixKeysContainingDotsOrSpaces (string $ query ): array
574
658
{
575
- $ queryWithDotAndSpaceReplacements = $ this ->replaceDotsAndSpacesInKeys ($ this ->toStringWithUnencodedBrackets ());
659
+ $ queryWithDotAndSpaceReplacements = $ this ->replaceDotsAndSpacesInKeys (
660
+ $ this ->toStringWithUnencodedBrackets ($ query )
661
+ );
576
662
577
663
parse_str ($ queryWithDotAndSpaceReplacements , $ array );
578
664
579
665
return $ this ->revertDotAndSpaceReplacementsInKeys ($ array );
580
666
}
581
667
582
668
/**
669
+ * @param array<mixed>|Query $query
670
+ * @return array<mixed>|Query
583
671
* @throws Exception
584
672
*/
585
- private function containsDotOrSpaceInKey ( ): bool
673
+ private function replaceDotsAndSpacesInArrayKeys ( array | Query $ query ): array | Query
586
674
{
587
- return preg_match ('/(?:^|&)([^\[=&]*\.)/ ' , $ this ->toStringWithUnencodedBrackets ()) ||
588
- preg_match ('/(?:^|&)([^\[=&]* )/ ' , $ this ->toStringWithUnencodedBrackets ());
675
+ $ newQuery = [];
676
+
677
+ if ($ query instanceof Query) {
678
+ $ newQuery = new Query ($ query ->toArray ());
679
+ }
680
+
681
+ foreach ($ query as $ key => $ value ) {
682
+ if (is_array ($ value ) || $ value instanceof Query) {
683
+ $ value = $ this ->replaceDotsAndSpacesInArrayKeys ($ value );
684
+ }
685
+
686
+ $ key = str_replace (
687
+ ['. ' , ' ' ],
688
+ [self ::TEMP_DOT_REPLACEMENT , self ::TEMP_SPACE_REPLACEMENT ],
689
+ $ key ,
690
+ );
691
+
692
+ if (is_array ($ newQuery )) {
693
+ $ newQuery [$ key ] = $ value ;
694
+ } else {
695
+ $ newQuery ->set ($ key , $ value );
696
+ }
697
+ }
698
+
699
+ return $ newQuery ;
700
+ }
701
+
702
+ /**
703
+ * @throws Exception
704
+ */
705
+ private function containsDotOrSpaceInKey (string $ query ): bool
706
+ {
707
+ return preg_match ('/(?:^|&)([^\[=&]*\.)/ ' , $ this ->toStringWithUnencodedBrackets ($ query )) ||
708
+ preg_match ('/(?:^|&)([^\[=&]* )/ ' , $ this ->toStringWithUnencodedBrackets ($ query ));
709
+ }
710
+
711
+ /**
712
+ * @param array<mixed>|Query $query
713
+ * @return bool
714
+ */
715
+ private function arrayContainsDotOrSpacesInKey (array |Query $ query ): bool
716
+ {
717
+ foreach ($ query as $ key => $ value ) {
718
+ if (is_array ($ value ) && $ this ->arrayContainsDotOrSpacesInKey ($ value )) {
719
+ return true ;
720
+ }
721
+
722
+ if (str_contains ($ key , ' ' ) || str_contains ($ key , '. ' )) {
723
+ return true ;
724
+ }
725
+ }
726
+
727
+ return false ;
589
728
}
590
729
591
730
private function replaceDotsAndSpacesInKeys (string $ queryString ): string
@@ -615,7 +754,7 @@ private function revertDotAndSpaceReplacementsInKeys(array $queryStringArray): a
615
754
if (str_contains ($ key , self ::TEMP_DOT_REPLACEMENT ) || str_contains ($ key , self ::TEMP_SPACE_REPLACEMENT )) {
616
755
$ fixedKey = str_replace ([self ::TEMP_DOT_REPLACEMENT , self ::TEMP_SPACE_REPLACEMENT ], ['. ' , ' ' ], $ key );
617
756
618
- $ newQueryStringArray [$ fixedKey ] = $ value ;
757
+ $ newQueryStringArray [trim ( $ fixedKey) ] = $ value ;
619
758
} else {
620
759
$ newQueryStringArray [$ key ] = $ value ;
621
760
}
@@ -624,6 +763,14 @@ private function revertDotAndSpaceReplacementsInKeys(array $queryStringArray): a
624
763
return $ newQueryStringArray ;
625
764
}
626
765
766
+ private function revertDotAndSpaceReplacementsInString (string $ query ): string
767
+ {
768
+ return str_replace ([
769
+ urlencode (self ::TEMP_DOT_REPLACEMENT ),
770
+ urlencode (self ::TEMP_SPACE_REPLACEMENT ),
771
+ ], ['. ' , $ this ->spaceCharacter ()], $ query );
772
+ }
773
+
627
774
/**
628
775
* @throws Exception
629
776
*/
@@ -641,26 +788,59 @@ private function initArray(): void
641
788
private function array (): array
642
789
{
643
790
if ($ this ->array === null ) {
644
- if ($ this ->separator !== '& ' ) {
645
- throw new Exception (
646
- 'Converting a query string to array with custom separator isn \'t implemented, because PHP \'s ' .
647
- 'parse_str() function doesn \'t have that functionality. If you \'d need this reach out to crwlr ' .
648
- 'on github or twitter. '
649
- );
650
- }
791
+ if (empty ($ this ->string )) {
792
+ return [];
793
+ } else {
794
+ if ($ this ->separator !== '& ' ) {
795
+ throw new Exception (
796
+ 'Converting a query string to array with custom separator isn \'t implemented, because PHP \'s ' .
797
+ 'parse_str() function doesn \'t have that functionality. If you \'d need this, reach out to crwlr ' .
798
+ 'on github or twitter. '
799
+ );
800
+ }
651
801
652
- if ($ this ->containsDotOrSpaceInKey ()) {
653
- return $ this ->fixKeysContainingDotsOrSpaces ();
802
+ $ this ->array = $ this ->stringToArray ($ this ->string );
654
803
}
804
+ }
655
805
656
- parse_str ($ this ->string ?? '' , $ array );
806
+ return $ this ->array ;
807
+ }
657
808
658
- $ this ->array = $ array ;
809
+ /**
810
+ * @return mixed[]
811
+ * @throws Exception
812
+ */
813
+ private function stringToArray (string $ query ): array
814
+ {
815
+ $ query = str_replace ($ this ->spaceCharacter (), ' ' , $ query );
659
816
660
- return $ array ;
817
+ if ($ this ->containsDotOrSpaceInKey ($ query )) {
818
+ return $ this ->fixKeysContainingDotsOrSpaces ($ query );
661
819
}
662
820
663
- return $ this ->array ;
821
+ parse_str ($ query , $ array );
822
+
823
+ return $ array ;
824
+ }
825
+
826
+ /**
827
+ * @param mixed[] $query
828
+ * @return string
829
+ * @throws Exception
830
+ */
831
+ private function arrayToString (array $ query ): string
832
+ {
833
+ if (!$ this ->boolToInt ) {
834
+ $ query = $ this ->boolsToString ($ query );
835
+ }
836
+
837
+ if ($ this ->arrayContainsDotOrSpacesInKey ($ query )) {
838
+ $ query = $ this ->replaceDotsAndSpacesInArrayKeys ($ query );
839
+ }
840
+
841
+ $ string = http_build_query ($ query , '' , $ this ->separator , $ this ->spaceCharacterEncoding );
842
+
843
+ return $ this ->revertDotAndSpaceReplacementsInString ($ string );
664
844
}
665
845
666
846
/**
@@ -709,6 +889,7 @@ private function firstOrLast(?string $key = null, bool $first = true): mixed
709
889
/**
710
890
* @param string|mixed[] $query
711
891
* @return $this
892
+ * @throws Exception
712
893
*/
713
894
private function newWithSameSettings (string |array $ query ): self
714
895
{
0 commit comments