2626use Tobyz \JsonApiServer \Exception \UnsupportedMediaTypeException ;
2727use Tobyz \JsonApiServer \Extension \Extension ;
2828use Tobyz \JsonApiServer \Schema \Concerns \HasMeta ;
29+ use Xynha \HttpAccept \AcceptParser ;
2930
3031final class JsonApi implements RequestHandlerInterface
3132{
@@ -113,33 +114,52 @@ public function getResourceType(string $type): ResourceType
113114 */
114115 public function handle (Request $ request ): Response
115116 {
116- // $this->validateRequest($request);
117-
118117 $ this ->validateQueryParameters ($ request );
119118
120119 $ context = new Context ($ this , $ request );
121120
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 );
126125 }
127126
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 );
129136
130- $ path = $ this ->stripBasePath (
131- $ request ->getUri ()->getPath ()
137+ $ activeExtensions = array_intersect_key (
138+ $ this ->extensions ,
139+ array_flip ($ contentTypeExtensionUris ),
140+ array_flip ($ acceptableExtensionUris )
132141 );
133142
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 (), '/ ' ));
135155 $ resourceType = $ this ->getResourceType ($ segments [0 ]);
136156
137157 switch (count ($ segments )) {
138158 case 1 :
139- return $ this ->handleCollection ($ context , $ resourceType );
159+ return $ this ->routeCollection ($ context , $ resourceType );
140160
141161 case 2 :
142- return $ this ->handleResource ($ context , $ resourceType , $ segments [1 ]);
162+ return $ this ->routeResource ($ context , $ resourceType , $ segments [1 ]);
143163
144164 case 3 :
145165 throw new NotImplementedException ();
@@ -165,63 +185,7 @@ private function validateQueryParameters(Request $request): void
165185 }
166186 }
167187
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
225189 {
226190 switch ($ context ->getRequest ()->getMethod ()) {
227191 case 'GET ' :
@@ -235,9 +199,9 @@ private function handleCollection(Context $context, ResourceType $resourceType):
235199 }
236200 }
237201
238- private function handleResource (Context $ context , ResourceType $ resourceType , string $ id ): Response
202+ private function routeResource (Context $ context , ResourceType $ resourceType , string $ resourceId ): Response
239203 {
240- $ model = $ this ->findResource ($ resourceType , $ id , $ context );
204+ $ model = $ this ->findResource ($ resourceType , $ resourceId , $ context );
241205
242206 switch ($ context ->getRequest ()->getMethod ()) {
243207 case 'PATCH ' :
@@ -254,6 +218,82 @@ private function handleResource(Context $context, ResourceType $resourceType, st
254218 }
255219 }
256220
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+
257297 /**
258298 * Convert an exception into a JSON:API error document response.
259299 *
0 commit comments