1919use SensitiveParameter ;
2020use Stringable ;
2121
22+ use function gettype ;
2223use function in_array ;
24+ use function is_scalar ;
2325use function preg_match ;
2426use function preg_replace_callback ;
2527use function rawurldecode ;
2628use function rawurlencode ;
29+ use function sprintf ;
2730use function strtoupper ;
2831
2932final class Encoder
@@ -100,94 +103,46 @@ public static function encodeQueryOrFragment(Stringable|string|null $component):
100103 public static function encodeQueryKeyValue (mixed $ component ): ?string
101104 {
102105 static $ pattern = '/[^ ' .self ::REGEXP_PART_UNRESERVED .']+| ' .self ::REGEXP_PART_ENCODED .'/ ' ;
103-
104- $ encodeMatches = static fn (array $ matches ): string => match (1 ) {
105- preg_match ('/[^ ' .self ::REGEXP_PART_UNRESERVED .']/ ' , rawurldecode ($ matches [0 ])) => rawurlencode ($ matches [0 ]),
106- default => $ matches [0 ],
107- };
108-
109- $ component = self ::filterComponent ($ component );
106+ $ encoder = static fn (array $ found ): string => 1 === preg_match ('/[^ ' .self ::REGEXP_PART_UNRESERVED .']/ ' , rawurldecode ($ found [0 ])) ? rawurlencode ($ found [0 ]) : $ found [0 ];
107+ $ filteredComponent = self ::filterComponent ($ component );
110108
111109 return match (true ) {
112- !is_scalar ($ component ) => throw new SyntaxError (sprintf ('A pair key/value must be a scalar value `%s` given. ' , gettype ($ component ))),
113- 1 === preg_match (self ::REGEXP_CHARS_INVALID , $ component ) => rawurlencode ($ component ),
114- 1 === preg_match ($ pattern , $ component ) => (string ) preg_replace_callback ($ pattern , $ encodeMatches (...), $ component ),
115- default => $ component ,
110+ null === $ filteredComponent => throw new SyntaxError (sprintf ('A pair key/value must be a scalar value `%s` given. ' , gettype ($ component ))),
111+ 1 === preg_match (self ::REGEXP_CHARS_INVALID , $ filteredComponent ) => rawurlencode ($ filteredComponent ),
112+ default => (string ) preg_replace_callback ($ pattern , $ encoder , $ filteredComponent ),
116113 };
117114 }
118115
119- /**
120- * Decodes the URI component without decoding the unreserved characters which are already encoded.
121- */
122- public static function decodePartial (Stringable |string |int |null $ component ): ?string
123- {
124- $ decodeMatches = static fn (array $ matches ): string => match (1 ) {
125- preg_match (self ::REGEXP_CHARS_PREVENTS_DECODING , $ matches [0 ]) => strtoupper ($ matches [0 ]),
126- default => rawurldecode ($ matches [0 ]),
127- };
128-
129- return self ::decode ($ component , $ decodeMatches );
130- }
131-
132116 /**
133117 * Decodes all the URI component characters.
134118 */
135- public static function decodeAll (Stringable |string |int | null $ component ): ?string
119+ public static function decodeAll (Stringable |string |null $ component ): ?string
136120 {
137121 return self ::decode ($ component , static fn (array $ matches ): string => rawurldecode ($ matches [0 ]));
138122 }
139123
140- private static function filterComponent (mixed $ component ): ?string
124+ /**
125+ * Decodes the URI component without decoding the unreserved characters which are already encoded.
126+ */
127+ public static function decodePartial (Stringable |string |int |null $ component ): ?string
141128 {
142- return match (true ) {
143- true === $ component => '1 ' ,
144- false === $ component => '0 ' ,
145- $ component instanceof UriComponentInterface => $ component ->value (),
146- $ component instanceof Stringable,
147- is_scalar ($ component ) => (string ) $ component ,
148- null === $ component => null ,
149- default => throw new SyntaxError (sprintf ('The component must be a scalar value `%s` given. ' , gettype ($ component ))),
150- };
151- }
129+ $ decoder = static function (array $ matches ): string {
130+ if (1 === preg_match (self ::REGEXP_CHARS_PREVENTS_DECODING , $ matches [0 ])) {
131+ return strtoupper ($ matches [0 ]);
132+ }
152133
153- private static function encode (Stringable |string |int |bool |null $ component , string $ pattern ): ?string
154- {
155- $ component = self ::filterComponent ($ component );
156- $ encodeMatches = static fn (array $ matches ): string => match (1 ) {
157- preg_match ('/[^ ' .self ::REGEXP_PART_UNRESERVED .']/ ' , rawurldecode ($ matches [0 ])) => rawurlencode ($ matches [0 ]),
158- default => $ matches [0 ],
134+ return rawurldecode ($ matches [0 ]);
159135 };
160136
161- return match (true ) {
162- null === $ component ,
163- '' === $ component => $ component ,
164- default => (string ) preg_replace_callback ($ pattern , $ encodeMatches (...), $ component ),
165- };
137+ return self ::decode ($ component , $ decoder );
166138 }
167139
168140 /**
169- * Decodes all the URI component characters.
141+ * Decodes the component unreserved characters.
170142 */
171- private static function decode (Stringable |string |int |null $ component , Closure $ decodeMatches ): ?string
172- {
173- $ component = self ::filterComponent ($ component );
174- if (null === $ component || '' === $ component ) {
175- return $ component ;
176- }
177-
178- if (1 === preg_match (self ::REGEXP_CHARS_INVALID , $ component )) {
179- throw new SyntaxError ('Invalid component string: ' .$ component .'. ' );
180- }
181-
182- if (1 === preg_match (self ::REGEXP_CHARS_ENCODED , $ component )) {
183- return (string ) preg_replace_callback (self ::REGEXP_CHARS_ENCODED , $ decodeMatches (...), $ component );
184- }
185-
186- return $ component ;
187- }
188-
189- public static function decodeUnreservedCharacters (?string $ str ): ?string
143+ public static function decodeUnreservedCharacters (Stringable |string |null $ str ): ?string
190144 {
145+ $ str = self ::filterComponent ($ str );
191146 if (null === $ str || '' === $ str ) {
192147 return $ str ;
193148 }
@@ -196,38 +151,89 @@ public static function decodeUnreservedCharacters(?string $str): ?string
196151 }
197152
198153 /**
199- * Decodes the URI query path while preserving delim character that should not be decoded for a path inside a URI.
154+ * Decodes the path component while preserving characters that should not be decoded in the context of a full valid URI.
200155 */
201156 public static function decodePath (Stringable |string |null $ path ): ?string
202157 {
203- $ decodeMatches = static function (array $ matches ): string {
158+ $ decoder = static function (array $ matches ): string {
204159 $ encodedChar = strtoupper ($ matches [0 ]);
205160
206161 return in_array ($ encodedChar , ['%2F ' , '%20 ' , '%3F ' , '%23 ' ], true ) ? $ encodedChar : rawurldecode ($ encodedChar );
207162 };
208163
209- return self ::decode ($ path , $ decodeMatches );
164+ return self ::decode ($ path , $ decoder );
210165 }
211166
212167 /**
213- * Decodes the URI query string while preserving delim character that should not be decoded for a query inside a URI.
168+ * Decodes the query component while preserving characters that should not be decoded in the context of a full valid URI.
214169 */
215170 public static function decodeQuery (Stringable |string |null $ path ): ?string
216171 {
217- $ decodeMatches = static function (array $ matches ): string {
172+ $ decoder = static function (array $ matches ): string {
218173 $ encodedChar = strtoupper ($ matches [0 ]);
219174
220175 return in_array ($ encodedChar , ['%26 ' , '%3D ' , '%20 ' , '%23 ' ], true ) ? $ encodedChar : rawurldecode ($ encodedChar );
221176 };
222177
223- return self ::decode ($ path , $ decodeMatches );
178+ return self ::decode ($ path , $ decoder );
224179 }
225180
226181 /**
227- * Decodes the URI query string while preserving delim character that should not be decoded for a query inside a URI.
182+ * Decodes the fragment component while preserving characters that should not be decoded in the context of a full valid URI.
228183 */
229184 public static function decodeFragment (Stringable |string |null $ path ): ?string
230185 {
231186 return self ::decode ($ path , static fn (array $ matches ): string => '%20 ' === $ matches [0 ] ? $ matches [0 ] : rawurldecode ($ matches [0 ]));
232187 }
188+
189+ private static function filterComponent (mixed $ component ): ?string
190+ {
191+ return match (true ) {
192+ true === $ component => '1 ' ,
193+ false === $ component => '0 ' ,
194+ $ component instanceof UriComponentInterface => $ component ->value (),
195+ $ component instanceof Stringable,
196+ is_scalar ($ component ) => (string ) $ component ,
197+ null === $ component => null ,
198+ default => throw new SyntaxError (sprintf ('The component must be a scalar value `%s` given. ' , gettype ($ component ))),
199+ };
200+ }
201+
202+ /**
203+ * Encodes the URI component characters using a regular expression to find which characters need encoding.
204+ */
205+ private static function encode (Stringable |string |int |bool |null $ component , string $ pattern ): ?string
206+ {
207+ $ component = self ::filterComponent ($ component );
208+ if (null === $ component || '' === $ component ) {
209+ return $ component ;
210+ }
211+
212+ return (string ) preg_replace_callback (
213+ $ pattern ,
214+ static fn (array $ found ): string => 1 === preg_match ('/[^ ' .self ::REGEXP_PART_UNRESERVED .']/ ' , rawurldecode ($ found [0 ])) ? rawurlencode ($ found [0 ]) : $ found [0 ],
215+ $ component
216+ );
217+ }
218+
219+ /**
220+ * Decodes the URI component characters using a closure.
221+ */
222+ private static function decode (Stringable |string |int |null $ component , Closure $ decoder ): ?string
223+ {
224+ $ component = self ::filterComponent ($ component );
225+ if (null === $ component || '' === $ component ) {
226+ return $ component ;
227+ }
228+
229+ if (1 === preg_match (self ::REGEXP_CHARS_INVALID , $ component )) {
230+ throw new SyntaxError ('Invalid component string: ' .$ component .'. ' );
231+ }
232+
233+ if (1 === preg_match (self ::REGEXP_CHARS_ENCODED , $ component )) {
234+ return (string ) preg_replace_callback (self ::REGEXP_CHARS_ENCODED , $ decoder , $ component );
235+ }
236+
237+ return $ component ;
238+ }
233239}
0 commit comments