Skip to content

Commit 8b37d47

Browse files
committed
Implement content negotiation
1 parent c1dc91c commit 8b37d47

File tree

6 files changed

+154
-224
lines changed

6 files changed

+154
-224
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"json-api-php/json-api": "^2.2",
99
"nyholm/psr7": "^1.3",
1010
"psr/http-message": "^1.0",
11-
"psr/http-server-handler": "^1.0"
11+
"psr/http-server-handler": "^1.0",
12+
"xynha/http-accept": "dev-master"
1213
},
1314
"license": "MIT",
1415
"authors": [

src/Http/MediaTypes.php

Lines changed: 0 additions & 65 deletions
This file was deleted.

src/JsonApi.php

Lines changed: 111 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Tobyz\JsonApiServer\Exception\UnsupportedMediaTypeException;
2727
use Tobyz\JsonApiServer\Extension\Extension;
2828
use Tobyz\JsonApiServer\Schema\Concerns\HasMeta;
29+
use Xynha\HttpAccept\AcceptParser;
2930

3031
final 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
*

src/functions.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
function json_api_response($document, int $status = 200): Response
2020
{
2121
return (new Response($status))
22-
->withHeader('content-type', JsonApi::MEDIA_TYPE)
22+
->withHeader('Content-Type', JsonApi::MEDIA_TYPE)
2323
->withBody(Stream::create(json_encode($document, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES)));
2424
}
2525

0 commit comments

Comments
 (0)