24
24
use ReflectionClass ;
25
25
use ReflectionObject ;
26
26
use Stringable ;
27
- use Traversable ;
28
27
use UnitEnum ;
29
28
30
29
/**
@@ -194,7 +193,45 @@ public static function filter(array $values, bool $discardInvalid = true): array
194
193
}
195
194
196
195
/**
197
- * Convert nested arrays to a query string
196
+ * Convert nested arrays and objects to a query string
197
+ *
198
+ * @see Get::formData() for details.
199
+ *
200
+ * @template T of object|mixed[]|string|null
201
+ *
202
+ * @param mixed[] $data
203
+ * @param int-mask-of<QueryFlag::*> $flags
204
+ * @param (callable(object): (T|false))|null $callback
205
+ */
206
+ public static function query (
207
+ array $ data ,
208
+ int $ flags = QueryFlag::PRESERVE_NUMERIC_KEYS | QueryFlag::PRESERVE_STRING_KEYS ,
209
+ ?DateFormatterInterface $ dateFormatter = null ,
210
+ ?callable $ callback = null
211
+ ): string {
212
+ $ data = self ::doFormData (
213
+ $ data ,
214
+ $ flags ,
215
+ $ dateFormatter ?? new DateFormatter (),
216
+ $ callback ,
217
+ );
218
+
219
+ foreach ($ data as [$ key , $ value ]) {
220
+ if (!is_string ($ value )) {
221
+ throw new InvalidArgumentException (sprintf (
222
+ "Invalid value at '%s': %s " ,
223
+ $ key ,
224
+ self ::type ($ value ),
225
+ ));
226
+ }
227
+ $ query [] = rawurlencode ($ key ) . '= ' . rawurlencode ($ value );
228
+ }
229
+
230
+ return implode ('& ' , $ query ?? []);
231
+ }
232
+
233
+ /**
234
+ * Convert nested arrays and objects to HTML form data
198
235
*
199
236
* List keys are not preserved by default. Use `$flags` to modify this
200
237
* behaviour.
@@ -203,107 +240,120 @@ public static function filter(array $values, bool $discardInvalid = true): array
203
240
* convert {@see DateTimeInterface} instances to ISO-8601 strings.
204
241
*
205
242
* `$callback` is applied to objects other than {@see DateTimeInterface}
206
- * instances found in `$data`. If it returns `null`, the value is excluded
207
- * from the query string.
243
+ * instances found in `$data`. It may return `null` to skip the value, or
244
+ * `false` to process the value as if no callback had been given. If a
245
+ * {@see DateTimeInterface} is returned, it is converted to `string` as per
246
+ * the `$dateFormatter` note above.
208
247
*
209
- * Objects are resolved as follows:
248
+ * If no `$callback` is given, objects are resolved as follows:
210
249
*
211
250
* - {@see DateTimeInterface}: converted to `string` (see `$dateFormatter`
212
251
* note above)
213
252
* - {@see Arrayable}: replaced with {@see Arrayable::toArray()}
214
- * - {@see Jsonable}: replaced with {@see Jsonable::toJson()} and decoded
215
- * - {@see Stringable}: cast to `string` unless {@see JsonSerializable} is
216
- * also implemented
217
- * - Others: converted to JSON and decoded
253
+ * - {@see JsonSerializable}: replaced with
254
+ * {@see JsonSerializable::jsonSerialize()} if it returns an `array`
255
+ * - {@see Jsonable}: replaced with {@see Jsonable::toJson()} after decoding
256
+ * if {@see json_decode()} returns an `array`
257
+ * - `object` with at least one public property: replaced with an array that
258
+ * maps public property names to values
259
+ * - {@see Stringable}: cast to `string`
218
260
*
219
- * @param mixed[] $data
261
+ * @template T of object|mixed[]|string|null
262
+ *
263
+ * @param mixed[]|object $data
220
264
* @param int-mask-of<QueryFlag::*> $flags
221
- * @param (callable(object): (object|mixed[]|string|null))|null $callback
265
+ * @param (callable(object): (T|false))|null $callback
266
+ * @return list<array{string,string|(T&object)}>
222
267
*/
223
- public static function query (
224
- array $ data ,
268
+ public static function formData (
269
+ $ data ,
225
270
int $ flags = QueryFlag::PRESERVE_NUMERIC_KEYS | QueryFlag::PRESERVE_STRING_KEYS ,
226
271
?DateFormatterInterface $ dateFormatter = null ,
227
272
?callable $ callback = null
228
- ): string {
229
- return implode ('& ' , self ::doQuery (
273
+ ) {
274
+ /** @var list<array{string,string|(T&object)}> */
275
+ return self ::doFormData (
230
276
$ data ,
231
277
$ flags ,
232
- $ dateFormatter ?: new DateFormatter (),
278
+ $ dateFormatter ?? new DateFormatter (),
233
279
$ callback ,
234
- )) ;
280
+ );
235
281
}
236
282
237
283
/**
238
- * @param mixed[] $data
284
+ * @template T of object|mixed[]|string|null
285
+ *
286
+ * @param object|mixed[]|string|null $data
239
287
* @param int-mask-of<QueryFlag::*> $flags
240
- * @param (callable(object): (object|mixed[]|string|null ))|null $cb
241
- * @param string[] $query
242
- * @return string[]
288
+ * @param (callable(object): (T|false ))|null $cb
289
+ * @param list<array{ string,string|(T&object)}> $query
290
+ * @return list<array{ string,string|(T&object)}>
243
291
*/
244
- private static function doQuery (
245
- array $ data ,
292
+ private static function doFormData (
293
+ $ data ,
246
294
int $ flags ,
247
295
DateFormatterInterface $ df ,
248
296
?callable $ cb ,
249
297
array &$ query = [],
250
- string $ keyPrefix = '' ,
251
- string $ keyFormat = '%s '
298
+ string $ name = ''
252
299
): array {
253
- foreach ($ data as $ key => $ value ) {
254
- $ key = $ keyPrefix . sprintf ($ keyFormat , $ key );
300
+ if ($ name === '' ) {
301
+ $ data = self ::getFormDataValue ($ data , $ df , $ cb );
302
+ }
255
303
256
- if ($ keyPrefix === '' ) {
257
- $ value = self ::getQueryValue ($ value , $ df , $ cb , $ key );
258
- }
304
+ /** @var (T&object)|mixed[]|string|null $data */
305
+ if ($ data === null || $ data === []) {
306
+ return $ query ;
307
+ }
259
308
260
- /** @var mixed[]|string|null $value */
261
- if ($ value === null || $ value === []) {
262
- continue ;
263
- }
309
+ if (is_array ($ data )) {
310
+ $ preserveKeys = $ name === '' || (
311
+ Arr::isList ($ data )
312
+ ? $ flags & QueryFlag::PRESERVE_LIST_KEYS
313
+ : (Arr::isIndexed ($ data )
314
+ ? $ flags & QueryFlag::PRESERVE_NUMERIC_KEYS
315
+ : $ flags & QueryFlag::PRESERVE_STRING_KEYS )
316
+ );
317
+ $ format = $ preserveKeys ? ($ name === '' ? '%s ' : '[%s] ' ) : '[] ' ;
264
318
265
- if (is_array ($ value )) {
266
- $ preserveKeys =
267
- Arr::isList ($ value )
268
- ? $ flags & QueryFlag::PRESERVE_LIST_KEYS
269
- : (Arr::isIndexed ($ value )
270
- ? $ flags & QueryFlag::PRESERVE_NUMERIC_KEYS
271
- : $ flags & QueryFlag::PRESERVE_STRING_KEYS );
272
- $ format = $ preserveKeys ? '[%s] ' : '[] ' ;
273
-
274
- $ hasArray = false ;
275
- foreach ($ value as $ k => &$ v ) {
276
- $ k = $ key . sprintf ($ format , $ k );
277
- $ v = self ::getQueryValue ($ v , $ df , $ cb , $ k );
278
- if (!$ preserveKeys && !$ hasArray && is_array ($ v )) {
279
- $ hasArray = true ;
280
- }
319
+ $ hasArray = false ;
320
+ foreach ($ data as &$ value ) {
321
+ $ value = self ::getFormDataValue ($ value , $ df , $ cb );
322
+ if (!$ preserveKeys && !$ hasArray && is_array ($ value )) {
323
+ $ hasArray = true ;
281
324
}
325
+ }
326
+ unset($ value );
282
327
283
- if ($ hasArray ) {
284
- $ format = '[%s] ' ;
285
- }
328
+ if ($ hasArray ) {
329
+ $ format = '[%s] ' ;
330
+ }
286
331
287
- self ::doQuery ($ value , $ flags , $ df , $ cb , $ query , $ key , $ format );
288
- continue ;
332
+ /** @var object|mixed[]|string|null $value */
333
+ foreach ($ data as $ key => $ value ) {
334
+ $ key = sprintf ($ format , $ key );
335
+ self ::doFormData ($ value , $ flags , $ df , $ cb , $ query , $ name . $ key );
289
336
}
290
337
291
- $ query [] = rawurlencode ( $ key ) . ' = ' . rawurlencode ( $ value ) ;
338
+ return $ query ;
292
339
}
293
340
341
+ $ query [] = [$ name , $ data ];
342
+
294
343
return $ query ;
295
344
}
296
345
297
346
/**
347
+ * @template T of object|mixed[]|string|null
348
+ *
298
349
* @param mixed $value
299
- * @param (callable(object): (object|mixed[]|string|null ))|null $cb
300
- * @return mixed[]|string|null
350
+ * @param (callable(object): (T|false ))|null $cb
351
+ * @return mixed[]|string|T
301
352
*/
302
- private static function getQueryValue (
353
+ private static function getFormDataValue (
303
354
$ value ,
304
355
DateFormatterInterface $ df ,
305
- ?callable $ cb ,
306
- string $ key
356
+ ?callable $ cb
307
357
) {
308
358
if (is_bool ($ value )) {
309
359
return (string ) (int ) $ value ;
@@ -317,41 +367,54 @@ private static function getQueryValue(
317
367
return $ value ;
318
368
}
319
369
370
+ if ($ value instanceof DateTimeInterface) {
371
+ return $ df ->format ($ value );
372
+ }
373
+
320
374
if (is_object ($ value )) {
321
- if ($ value instanceof DateTimeInterface) {
322
- return $ df ->format ($ value );
323
- }
324
375
if ($ cb !== null ) {
325
- $ value = $ cb ($ value );
326
- if (! is_object ( $ value ) ) {
327
- return $ value ;
376
+ $ result = $ cb ($ value );
377
+ if ($ result instanceof DateTimeInterface ) {
378
+ return $ df -> format ( $ result ) ;
328
379
}
329
- if ($ value instanceof DateTimeInterface ) {
330
- return $ df -> format ( $ value ) ;
380
+ if ($ result !== false ) {
381
+ return $ result ;
331
382
}
332
383
}
384
+
333
385
if ($ value instanceof Arrayable) {
334
386
return $ value ->toArray ();
335
387
}
336
- if ($ value instanceof Jsonable) {
337
- return self ::getQueryValue (
338
- Json::parseObjectAsArray ($ value ->toJson ()), $ df , $ cb , $ key
339
- );
388
+
389
+ if ((
390
+ $ value instanceof JsonSerializable &&
391
+ is_array ($ result = $ value ->jsonSerialize ())
392
+ ) || (
393
+ $ value instanceof Jsonable &&
394
+ is_array ($ result = Json::parseObjectAsArray ($ value ->toJson ()))
395
+ )) {
396
+ return $ result ;
397
+ }
398
+
399
+ // Get public property values
400
+ $ result = [];
401
+ // @phpstan-ignore-next-line
402
+ foreach ($ value as $ key => $ val ) {
403
+ $ result [$ key ] = $ val ;
340
404
}
341
- if (
342
- ($ value instanceof Stringable || method_exists ($ value , '__toString ' )) &&
343
- !($ value instanceof JsonSerializable)
344
- ) {
405
+ if ($ result !== []) {
406
+ return $ result ;
407
+ }
408
+
409
+ if (Test::isStringable ($ value )) {
345
410
return (string ) $ value ;
346
411
}
347
- return self ::getQueryValue (
348
- Json::parseObjectAsArray (Json::stringify ($ value )), $ df , $ cb , $ key
349
- );
412
+
413
+ return [];
350
414
}
351
415
352
416
throw new InvalidArgumentException (sprintf (
353
- 'Invalid value at %s: %s ' ,
354
- $ key ,
417
+ 'Invalid value: %s ' ,
355
418
self ::type ($ value ),
356
419
));
357
420
}
@@ -385,21 +448,21 @@ public static function coalesce(...$values)
385
448
* @param Arrayable<TKey,TValue>|iterable<TKey,TValue> $value
386
449
* @return array<TKey,TValue>
387
450
*/
388
- public static function array ($ value, bool $ preserveKeys = false ): array
451
+ public static function array ($ value ): array
389
452
{
390
453
if (is_array ($ value )) {
391
454
return $ value ;
392
455
}
393
456
if ($ value instanceof Arrayable) {
394
457
return $ value ->toArray ();
395
458
}
396
- return iterator_to_array ($ value, $ preserveKeys );
459
+ return iterator_to_array ($ value );
397
460
}
398
461
399
462
/**
400
463
* Resolve a value to an item count
401
464
*
402
- * @param Traversable <array-key,mixed>|Arrayable <array-key,mixed>|Countable|array<array-key,mixed> |int $value
465
+ * @param Arrayable <array-key,mixed>|iterable <array-key,mixed>|Countable|int $value
403
466
*/
404
467
public static function count ($ value ): int
405
468
{
0 commit comments