99use Psr \Http \Message \ResponseInterface ;
1010use Psr \Http \Message \ServerRequestInterface ;
1111use 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 ;
1315use Tobyz \JsonApiServer \Exception \NotAcceptableException ;
16+ use Tobyz \JsonApiServer \Exception \Request \InvalidQueryParameterException ;
1417use Tobyz \JsonApiServer \Exception \Request \InvalidSparseFieldsetsException ;
15- use Tobyz \JsonApiServer \Resource \Collection ;
1618use Tobyz \JsonApiServer \Resource \Resource ;
1719use Tobyz \JsonApiServer \Schema \Field \Field ;
20+ use Tobyz \JsonApiServer \Schema \Parameter ;
1821use WeakMap ;
1922
20- class Context
23+ class Context extends SchemaContext
2124{
22- public ?Collection $ collection = null ;
23- public ?Resource $ resource = null ;
24- public ?Endpoint $ endpoint = null ;
2525 public ?object $ query = null ;
2626 public ?Serializer $ serializer = null ;
2727 public ?object $ model = null ;
2828 public ?array $ data = null ;
29- public ?Field $ field = null ;
3029 public ?array $ include = null ;
3130 public ArrayObject $ documentMeta ;
3231 public ArrayObject $ documentLinks ;
@@ -35,23 +34,24 @@ class Context
3534
3635 private ?array $ body ;
3736 private ?string $ path ;
37+ private ?array $ pathSegments = null ;
3838 private ?array $ requestedExtensions = null ;
3939 private ?array $ requestedProfiles = null ;
40+ private array $ parameters = [];
41+ private ?array $ queryParameterMap = null ;
4042
41- private WeakMap $ endpoints ;
4243 private WeakMap $ resourceIds ;
4344 private WeakMap $ modelIds ;
44- private WeakMap $ fields ;
4545 private WeakMap $ sparseFields ;
4646
47- public function __construct (public JsonApi $ api , public ServerRequestInterface $ request )
47+ public function __construct (JsonApi $ api , public ServerRequestInterface $ request )
4848 {
49+ parent ::__construct ($ api );
50+
4951 $ this ->parseAcceptHeader ();
5052
51- $ this ->endpoints = new WeakMap ();
5253 $ this ->resourceIds = new WeakMap ();
5354 $ this ->modelIds = new WeakMap ();
54- $ this ->fields = new WeakMap ();
5555 $ this ->sparseFields = new WeakMap ();
5656
5757 $ this ->documentMeta = new ArrayObject ();
@@ -61,14 +61,6 @@ public function __construct(public JsonApi $api, public ServerRequestInterface $
6161 $ this ->resourceMeta = new WeakMap ();
6262 }
6363
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-
7264 /**
7365 * Get the request method.
7466 */
@@ -88,6 +80,21 @@ public function path(): string
8880 );
8981 }
9082
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+
9198 /**
9299 * Get the URL of the current request, optionally with query parameter overrides.
93100 */
@@ -121,19 +128,6 @@ public function body(): ?array
121128 json_decode ($ this ->request ->getBody ()->getContents (), true );
122129 }
123130
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-
137131 public function id (Resource $ resource , $ model ): string
138132 {
139133 if (isset ($ this ->modelIds [$ model ])) {
@@ -145,26 +139,6 @@ public function id(Resource $resource, $model): string
145139 return $ this ->modelIds [$ model ] = $ id ->serializeValue ($ id ->getValue ($ this ), $ this );
146140 }
147141
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-
168142 /**
169143 * Get only the requested fields for the given resource, keyed by name.
170144 *
@@ -178,7 +152,7 @@ public function sparseFields(Resource $resource): array
178152
179153 $ fields = $ this ->fields ($ resource );
180154 $ type = $ resource ->type ();
181- $ fieldsParam = $ this ->queryParam ('fields ' );
155+ $ fieldsParam = $ this ->parameter ('fields ' );
182156
183157 if (is_array ($ fieldsParam ) && array_key_exists ($ type , $ fieldsParam )) {
184158 $ requested = $ fieldsParam [$ type ];
@@ -210,7 +184,7 @@ public function fieldRequested(string $type, string $field): bool
210184 */
211185 public function sortRequested (string $ field ): bool
212186 {
213- if ($ sort = $ this ->queryParam ('sort ' )) {
187+ if ($ sort = $ this ->parameter ('sort ' )) {
214188 foreach (parse_sort_string ($ sort ) as [$ name , $ direction ]) {
215189 if ($ name === $ field ) {
216190 return true ;
@@ -305,8 +279,10 @@ public function withRequest(ServerRequestInterface $request): static
305279 $ new ->sparseFields = new WeakMap ();
306280 $ new ->body = null ;
307281 $ new ->path = null ;
282+ $ new ->pathSegments = null ;
308283 $ new ->requestedProfiles = null ;
309284 $ new ->requestedExtensions = null ;
285+ $ new ->queryParameterMap = null ;
310286 $ new ->parseAcceptHeader ();
311287 return $ new ;
312288 }
@@ -325,27 +301,6 @@ public function withData(?array $data): static
325301 return $ new ;
326302 }
327303
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-
349304 public function withQuery (?object $ query ): static
350305 {
351306 $ new = clone $ this ;
@@ -367,13 +322,6 @@ public function withModel(?object $model): static
367322 return $ new ;
368323 }
369324
370- public function withField (?Field $ field ): static
371- {
372- $ new = clone $ this ;
373- $ new ->field = $ field ;
374- return $ new ;
375- }
376-
377325 public function withInclude (?array $ include ): static
378326 {
379327 $ new = clone $ this ;
@@ -395,6 +343,130 @@ public function activateProfile(string $uri): static
395343 return $ this ;
396344 }
397345
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+
398470 public function forModel (array $ collections , ?object $ model ): static
399471 {
400472 $ new = clone $ this ;
0 commit comments