230 lines
6.4 KiB
PHP
230 lines
6.4 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of tobyz/json-api-server.
|
|
*
|
|
* (c) Toby Zerner <toby.zerner@gmail.com>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Tobyz\JsonApiServer;
|
|
|
|
use JsonApiPhp\JsonApi\ErrorDocument;
|
|
use Psr\Http\Message\ResponseInterface as Response;
|
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
|
use Psr\Http\Server\RequestHandlerInterface;
|
|
use Tobyz\JsonApiServer\Adapter\AdapterInterface;
|
|
use Tobyz\JsonApiServer\Exception\BadRequestException;
|
|
use Tobyz\JsonApiServer\Exception\InternalServerErrorException;
|
|
use Tobyz\JsonApiServer\Exception\MethodNotAllowedException;
|
|
use Tobyz\JsonApiServer\Exception\NotAcceptableException;
|
|
use Tobyz\JsonApiServer\Exception\NotImplementedException;
|
|
use Tobyz\JsonApiServer\Exception\ResourceNotFoundException;
|
|
use Tobyz\JsonApiServer\Exception\UnsupportedMediaTypeException;
|
|
use Tobyz\JsonApiServer\Endpoint\Concerns\FindsResources;
|
|
use Tobyz\JsonApiServer\Http\MediaTypes;
|
|
use Tobyz\JsonApiServer\Schema\Concerns\HasMeta;
|
|
use Tobyz\JsonApiServer\Context;
|
|
|
|
final class JsonApi implements RequestHandlerInterface
|
|
{
|
|
const MEDIA_TYPE = 'application/vnd.api+json';
|
|
|
|
use FindsResources;
|
|
use HasMeta;
|
|
|
|
private $resources = [];
|
|
private $basePath;
|
|
|
|
public function __construct(string $basePath)
|
|
{
|
|
$this->basePath = $basePath;
|
|
}
|
|
|
|
/**
|
|
* Define a new resource type.
|
|
*/
|
|
public function resource(string $type, AdapterInterface $adapter, callable $buildSchema = null): void
|
|
{
|
|
$this->resources[$type] = new ResourceType($type, $adapter, $buildSchema);
|
|
}
|
|
|
|
/**
|
|
* Get defined resource types.
|
|
*
|
|
* @return ResourceType[]
|
|
*/
|
|
public function getResources(): array
|
|
{
|
|
return $this->resources;
|
|
}
|
|
|
|
/**
|
|
* Get a resource type.
|
|
*
|
|
* @throws ResourceNotFoundException if the resource type has not been defined.
|
|
*/
|
|
public function getResource(string $type): ResourceType
|
|
{
|
|
if (! isset($this->resources[$type])) {
|
|
throw new ResourceNotFoundException($type);
|
|
}
|
|
|
|
return $this->resources[$type];
|
|
}
|
|
|
|
/**
|
|
* Handle a request.
|
|
*
|
|
* @throws UnsupportedMediaTypeException if the request Content-Type header is invalid
|
|
* @throws NotAcceptableException if the request Accept header is invalid
|
|
* @throws MethodNotAllowedException if the request method is invalid
|
|
* @throws BadRequestException if the request URI is invalid
|
|
* @throws NotImplementedException
|
|
*/
|
|
public function handle(Request $request): Response
|
|
{
|
|
$this->validateRequest($request);
|
|
|
|
$path = $this->stripBasePath(
|
|
$request->getUri()->getPath()
|
|
);
|
|
|
|
$segments = explode('/', trim($path, '/'));
|
|
$resource = $this->getResource($segments[0]);
|
|
$context = new Context($request);
|
|
|
|
switch (count($segments)) {
|
|
case 1:
|
|
return $this->handleCollection($context, $resource);
|
|
|
|
case 2:
|
|
return $this->handleResource($context, $resource, $segments[1]);
|
|
|
|
case 3:
|
|
throw new NotImplementedException;
|
|
|
|
case 4:
|
|
if ($segments[2] === 'relationships') {
|
|
throw new NotImplementedException;
|
|
}
|
|
}
|
|
|
|
throw new BadRequestException;
|
|
}
|
|
|
|
private function validateRequest(Request $request): void
|
|
{
|
|
$this->validateRequestContentType($request);
|
|
$this->validateRequestAccepts($request);
|
|
}
|
|
|
|
private function validateRequestContentType(Request $request): void
|
|
{
|
|
$header = $request->getHeaderLine('Content-Type');
|
|
|
|
if (empty($header)) {
|
|
return;
|
|
}
|
|
|
|
if ((new MediaTypes($header))->containsExactly(self::MEDIA_TYPE)) {
|
|
return;
|
|
}
|
|
|
|
throw new UnsupportedMediaTypeException;
|
|
}
|
|
|
|
private function validateRequestAccepts(Request $request): void
|
|
{
|
|
$header = $request->getHeaderLine('Accept');
|
|
|
|
if (empty($header)) {
|
|
return;
|
|
}
|
|
|
|
$mediaTypes = new MediaTypes($header);
|
|
|
|
if ($mediaTypes->containsExactly('*/*') || $mediaTypes->containsExactly(self::MEDIA_TYPE)) {
|
|
return;
|
|
}
|
|
|
|
throw new NotAcceptableException;
|
|
}
|
|
|
|
private function stripBasePath(string $path): string
|
|
{
|
|
$basePath = parse_url($this->basePath, PHP_URL_PATH);
|
|
|
|
$len = strlen($basePath);
|
|
|
|
if (substr($path, 0, $len) === $basePath) {
|
|
$path = substr($path, $len);
|
|
}
|
|
|
|
return $path;
|
|
}
|
|
|
|
private function handleCollection(Context $context, ResourceType $resource): Response
|
|
{
|
|
switch ($context->getRequest()->getMethod()) {
|
|
case 'GET':
|
|
return (new Endpoint\Index($this, $resource))->handle($context);
|
|
|
|
case 'POST':
|
|
return (new Endpoint\Create($this, $resource))->handle($context);
|
|
|
|
default:
|
|
throw new MethodNotAllowedException;
|
|
}
|
|
}
|
|
|
|
private function handleResource(Context $context, ResourceType $resource, string $id): Response
|
|
{
|
|
$model = $this->findResource($resource, $id, $context);
|
|
|
|
switch ($context->getRequest()->getMethod()) {
|
|
case 'PATCH':
|
|
return (new Endpoint\Update($this, $resource, $model))->handle($context);
|
|
|
|
case 'GET':
|
|
return (new Endpoint\Show($this, $resource, $model))->handle($context);
|
|
|
|
case 'DELETE':
|
|
return (new Endpoint\Delete($this, $resource, $model))->handle($context);
|
|
|
|
default:
|
|
throw new MethodNotAllowedException;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert an exception into a JSON:API error document response.
|
|
*
|
|
* If the exception is not an instance of ErrorProviderInterface, an
|
|
* Internal Server Error response will be produced.
|
|
*/
|
|
public function error($e)
|
|
{
|
|
if (! $e instanceof ErrorProviderInterface) {
|
|
$e = new InternalServerErrorException;
|
|
}
|
|
|
|
$errors = $e->getJsonApiErrors();
|
|
$status = $e->getJsonApiStatus();
|
|
|
|
$document = new ErrorDocument(...$errors);
|
|
|
|
return json_api_response($document, $status);
|
|
}
|
|
|
|
/**
|
|
* Get the base path for the API.
|
|
*/
|
|
public function getBasePath(): string
|
|
{
|
|
return $this->basePath;
|
|
}
|
|
}
|