9
9
use Psr \Http \Message \ResponseInterface ;
10
10
use Psr \Http \Message \ServerRequestInterface ;
11
11
use RuntimeException ;
12
- use Tobyz \JsonApiServer \Endpoint \Endpoint ;
12
+ use Tobyz \JsonApiServer \Exception \ErrorProvider ;
13
+ use Tobyz \JsonApiServer \Exception \Field \InvalidFieldValueException ;
14
+ use Tobyz \JsonApiServer \Exception \JsonApiErrorsException ;
13
15
use Tobyz \JsonApiServer \Exception \NotAcceptableException ;
16
+ use Tobyz \JsonApiServer \Exception \Request \InvalidQueryParameterException ;
14
17
use Tobyz \JsonApiServer \Exception \Request \InvalidSparseFieldsetsException ;
15
- use Tobyz \JsonApiServer \Resource \Collection ;
16
18
use Tobyz \JsonApiServer \Resource \Resource ;
17
19
use Tobyz \JsonApiServer \Schema \Field \Field ;
20
+ use Tobyz \JsonApiServer \Schema \Parameter ;
18
21
use WeakMap ;
19
22
20
- class Context
23
+ class Context extends SchemaContext
21
24
{
22
- public ?Collection $ collection = null ;
23
- public ?Resource $ resource = null ;
24
- public ?Endpoint $ endpoint = null ;
25
25
public ?object $ query = null ;
26
26
public ?Serializer $ serializer = null ;
27
27
public ?object $ model = null ;
28
28
public ?array $ data = null ;
29
- public ?Field $ field = null ;
30
29
public ?array $ include = null ;
31
30
public ArrayObject $ documentMeta ;
32
31
public ArrayObject $ documentLinks ;
@@ -35,23 +34,24 @@ class Context
35
34
36
35
private ?array $ body ;
37
36
private ?string $ path ;
37
+ private ?array $ pathSegments = null ;
38
38
private ?array $ requestedExtensions = null ;
39
39
private ?array $ requestedProfiles = null ;
40
+ private array $ parameters = [];
41
+ private ?array $ queryParameterMap = null ;
40
42
41
- private WeakMap $ endpoints ;
42
43
private WeakMap $ resourceIds ;
43
44
private WeakMap $ modelIds ;
44
- private WeakMap $ fields ;
45
45
private WeakMap $ sparseFields ;
46
46
47
- public function __construct (public JsonApi $ api , public ServerRequestInterface $ request )
47
+ public function __construct (JsonApi $ api , public ServerRequestInterface $ request )
48
48
{
49
+ parent ::__construct ($ api );
50
+
49
51
$ this ->parseAcceptHeader ();
50
52
51
- $ this ->endpoints = new WeakMap ();
52
53
$ this ->resourceIds = new WeakMap ();
53
54
$ this ->modelIds = new WeakMap ();
54
- $ this ->fields = new WeakMap ();
55
55
$ this ->sparseFields = new WeakMap ();
56
56
57
57
$ this ->documentMeta = new ArrayObject ();
@@ -61,14 +61,6 @@ public function __construct(public JsonApi $api, public ServerRequestInterface $
61
61
$ this ->resourceMeta = new WeakMap ();
62
62
}
63
63
64
- /**
65
- * Get the value of a query param.
66
- */
67
- public function queryParam (string $ name , $ default = null )
68
- {
69
- return $ this ->request ->getQueryParams ()[$ name ] ?? $ default ;
70
- }
71
-
72
64
/**
73
65
* Get the request method.
74
66
*/
@@ -88,6 +80,21 @@ public function path(): string
88
80
);
89
81
}
90
82
83
+ public function pathSegments (): array
84
+ {
85
+ return $ this ->pathSegments ??= array_values (
86
+ array_filter (explode ('/ ' , trim ($ this ->path (), '/ ' ))),
87
+ );
88
+ }
89
+
90
+ public function withPathSegments (array $ segments ): static
91
+ {
92
+ $ new = clone $ this ;
93
+ $ new ->pathSegments = array_values ($ segments );
94
+
95
+ return $ new ;
96
+ }
97
+
91
98
/**
92
99
* Get the URL of the current request, optionally with query parameter overrides.
93
100
*/
@@ -121,19 +128,6 @@ public function body(): ?array
121
128
json_decode ($ this ->request ->getBody ()->getContents (), true );
122
129
}
123
130
124
- /**
125
- * Get a resource by its type.
126
- */
127
- public function resource (string $ type ): Resource
128
- {
129
- return $ this ->api ->getResource ($ type );
130
- }
131
-
132
- public function endpoints (Collection $ collection ): array
133
- {
134
- return $ this ->endpoints [$ collection ] ??= $ collection ->endpoints ();
135
- }
136
-
137
131
public function id (Resource $ resource , $ model ): string
138
132
{
139
133
if (isset ($ this ->modelIds [$ model ])) {
@@ -145,26 +139,6 @@ public function id(Resource $resource, $model): string
145
139
return $ this ->modelIds [$ model ] = $ id ->serializeValue ($ id ->getValue ($ this ), $ this );
146
140
}
147
141
148
- /**
149
- * Get the fields for the given resource, keyed by name.
150
- *
151
- * @return array<string, Field>
152
- */
153
- public function fields (Resource $ resource ): array
154
- {
155
- if (isset ($ this ->fields [$ resource ])) {
156
- return $ this ->fields [$ resource ];
157
- }
158
-
159
- $ fields = [];
160
-
161
- foreach ($ resource ->fields () as $ field ) {
162
- $ fields [$ field ->name ] = $ field ;
163
- }
164
-
165
- return $ this ->fields [$ resource ] = $ fields ;
166
- }
167
-
168
142
/**
169
143
* Get only the requested fields for the given resource, keyed by name.
170
144
*
@@ -178,7 +152,7 @@ public function sparseFields(Resource $resource): array
178
152
179
153
$ fields = $ this ->fields ($ resource );
180
154
$ type = $ resource ->type ();
181
- $ fieldsParam = $ this ->queryParam ('fields ' );
155
+ $ fieldsParam = $ this ->parameter ('fields ' );
182
156
183
157
if (is_array ($ fieldsParam ) && array_key_exists ($ type , $ fieldsParam )) {
184
158
$ requested = $ fieldsParam [$ type ];
@@ -210,7 +184,7 @@ public function fieldRequested(string $type, string $field): bool
210
184
*/
211
185
public function sortRequested (string $ field ): bool
212
186
{
213
- if ($ sort = $ this ->queryParam ('sort ' )) {
187
+ if ($ sort = $ this ->parameter ('sort ' )) {
214
188
foreach (parse_sort_string ($ sort ) as [$ name , $ direction ]) {
215
189
if ($ name === $ field ) {
216
190
return true ;
@@ -305,8 +279,10 @@ public function withRequest(ServerRequestInterface $request): static
305
279
$ new ->sparseFields = new WeakMap ();
306
280
$ new ->body = null ;
307
281
$ new ->path = null ;
282
+ $ new ->pathSegments = null ;
308
283
$ new ->requestedProfiles = null ;
309
284
$ new ->requestedExtensions = null ;
285
+ $ new ->queryParameterMap = null ;
310
286
$ new ->parseAcceptHeader ();
311
287
return $ new ;
312
288
}
@@ -325,27 +301,6 @@ public function withData(?array $data): static
325
301
return $ new ;
326
302
}
327
303
328
- public function withCollection (?Collection $ collection ): static
329
- {
330
- $ new = clone $ this ;
331
- $ new ->collection = $ collection ;
332
- return $ new ;
333
- }
334
-
335
- public function withResource (?Resource $ resource ): static
336
- {
337
- $ new = clone $ this ;
338
- $ new ->resource = $ resource ;
339
- return $ new ;
340
- }
341
-
342
- public function withEndpoint (?Endpoint $ endpoint ): static
343
- {
344
- $ new = clone $ this ;
345
- $ new ->endpoint = $ endpoint ;
346
- return $ new ;
347
- }
348
-
349
304
public function withQuery (?object $ query ): static
350
305
{
351
306
$ new = clone $ this ;
@@ -367,13 +322,6 @@ public function withModel(?object $model): static
367
322
return $ new ;
368
323
}
369
324
370
- public function withField (?Field $ field ): static
371
- {
372
- $ new = clone $ this ;
373
- $ new ->field = $ field ;
374
- return $ new ;
375
- }
376
-
377
325
public function withInclude (?array $ include ): static
378
326
{
379
327
$ new = clone $ this ;
@@ -395,6 +343,130 @@ public function activateProfile(string $uri): static
395
343
return $ this ;
396
344
}
397
345
346
+ /**
347
+ * Load and validate parameters from the request.
348
+ *
349
+ * @param Parameter[] $parameters
350
+ */
351
+ public function withParameters (array $ parameters ): static
352
+ {
353
+ $ context = clone $ this ;
354
+ $ context ->parameters = [];
355
+
356
+ $ this ->validateQueryParameters (
357
+ array_filter ($ parameters , fn (Parameter $ p ) => $ p ->in === 'query ' ),
358
+ );
359
+
360
+ $ errors = [];
361
+
362
+ foreach ($ parameters as $ parameter ) {
363
+ $ value = $ this ->extractParameterValue ($ parameter );
364
+
365
+ if ($ value === null && $ parameter ->default ) {
366
+ $ value = ($ parameter ->default )();
367
+ }
368
+
369
+ $ value = $ parameter ->deserializeValue ($ value , $ context );
370
+
371
+ if ($ value === null && !$ parameter ->required ) {
372
+ continue ;
373
+ }
374
+
375
+ $ fail = function ($ error = []) use (&$ errors , $ parameter ) {
376
+ if (!$ error instanceof ErrorProvider) {
377
+ $ error = new InvalidFieldValueException (
378
+ is_scalar ($ error ) ? ['detail ' => (string ) $ error ] : $ error ,
379
+ );
380
+ }
381
+
382
+ $ errors [] = $ error ->source (['parameter ' => $ parameter ->name ]);
383
+ };
384
+
385
+ $ parameter ->validateValue ($ value , $ fail , $ context );
386
+
387
+ $ context ->parameters [$ parameter ->name ] = $ value ;
388
+ }
389
+
390
+ if ($ errors ) {
391
+ throw new JsonApiErrorsException ($ errors );
392
+ }
393
+
394
+ return $ context ;
395
+ }
396
+
397
+ /**
398
+ * Get a validated parameter value.
399
+ */
400
+ public function parameter (string $ name ): mixed
401
+ {
402
+ return $ this ->parameters [$ name ] ?? null ;
403
+ }
404
+
405
+ private function validateQueryParameters (array $ parameters ): void
406
+ {
407
+ foreach ($ this ->request ->getQueryParams () as $ key => $ value ) {
408
+ if (!ctype_lower ($ key )) {
409
+ continue ;
410
+ }
411
+
412
+ foreach ($ this ->flattenQueryParameters ([$ key => $ value ]) as $ flattenedKey => $ v ) {
413
+ $ matched = false ;
414
+
415
+ foreach ($ parameters as $ parameter ) {
416
+ if (
417
+ $ flattenedKey === $ parameter ->name ||
418
+ str_starts_with ($ flattenedKey , $ parameter ->name . '[ ' )
419
+ ) {
420
+ $ matched = true ;
421
+ }
422
+ }
423
+
424
+ if (!$ matched ) {
425
+ throw new InvalidQueryParameterException ($ flattenedKey );
426
+ }
427
+ }
428
+ }
429
+ }
430
+
431
+ private function flattenQueryParameters (array $ params , string $ prefix = '' ): array
432
+ {
433
+ $ result = [];
434
+
435
+ foreach ($ params as $ key => $ value ) {
436
+ $ newKey = $ prefix ? "{$ prefix }[ {$ key }] " : $ key ;
437
+
438
+ if (is_array ($ value )) {
439
+ $ result += $ this ->flattenQueryParameters ($ value , $ newKey );
440
+ } else {
441
+ $ result [$ newKey ] = $ value ;
442
+ }
443
+ }
444
+
445
+ return $ result ;
446
+ }
447
+
448
+ private function extractParameterValue (Parameter $ param ): mixed
449
+ {
450
+ return match ($ param ->in ) {
451
+ 'query ' => $ this ->getNestedQueryParam ($ param ->name ),
452
+ 'header ' => $ this ->request ->getHeaderLine ($ param ->name ) ?: null ,
453
+ default => null ,
454
+ };
455
+ }
456
+
457
+ private function getNestedQueryParam (string $ name ): mixed
458
+ {
459
+ $ value = $ this ->request ->getQueryParams ();
460
+
461
+ preg_match_all ('/[^\[\]]+/ ' , $ name , $ matches );
462
+
463
+ foreach ($ matches [0 ] ?? [] as $ segment ) {
464
+ $ value = $ value [$ segment ] ?? null ;
465
+ }
466
+
467
+ return $ value ;
468
+ }
469
+
398
470
public function forModel (array $ collections , ?object $ model ): static
399
471
{
400
472
$ new = clone $ this ;
0 commit comments