Created
May 16, 2019 18:50
-
-
Save gabesullice/9fa041caf2bd5b42b500e96cb76b065b to your computer and use it in GitHub Desktop.
Experiment (ignore everything about the "registry")
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/jsonapi_operations.services.yml b/jsonapi_operations.services.yml | |
index bbb3da6..d6fd805 100644 | |
--- a/jsonapi_operations.services.yml | |
+++ b/jsonapi_operations.services.yml | |
@@ -6,3 +6,8 @@ services: | |
- '@jsonapi.resource_type.repository' | |
- '@database' | |
- '@http_kernel' | |
+ | |
+ jsonapi_operations.local_id_registry: | |
+ class: Drupal\jsonapi_operations\LocalIdRegistry | |
+ arguments: | |
+ - '@router.no_access_checks' | |
diff --git a/src/Controller/OperationsHandler.php b/src/Controller/OperationsHandler.php | |
index 6d216d9..e6ebd57 100644 | |
--- a/src/Controller/OperationsHandler.php | |
+++ b/src/Controller/OperationsHandler.php | |
@@ -9,10 +9,13 @@ use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel; | |
use Drupal\jsonapi\JsonApiResource\LinkCollection; | |
use Drupal\jsonapi\JsonApiResource\ResourceObjectData; | |
use Drupal\jsonapi\ResourceResponse; | |
+use Drupal\jsonapi_operations\JsonApiObject\JsonApiOperationObject; | |
use JsonPath\JsonObject; | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\HttpFoundation\Response; | |
use Symfony\Component\HttpKernel\HttpKernelInterface; | |
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; | |
+use Symfony\Component\Serializer\SerializerInterface; | |
/** | |
* Bulk operations front controller. | |
@@ -33,23 +36,26 @@ class OperationsHandler { | |
/** @var \Symfony\Component\HttpKernel\HttpKernelInterface */ | |
protected $httpKernel; | |
+ /** @var \Symfony\Component\Serializer\Normalizer\DenormalizerInterface */ | |
+ protected $serializer; | |
+ | |
/** | |
* Construct a new OperationsHandler. | |
* | |
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager | |
* the Drupal entity type manager | |
- * @param $resourceTypeRepository | |
- * the JSON:API resource type repository | |
* @param $databaseConnection | |
* the connection to the database | |
* @param $httpKernel | |
* the Drupal http kernel | |
+ * @param $serializer | |
+ * the serializer. | |
*/ | |
- public function __construct($entityTypeManager, $resourceTypeRepository, $databaseConnection, $httpKernel) { | |
+ public function __construct($entityTypeManager, $databaseConnection, $httpKernel, $serializer) { | |
$this->entityTypeManager = $entityTypeManager; | |
- $this->resourceTypeRepository = $resourceTypeRepository; | |
$this->databaseConnection = $databaseConnection; | |
$this->httpKernel = $httpKernel; | |
+ $this->serializer = $serializer; | |
} | |
/** | |
@@ -72,58 +78,41 @@ class OperationsHandler { | |
return new ResourceResponse($jsonApiDocument, 400); | |
} | |
+ $operations = array_map(function ($operation) { | |
+ return $this->serializer->denormalize($operation, JsonApiOperationObject::class, 'api_json'); | |
+ }, $operations); | |
+ | |
$transaction = $this->databaseConnection->startTransaction(); | |
try { | |
- $responses = array_reduce($operations, function ($carry, $operation) use ($request, $transaction) { | |
- | |
- $op = $operation['op']; | |
+ $responses = array_reduce($operations, function ($carry, JsonApiOperationObject $operation) use ($request, $transaction) { | |
$data = !empty($operation['data']) ? $operation['data'] : NULL; | |
- $type = !empty($operation['ref']['type']) ? $operation['ref']['type'] : NULL; | |
- $id = !empty($operation['ref']['id']) ? $operation['ref']['id'] : NULL; | |
- | |
- $resourceType = $this->resourceTypeRepository->getByTypeName($type); | |
- $storage = $this->entityTypeManager->getStorage($resourceType->getEntityTypeId()); | |
+ $ref = $operation->getRef(); | |
+ $resourceType = $operation->getRef()->getResourceType(); | |
$data = self::replaceTokens($data, $carry); | |
- if ($id) { | |
- $entities = $storage->loadByProperties(['uuid' => $id]); | |
- $entity = $entities && count($entities) ? reset($entities) : NULL; | |
- $url = Url::fromRoute(sprintf('jsonapi.%s.individual', $type), ['entity' => $entity->uuid()]); | |
+ if ($id = $operation->getRef()) { | |
+ $url = Url::fromRoute(sprintf('jsonapi.%s.individual', $resourceType->getTypeName()), ['entity' => $ref->getId()]); | |
} | |
else{ | |
- $url =Url::fromRoute(sprintf('jsonapi.%s.collection', $type)); | |
- } | |
- $content = NULL; | |
- switch ($op) { | |
- case 'get': | |
- $method = 'GET'; | |
- break; | |
- case 'add': | |
- case 'update': | |
- $content = Json::encode([ | |
- 'data' => $data, | |
- ]); | |
- if ($op === 'update') { | |
- $method = 'PATCH'; | |
- } | |
- else { | |
- $method = 'POST'; | |
- } | |
- break; | |
- case 'delete': | |
- $method = 'DELETE'; | |
- break; | |
- | |
- default: | |
- throw new \Exception('unsupported operation: ' + $op); | |
+ $url = Url::fromRoute(sprintf('jsonapi.%s.collection', $resourceType)); | |
} | |
+ $method = [ | |
+ 'get' => 'GET', | |
+ 'add' => 'POST', | |
+ 'update' => 'PATCH', | |
+ 'remove' => 'DELETE', | |
+ ]; | |
+ $meta = $operation->getMeta(); | |
+ $content = Json::encode([ | |
+ 'data' => $operation->getData(), | |
+ ] + $meta ? ['meta' => $meta] : []); | |
try { | |
- $subRequest = Request::create($url->toString(TRUE)->getGeneratedUrl(), $method, [], $request->cookies->all(), $request->files->all(), $request->server->all(), $content); | |
+ $subRequest = Request::create($url->toString(TRUE)->getGeneratedUrl(), $method[$operation->getOp()], [], $request->cookies->all(), $request->files->all(), $request->server->all(), $content); | |
$responseItem = [ | |
'operation' => $operation, | |
'response' => $this->httpKernel->handle($subRequest, HttpKernelInterface::MASTER_REQUEST, FALSE), | |
diff --git a/src/JsonApiObject/JsonApiOperationObject.php b/src/JsonApiObject/JsonApiOperationObject.php | |
new file mode 100644 | |
index 0000000..1be9f29 | |
--- /dev/null | |
+++ b/src/JsonApiObject/JsonApiOperationObject.php | |
@@ -0,0 +1,85 @@ | |
+<?php | |
+ | |
+namespace Drupal\jsonapi_operations\JsonApiObject; | |
+ | |
+class JsonApiOperationObject { | |
+ | |
+ public static $validOps = [ | |
+ 'get', | |
+ 'add', | |
+ 'update', | |
+ 'remove', | |
+ ]; | |
+ | |
+ /** | |
+ * @var string | |
+ */ | |
+ protected $op; | |
+ | |
+ /** | |
+ * @var array|NULL | |
+ */ | |
+ protected $data; | |
+ | |
+ /** | |
+ * @var \Drupal\jsonapi_operations\JsonApiObject\ResourceReference|null | |
+ */ | |
+ protected $ref; | |
+ | |
+ /** | |
+ * @var array | |
+ */ | |
+ protected $params; | |
+ | |
+ /** | |
+ * @var array | |
+ */ | |
+ protected $meta; | |
+ | |
+ public function __construct($op, $data = NULL, array $params = [], $ref = NULL, array $meta = []) { | |
+ assert(is_array($data) || is_null($data)); | |
+ assert(in_array($op, static::$validOps, TRUE)); | |
+ assert(is_null($ref) || $ref instanceof ResourceReference); | |
+ $this->op = $op; | |
+ $this->data = $data; | |
+ $this->ref = $ref; | |
+ $this->params = $params; | |
+ $this->meta = $meta; | |
+ } | |
+ | |
+ /** | |
+ * @return string | |
+ */ | |
+ public function getOp() { | |
+ return $this->op; | |
+ } | |
+ | |
+ /** | |
+ * @return array|NULL | |
+ */ | |
+ public function getData() { | |
+ return $this->data; | |
+ } | |
+ | |
+ /** | |
+ * @return \Drupal\jsonapi_operations\JsonApiObject\ResourceReference|null | |
+ */ | |
+ public function getRef() { | |
+ return $this->ref; | |
+ } | |
+ | |
+ /** | |
+ * @return array | |
+ */ | |
+ public function getParams() { | |
+ return $this->params; | |
+ } | |
+ | |
+ /** | |
+ * @return array | |
+ */ | |
+ public function getMeta() { | |
+ return $this->meta; | |
+ } | |
+ | |
+} | |
diff --git a/src/JsonApiObject/ResourceReference.php b/src/JsonApiObject/ResourceReference.php | |
new file mode 100644 | |
index 0000000..2c06167 | |
--- /dev/null | |
+++ b/src/JsonApiObject/ResourceReference.php | |
@@ -0,0 +1,59 @@ | |
+<?php | |
+ | |
+namespace Drupal\jsonapi_operations\JsonApiObject; | |
+ | |
+use Drupal\jsonapi\ResourceType\ResourceType; | |
+ | |
+class ResourceReference { | |
+ | |
+ protected $resourceType; | |
+ | |
+ protected $id; | |
+ | |
+ /** | |
+ * The referenced relationship. | |
+ * | |
+ * @var string | |
+ */ | |
+ protected $relationshipFieldName; | |
+ | |
+ public function __construct(ResourceType $resource_type, string $id = NULL, $relationship_field_name = NULL) { | |
+ assert(is_null($relationship_field_name) || (is_string($id) && $resource_type->hasField($relationship_field_name))); | |
+ $this->resourceType = $resource_type; | |
+ $this->id = $id; | |
+ $this->relationshipFieldName = $relationship_field_name; | |
+ } | |
+ | |
+ /** | |
+ * The referenced JSON:API resource type. | |
+ * | |
+ * @return \Drupal\jsonapi\ResourceType\ResourceType | |
+ * | |
+ */ | |
+ public function getResourceType() { | |
+ return $this->resourceType; | |
+ } | |
+ | |
+ /** | |
+ * The referenced resource object ID. | |
+ * | |
+ * @return string|NULL | |
+ * The referenced resource object ID or NULL if a resource object has not | |
+ * been referenced. | |
+ */ | |
+ public function getId() { | |
+ return $this->id; | |
+ } | |
+ | |
+ /** | |
+ * The referenced relationship field name. | |
+ * | |
+ * @return string|NULL | |
+ * The referenced relationship field name or NULL if a relationship has not | |
+ * been referenced. | |
+ */ | |
+ public function getRelationshipFieldName() { | |
+ return $this->relationshipFieldName; | |
+ } | |
+ | |
+} | |
diff --git a/src/LocalIdRegistry.php b/src/LocalIdRegistry.php | |
new file mode 100644 | |
index 0000000..b1feb92 | |
--- /dev/null | |
+++ b/src/LocalIdRegistry.php | |
@@ -0,0 +1,49 @@ | |
+<?php | |
+ | |
+namespace Drupal\jsonapi_operations; | |
+ | |
+use Drupal\Core\Routing\RouteMatchInterface; | |
+use Drupal\Core\Url; | |
+use Drupal\jsonapi\JsonApiResource\ResourceIdentifier; | |
+use Symfony\Cmf\Component\Routing\RouteObjectInterface; | |
+use Symfony\Component\HttpFoundation\Request; | |
+use Symfony\Component\Routing\Matcher\RequestMatcherInterface; | |
+use Symfony\Component\Routing\Route; | |
+ | |
+class LocalIdRegistry { | |
+ | |
+ protected $registry = []; | |
+ | |
+ /** | |
+ * @var \Symfony\Component\Routing\Matcher\RequestMatcherInterface | |
+ */ | |
+ protected $router; | |
+ | |
+ public function __construct(RequestMatcherInterface $router) { | |
+ $this->router = $router; | |
+ } | |
+ | |
+ public function register($incoming_document, $outgoing_document) { | |
+ assert(is_array($outgoing_document)); | |
+ if (isset($incoming_document['data']['type'])) { | |
+ if (isset($incoming_document['data']['lid'])) { | |
+ $this->associate($incoming_document, $outgoing_document); | |
+ } | |
+ } | |
+ } | |
+ | |
+ protected function associate($incoming_identifiable, $outgoing_identifiable) { | |
+ $this->registry[$incoming_identifiable['data']['lid']] = $this->getResourceIdentifier($outgoing_identifiable); | |
+ } | |
+ | |
+ protected function getResourceIdentifier($identifiable) { | |
+ return new ResourceIdentifier($identifiable['data']['type'], $identifiable['data']['id']); | |
+ } | |
+ | |
+ protected function isRelationshipResource($href) { | |
+ $route_match = $this->router->matchRequest($href); | |
+ $route_name = $route_match[RouteObjectInterface::ROUTE_NAME]; | |
+ return strpos($route_name, 'relationship') !== FALSE; | |
+ } | |
+ | |
+} | |
\ No newline at end of file | |
diff --git a/src/Normalizer/JsonApiOperationObjectNormalizer.php b/src/Normalizer/JsonApiOperationObjectNormalizer.php | |
new file mode 100644 | |
index 0000000..7e2d595 | |
--- /dev/null | |
+++ b/src/Normalizer/JsonApiOperationObjectNormalizer.php | |
@@ -0,0 +1,127 @@ | |
+<?php | |
+ | |
+namespace Drupal\jsonapi_operations\Normalizer; | |
+ | |
+use Drupal\Component\Assertion\Inspector; | |
+use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface; | |
+use Drupal\jsonapi_operations\JsonApiObject\JsonApiOperationObject; | |
+use Drupal\jsonapi_operations\JsonApiObject\ResourceReference; | |
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; | |
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; | |
+ | |
+class JsonApiOperationObjectNormalizer implements DenormalizerInterface { | |
+ | |
+ const RESOURCE_REFERENCE_KEY = 'jsonapi_operations_resource_ref'; | |
+ | |
+ protected $jsonApiSerializer; | |
+ | |
+ protected $resourceTypeRepository; | |
+ | |
+ public function __construct(DenormalizerInterface $jsonapi_serializer, ResourceTypeRepositoryInterface $resource_type_repository) { | |
+ $this->jsonApiSerializer = $jsonapi_serializer; | |
+ $this->resourceTypeRepository = $resource_type_repository; | |
+ } | |
+ | |
+ protected static $knownMembers = [ | |
+ 'op', | |
+ 'data', | |
+ 'included', | |
+ 'ref', | |
+ 'params', | |
+ 'links', | |
+ 'meta', | |
+ ]; | |
+ | |
+ /** | |
+ * {@inheritdoc} | |
+ */ | |
+ public function supportsDenormalization($data, $type, $format = NULL) { | |
+ return $type === JsonApiOperationObject::class && $format = 'application/vnd.api+json'; | |
+ } | |
+ | |
+ /** | |
+ * {@inheritdoc} | |
+ */ | |
+ public function denormalize($data, $class, $format = NULL, array $context = []) { | |
+ $recognized_members = array_intersect_key($data, static::$knownMembers); | |
+ if (!isset($recognized_members['op'])) { | |
+ throw new BadRequestHttpException('The `op` member is required for all operation objects.'); | |
+ } | |
+ $op = $recognized_members['op']; | |
+ if (!is_string($op) || !in_array($op, JsonApiOperationObject::$validOps, TRUE)) { | |
+ throw new BadRequestHttpException(sprintf('Unrecognized `op` member. Valid values include: %s', implode(', ', JsonApiOperationObject::$validOps))); | |
+ } | |
+ if (!isset($recognized_members['ref']) && !isset($context[static::RESOURCE_REFERENCE_KEY])) { | |
+ throw new BadRequestHttpException('A `ref` member is required to process this operation.'); | |
+ } | |
+ if (isset($recognized_members['ref'])) { | |
+ $ref = $recognized_members['ref']; | |
+ if (!isset($ref['type'])) { | |
+ throw new BadRequestHttpException('The `ref` member must contain a `type` member.'); | |
+ } | |
+ $resource_type = $this->resourceTypeRepository->getByTypeName($ref['type']); | |
+ if (isset($ref['id'])) { | |
+ $ref = isset($ref['relationship']) | |
+ ? new ResourceReference($resource_type, $ref['id'], $ref['relationship']) | |
+ : new ResourceReference($resource_type, $ref['id']); | |
+ } | |
+ else { | |
+ if (isset($ref['relationship'])) { | |
+ throw new BadRequestHttpException('The `ref` member must have an `id` member if it also has a `relationship` member.'); | |
+ } | |
+ $ref = new ResourceReference($resource_type); | |
+ } | |
+ } | |
+ elseif (isset($context[static::RESOURCE_REFERENCE_KEY])) { | |
+ assert($context[static::RESOURCE_REFERENCE_KEY] instanceof ResourceReference); | |
+ $ref = $context[static::RESOURCE_REFERENCE_KEY]; | |
+ } | |
+ else { | |
+ $ref = NULL; | |
+ } | |
+ if (isset($recognized_members['included'])) { | |
+ throw new BadRequestHttpException('Requests with an `included` member on an operation object are not supported by this server.'); | |
+ } | |
+ if (isset($recognized_members['links'])) { | |
+ throw new BadRequestHttpException('Requests with a `links` member on an operation object are not supported by this server.'); | |
+ } | |
+ if (isset($recognized_members['params'])) { | |
+ $params = $recognized_members['params'] ; | |
+ static::validateParamsObject($params); | |
+ } | |
+ else { | |
+ $params = []; | |
+ } | |
+ return new JsonApiOperationObject($op, $data, $params, $ref, empty($recognized_members['meta']) ? $recognized_members['meta'] : []); | |
+ } | |
+ | |
+ protected static function validateParamsObject($params_object) { | |
+ if (isset($recognized_members['params'])) { | |
+ $invalid_params_object = FALSE; | |
+ if (!is_array($params_object)) { | |
+ $invalid_params_object = 'The value must be an object.'; | |
+ } | |
+ if (Inspector::assertAllStrings(array_keys($params_object))) { | |
+ $invalid_params_object = 'All parameter names must be strings.'; | |
+ } | |
+ $valid_param_values = function ($value) { | |
+ if (is_string($value) || is_numeric($value)) { | |
+ return TRUE; | |
+ } | |
+ if (is_array($value)) { | |
+ return array_reduce($value, function ($valid, $value) { | |
+ return $valid && is_string($value) || is_numeric($value); | |
+ }, TRUE); | |
+ } | |
+ return FALSE; | |
+ }; | |
+ if (Inspector::assertAll($valid_param_values, $params_object)) { | |
+ $invalid_params_object = 'All parameter values must be strings, numbers, or an array of strings or numbers.'; | |
+ } | |
+ if ($invalid_params_object) { | |
+ throw new BadRequestHttpException("The `params` operation object member is invalid. $invalid_params_object"); | |
+ } | |
+ } | |
+ } | |
+ | |
+} | |
diff --git a/tests/src/Kernel/LocalIdRegistryTest.php b/tests/src/Kernel/LocalIdRegistryTest.php | |
new file mode 100644 | |
index 0000000..0cf2f58 | |
--- /dev/null | |
+++ b/tests/src/Kernel/LocalIdRegistryTest.php | |
@@ -0,0 +1,71 @@ | |
+<?php | |
+ | |
+namespace Drupal\Tests\jsonapi_operations\Kernel; | |
+ | |
+use Drupal\Core\Link; | |
+use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel; | |
+use Drupal\jsonapi\JsonApiResource\LinkCollection; | |
+use Drupal\jsonapi\JsonApiResource\NullIncludedData; | |
+use Drupal\jsonapi\JsonApiResource\ResourceObject; | |
+use Drupal\jsonapi\JsonApiResource\ResourceObjectData; | |
+use Drupal\KernelTests\KernelTestBase; | |
+use Drupal\node\Entity\Node; | |
+use Drupal\node\Entity\NodeType; | |
+ | |
+class LocalIdRegistryTest extends KernelTestBase { | |
+ | |
+ protected static $modules = [ | |
+ 'field', | |
+ 'jsonapi', | |
+ 'jsonapi_operations', | |
+ 'node', | |
+ 'serialization', | |
+ 'system', | |
+ 'user', | |
+ ]; | |
+ | |
+ /** | |
+ * @var \Drupal\jsonapi_operations\LocalIdRegistry | |
+ */ | |
+ protected $registry; | |
+ | |
+ protected function setUp() { | |
+ parent::setUp(); | |
+ // Add the entity schemas. | |
+ $this->installEntitySchema('node'); | |
+ $this->installEntitySchema('user'); | |
+ // Add the additional table schemas. | |
+ $this->installSchema('system', ['sequences']); | |
+ $this->installSchema('node', ['node_access']); | |
+ $this->installSchema('user', ['users_data']); | |
+ | |
+ $this->container->get('cache_tags.invalidator')->invalidateTags(['jsonapi_resource_types']); | |
+ | |
+ NodeType::create(['type' => 'article'])->save(); | |
+ | |
+ $this->node = Node::create([ | |
+ 'type' => 'article', | |
+ 'title' => 'Test node', | |
+ ]); | |
+ $this->node->save(); | |
+ $this->registry = $this->container->get('jsonapi_operations.local_id_registry'); | |
+ $this->serializer = $this->container->get('jsonapi.serializer'); | |
+ $this->rtr = $this->container->get('jsonapi.resource_type.repository'); | |
+ } | |
+ | |
+ public function testRegister() { | |
+ $incoming_document = [ | |
+ 'data' => [ | |
+ 'type' => 'node--article', | |
+ 'lid' => 'new-node', | |
+ ], | |
+ ]; | |
+ $resource_type = $this->rtr->get($this->node->getEntityTypeId(), $this->node->bundle()); | |
+ $outgoing_object = new JsonApiDocumentTopLevel(new ResourceObjectData([ResourceObject::createFromEntity($resource_type, $this->node)], 1), new NullIncludedData(), new LinkCollection([])); | |
+ $outgoing_document = $this->serializer->normalize($outgoing_object, 'api_json', [ | |
+ 'account' => NULL, | |
+ ]); | |
+ $this->registry->register($incoming_document, $outgoing_document->getNormalization()); | |
+ } | |
+ | |
+} | |
\ No newline at end of file |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment