Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save gabesullice/9fa041caf2bd5b42b500e96cb76b065b to your computer and use it in GitHub Desktop.
Save gabesullice/9fa041caf2bd5b42b500e96cb76b065b to your computer and use it in GitHub Desktop.
Experiment (ignore everything about the "registry")
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