26
26
use Tobyz \JsonApiServer \Exception \UnsupportedMediaTypeException ;
27
27
use Tobyz \JsonApiServer \Extension \Extension ;
28
28
use Tobyz \JsonApiServer \Schema \Concerns \HasMeta ;
29
+ use Xynha \HttpAccept \AcceptParser ;
29
30
30
31
final class JsonApi implements RequestHandlerInterface
31
32
{
@@ -113,33 +114,52 @@ public function getResourceType(string $type): ResourceType
113
114
*/
114
115
public function handle (Request $ request ): Response
115
116
{
116
- // $this->validateRequest($request);
117
-
118
117
$ this ->validateQueryParameters ($ request );
119
118
120
119
$ context = new Context ($ this , $ request );
121
120
122
- foreach ( $ this ->extensions as $ extension ) {
123
- if ( $ response = $ extension -> handle ( $ context )) {
124
- return $ response;
125
- }
121
+ $ response = $ this ->runExtensions ( $ context );
122
+
123
+ if (! $ response) {
124
+ $ response = $ this -> route ( $ context );
126
125
}
127
126
128
- // TODO: apply Vary: Accept header to response
127
+ return $ response ->withAddedHeader ('Vary ' , 'Accept ' );
128
+ }
129
+
130
+ private function runExtensions (Context $ context ): ?Response
131
+ {
132
+ $ request = $ context ->getRequest ();
133
+
134
+ $ contentTypeExtensionUris = $ this ->getContentTypeExtensionUris ($ request );
135
+ $ acceptableExtensionUris = $ this ->getAcceptableExtensionUris ($ request );
129
136
130
- $ path = $ this ->stripBasePath (
131
- $ request ->getUri ()->getPath ()
137
+ $ activeExtensions = array_intersect_key (
138
+ $ this ->extensions ,
139
+ array_flip ($ contentTypeExtensionUris ),
140
+ array_flip ($ acceptableExtensionUris )
132
141
);
133
142
134
- $ segments = explode ('/ ' , trim ($ path , '/ ' ));
143
+ foreach ($ activeExtensions as $ extension ) {
144
+ if ($ response = $ extension ->handle ($ context )) {
145
+ return $ response ->withHeader ('Content-Type ' , self ::MEDIA_TYPE .'; ext= ' .$ extension ->uri ());
146
+ }
147
+ }
148
+
149
+ return null ;
150
+ }
151
+
152
+ private function route (Context $ context ): Response
153
+ {
154
+ $ segments = explode ('/ ' , trim ($ context ->getPath (), '/ ' ));
135
155
$ resourceType = $ this ->getResourceType ($ segments [0 ]);
136
156
137
157
switch (count ($ segments )) {
138
158
case 1 :
139
- return $ this ->handleCollection ($ context , $ resourceType );
159
+ return $ this ->routeCollection ($ context , $ resourceType );
140
160
141
161
case 2 :
142
- return $ this ->handleResource ($ context , $ resourceType , $ segments [1 ]);
162
+ return $ this ->routeResource ($ context , $ resourceType , $ segments [1 ]);
143
163
144
164
case 3 :
145
165
throw new NotImplementedException ();
@@ -165,63 +185,7 @@ private function validateQueryParameters(Request $request): void
165
185
}
166
186
}
167
187
168
- private function validateRequest (Request $ request ): void
169
- {
170
- // TODO
171
-
172
- // split content type
173
- // ensure type is json-api
174
- // ensure no params other than ext/profile
175
- // ensure no ext other than those supported
176
- // return list of ext/profiles to apply
177
-
178
- if ($ accept = $ request ->getHeaderLine ('Accept ' )) {
179
- $ types = array_map ('trim ' , explode (', ' , $ accept ));
180
-
181
- foreach ($ types as $ type ) {
182
- $ parts = array_map ('trim ' , explode ('; ' , $ type ));
183
- }
184
- }
185
-
186
- // if accept present
187
- // split accept, order by qvalue
188
- // for each media type:
189
- // if type is not json-api, continue
190
- // if any params other than ext/profile, continue
191
- // if any ext other than those supported, continue
192
- // return list of ext/profiles to apply
193
- // if none matching, Not Acceptable
194
- }
195
-
196
- // private function validateRequestContentType(Request $request): void
197
- // {
198
- // $header = $request->getHeaderLine('Content-Type');
199
- //
200
- // if ((new MediaTypes($header))->containsWithOptionalParameters(self::MEDIA_TYPE, ['ext'])) {
201
- // return;
202
- // }
203
- //
204
- // throw new UnsupportedMediaTypeException;
205
- // }
206
- //
207
- // private function getAcceptedParameters(Request $request): array
208
- // {
209
- // $header = $request->getHeaderLine('Accept');
210
- //
211
- // if (empty($header)) {
212
- // return [];
213
- // }
214
- //
215
- // $mediaTypes = new MediaTypes($header);
216
- //
217
- // if ($parameters = $mediaTypes->get(self::MEDIA_TYPE, ['ext', 'profile'])) {
218
- // return $parameters;
219
- // }
220
- //
221
- // throw new NotAcceptableException;
222
- // }
223
-
224
- private function handleCollection (Context $ context , ResourceType $ resourceType ): Response
188
+ private function routeCollection (Context $ context , ResourceType $ resourceType ): Response
225
189
{
226
190
switch ($ context ->getRequest ()->getMethod ()) {
227
191
case 'GET ' :
@@ -235,9 +199,9 @@ private function handleCollection(Context $context, ResourceType $resourceType):
235
199
}
236
200
}
237
201
238
- private function handleResource (Context $ context , ResourceType $ resourceType , string $ id ): Response
202
+ private function routeResource (Context $ context , ResourceType $ resourceType , string $ resourceId ): Response
239
203
{
240
- $ model = $ this ->findResource ($ resourceType , $ id , $ context );
204
+ $ model = $ this ->findResource ($ resourceType , $ resourceId , $ context );
241
205
242
206
switch ($ context ->getRequest ()->getMethod ()) {
243
207
case 'PATCH ' :
@@ -254,6 +218,82 @@ private function handleResource(Context $context, ResourceType $resourceType, st
254
218
}
255
219
}
256
220
221
+ private function getContentTypeExtensionUris (Request $ request ): array
222
+ {
223
+ if (! $ contentType = $ request ->getHeaderLine ('Content-Type ' )) {
224
+ return [];
225
+ }
226
+
227
+ $ mediaList = (new AcceptParser ())->parse ($ contentType );
228
+
229
+ if ($ mediaList ->count () > 1 ) {
230
+ throw new UnsupportedMediaTypeException ();
231
+ }
232
+
233
+ $ mediaType = $ mediaList ->preferredMedia (0 );
234
+
235
+ if ($ mediaType ->mimetype () !== JsonApi::MEDIA_TYPE ) {
236
+ throw new UnsupportedMediaTypeException ();
237
+ }
238
+
239
+ $ parameters = $ this ->parseParameters ($ mediaType ->parameters ());
240
+
241
+ if (! empty (array_diff (array_keys ($ parameters ), ['ext ' , 'profile ' ]))) {
242
+ throw new UnsupportedMediaTypeException ();
243
+ }
244
+
245
+ $ extensionUris = isset ($ parameters ['ext ' ]) ? explode (' ' , $ parameters ['ext ' ]) : [];
246
+
247
+ if (! empty (array_diff ($ extensionUris , array_keys ($ this ->extensions )))) {
248
+ throw new UnsupportedMediaTypeException ();
249
+ }
250
+
251
+ return $ extensionUris ;
252
+ }
253
+
254
+ private function getAcceptableExtensionUris (Request $ request ): array
255
+ {
256
+ if (! $ accept = $ request ->getHeaderLine ('Accept ' )) {
257
+ return [];
258
+ }
259
+
260
+ $ mediaList = (new AcceptParser ())->parse ($ accept );
261
+ $ count = $ mediaList ->count ();
262
+
263
+ for ($ i = 0 ; $ i < $ count ; $ i ++) {
264
+ $ mediaType = $ mediaList ->preferredMedia ($ i );
265
+
266
+ if (! in_array ($ mediaType ->mimetype (), [JsonApi::MEDIA_TYPE , '*/* ' ])) {
267
+ continue ;
268
+ }
269
+
270
+ $ parameters = $ this ->parseParameters ($ mediaType ->parameters ());
271
+
272
+ if (! empty (array_diff (array_keys ($ parameters ), ['ext ' , 'profile ' ]))) {
273
+ continue ;
274
+ }
275
+
276
+ $ extensionUris = isset ($ parameters ['ext ' ]) ? explode (' ' , $ parameters ['ext ' ]) : [];
277
+
278
+ if (! empty (array_diff ($ extensionUris , array_keys ($ this ->extensions )))) {
279
+ continue ;
280
+ }
281
+
282
+ return $ extensionUris ;
283
+ }
284
+
285
+ throw new NotAcceptableException ();
286
+ }
287
+
288
+ private function parseParameters (array $ parameters ): array
289
+ {
290
+ return array_reduce ($ parameters , function ($ a , $ v ) {
291
+ $ parts = explode ('= ' , $ v , 2 );
292
+ $ a [$ parts [0 ]] = trim ($ parts [1 ], '" ' );
293
+ return $ a ;
294
+ }, []);
295
+ }
296
+
257
297
/**
258
298
* Convert an exception into a JSON:API error document response.
259
299
*
0 commit comments