View source
<?php
declare (strict_types=1);
namespace Drupal\jsonapi_resources\Unstable;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Component\Uuid\Uuid;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel;
use Drupal\jsonapi\JsonApiResource\LinkCollection;
use Drupal\jsonapi\JsonApiResource\NullIncludedData;
use Drupal\jsonapi\JsonApiResource\ResourceObjectData;
use Drupal\jsonapi\ResourceType\ResourceType;
use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
use Drupal\jsonapi\Serializer\Serializer;
use Drupal\jsonapi_resources\Unstable\Value\NewResourceObject;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
final class DocumentExtractor {
protected $serializer;
protected $resourceTypeRepository;
protected $entityTypeManager;
private $documents;
public function __construct(Serializer $serializer, ResourceTypeRepositoryInterface $resource_type_repository, EntityTypeManagerInterface $entity_type_manager) {
$this->documents = new \SplObjectStorage();
$this->serializer = $serializer;
$this->resourceTypeRepository = $resource_type_repository;
$this->entityTypeManager = $entity_type_manager;
}
public function getDocument(Request $request) {
if (!$this->documents
->contains($request)) {
$this->documents[$request] = $this
->extractDocument($request);
}
return $this->documents[$request];
}
protected function extractDocument(Request $request) {
return new JsonApiDocumentTopLevel(new ResourceObjectData([
$this
->extractResourceObjectFromRequest($request),
], 1), new NullIncludedData(), new LinkCollection([]));
}
protected function extractResourceObjectFromRequest(Request $request) {
$decoded = $this
->decodeRequestPayload($request);
$primary_data = $decoded['data'] ?? [];
if (isset($decoded['data'][0])) {
if (!empty(array_intersect_key($decoded['data'][0], array_flip([
'attributes',
'relationships',
])))) {
throw new UnprocessableEntityHttpException("To add or update a resource object, the request document's primary data must not be an array.");
}
else {
throw new HttpException(501, 'The JSON:API Resources module does not yet support updating relationships.');
}
}
if (!isset($primary_data['type'])) {
throw new UnprocessableEntityHttpException("The document's primary data must have a `type` member.");
}
if (isset($primary_data['id']) && !Uuid::isValid($primary_data['id'])) {
throw new UnprocessableEntityHttpException('IDs should be properly generated and formatted UUIDs as described in RFC 4122. See https://jsonapi.org/format/#crud-creating-client-ids.');
}
$route_resource_types = $request->attributes
->get('resource_types');
foreach ($route_resource_types as $route_resource_type) {
if ($primary_data['type'] === $route_resource_type
->getTypeName()) {
$resource_type = $route_resource_type;
}
}
if (!isset($resource_type)) {
$format = "The document's primary data contains a resource object with a type that cannot be created via this URL. Allowed resource types: `%s`";
$supported_resource_types = array_map(static function (ResourceType $resource_type) {
return $resource_type
->getTypeName();
}, $route_resource_types);
$message = sprintf($format, implode('`, `', $supported_resource_types));
throw new AccessDeniedHttpException($message);
}
if (isset($primary_data['attributes'])) {
$received_attribute_field_names = array_keys($primary_data['attributes']);
$relationship_field_names = array_keys($resource_type
->getRelatableResourceTypes());
if ($relationship_fields_sent_as_attributes = array_intersect($received_attribute_field_names, $relationship_field_names)) {
throw new UnprocessableEntityHttpException(sprintf('The following relationship fields were provided as attributes: [ %s ]', implode(', ', $relationship_fields_sent_as_attributes)));
}
}
if (isset($decoded['data']['relationships'])) {
$primary_data['relationships'] = $this
->handleRelationships($decoded['data']);
}
return NewResourceObject::createFromPrimaryData($resource_type, $primary_data);
}
protected function decodeRequestPayload(Request $request) : array {
$received = (string) $request
->getContent();
if (!$received) {
$relationship_field_name = $request->attributes
->get('_jsonapi_relationship_field_name');
if ($request
->isMethod('DELETE') && $relationship_field_name) {
throw new BadRequestHttpException(sprintf('You need to provide a body for DELETE operations on a relationship (%s).', $relationship_field_name));
}
else {
throw new BadRequestHttpException('Empty request body.');
}
}
try {
$decoded = $this->serializer
->decode($received, 'api_json');
} catch (\UnexpectedValueException $e) {
throw new BadRequestHttpException($e
->getMessage());
}
return $decoded;
}
protected function handleRelationships(array $data) : array {
$relationships = array_map(function ($relationship) {
if (isset($relationship['data']['type']) && isset($relationship['data']['id'])) {
return [
'data' => [
$relationship['data'],
],
];
}
else {
return $relationship;
}
}, $data['relationships']);
$result = [];
foreach ($relationships as $relationship_field => $relationship) {
if (empty($relationship['data'])) {
return [];
}
if (empty($relationship['data'][0]['id'])) {
throw new BadRequestHttpException('No ID specified for related resource');
}
$id_list = array_column($relationship['data'], 'id');
if (empty($relationship['data'][0]['type'])) {
throw new BadRequestHttpException('No type specified for related resource');
}
if (!($resource_type = $this->resourceTypeRepository
->getByTypeName($relationship['data'][0]['type']))) {
throw new BadRequestHttpException(sprintf('Invalid type specified for related resource: "%s"', $relationship['data'][0]['type']));
}
$entity_type_id = $resource_type
->getEntityTypeId();
try {
$entity_storage = $this->entityTypeManager
->getStorage($entity_type_id);
} catch (PluginNotFoundException $e) {
throw new BadRequestHttpException(sprintf('Invalid type specified for related resource: "%s"', $relationship['data'][0]['type']));
}
$uuid_key = $this->entityTypeManager
->getDefinition($entity_type_id)
->getKey('uuid');
$related_entities = array_values($entity_storage
->loadByProperties([
$uuid_key => $id_list,
]));
$map = [];
foreach ($related_entities as $related_entity) {
$map[$related_entity
->uuid()] = $related_entity
->id();
}
$canonical_ids = [];
foreach ($id_list as $delta => $uuid) {
if (!isset($map[$uuid])) {
if ($uuid === 'virtual') {
continue;
}
throw new NotFoundHttpException(sprintf('The resource identified by `%s:%s` (given as a relationship item) could not be found.', $relationship['data'][$delta]['type'], $uuid));
}
$reference_item = [
'target_id' => $map[$uuid],
];
if (isset($relationship['data'][$delta]['meta'])) {
$reference_item += $relationship['data'][$delta]['meta'];
}
$canonical_ids[] = $reference_item;
}
$result[$relationship_field] = $canonical_ids;
}
return $result;
}
}