jsonapi.services.yml | 3 + src/Annotation/JsonApiResource.php | 61 -------- src/Annotation/JsonApiResourceType.php | 52 +++++++ src/EntityCollection.php | 4 +- src/EntityCollectionInterface.php | 5 +- src/Plugin/Deriver/EntityDeriver.php | 118 +++++++++++++++ src/Plugin/Deriver/ResourceDeriver.php | 82 ----------- .../JsonApiResourceTypeInterface.php} | 86 +++++------ src/Plugin/JsonApiResourceTypeManager.php | 36 +++++ src/Plugin/jsonapi/BundleJsonApiResource.php | 24 ---- .../resource_type/EntityJsonApiResourceType.php} | 56 +++++--- src/Routing/Routes.php | 160 +++++++++------------ 12 files changed, 361 insertions(+), 326 deletions(-) diff --git a/jsonapi.services.yml b/jsonapi.services.yml index 28f5b7d..5f88434 100644 --- a/jsonapi.services.yml +++ b/jsonapi.services.yml @@ -82,6 +82,9 @@ services: arguments: ['@entity.manager'] jsonapi.error_handler: class: Drupal\jsonapi\Error\ErrorHandler + plugin.manager.jsonapi.resource_type: + class: Drupal\jsonapi\Plugin\JsonApiResourceTypeManager + parent: default_plugin_manager jsonapi.exception_subscriber: class: Drupal\jsonapi\EventSubscriber\DefaultExceptionSubscriber tags: diff --git a/src/Annotation/JsonApiResource.php b/src/Annotation/JsonApiResource.php deleted file mode 100644 index 277de71..0000000 --- a/src/Annotation/JsonApiResource.php +++ /dev/null @@ -1,61 +0,0 @@ -entityTypeManager = $entity_type_manager; + $this->bundleManager = $bundle_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $container->get('entity_type.manager'), + $container->get('entity_type.bundle.info') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + if (isset($this->derivatives)) { + return $this->derivatives; + } + $this->derivatives = []; + + $entity_types = $this->entityTypeManager->getDefinitions(); + foreach ($entity_types as $entity_type_id => $entity_type) { + $plugin_definition = [ + 'serialization_class' => $entity_type->getClass(), + 'route_options' => [ + 'parameters' => [ + $entity_type_id => [ + 'type' => 'entity:' . $entity_type_id, + ], + ], + ], + 'route_path_part_for_individual_resource' => $entity_type_id, + ] + $base_plugin_definition; + + // First derive a JSON API Resource Type for this entity type (this covers + // all bundles). + $id = sprintf('%s', $entity_type_id); + $this->derivatives[$id] = [ + 'id' => $id, + 'type_hierarchy' => [$entity_type_id], + 'route_requirements' => [ + '_entity_type' => $entity_type_id, + ], + ] + $plugin_definition; + + // Now derive an additional JSON API Resource Type for every bundle that + // exists for this entity type. + // @todo ensure new bundles are picked up immediately, see \Drupal\Core\Entity\EntityTypeBundleInfo::clearCachedBundles(). + if ($entity_type->getBundleEntityType() !== NULL) { + $bundles = array_keys($this->bundleManager->getBundleInfo($entity_type_id)); + foreach ($bundles as $bundle) { + $id = sprintf('%s.%s', $entity_type_id, $bundle); + $this->derivatives[$id] = [ + 'id' => $id, + 'type_hierarchy' => [$entity_type_id, $bundle], + 'route_requirements' => [ + '_entity_type' => $entity_type_id, + '_bundle' => $bundle, + ], + ] + $plugin_definition; + } + } + } + return $this->derivatives; + } + +} diff --git a/src/Plugin/Deriver/ResourceDeriver.php b/src/Plugin/Deriver/ResourceDeriver.php deleted file mode 100644 index 3048b62..0000000 --- a/src/Plugin/Deriver/ResourceDeriver.php +++ /dev/null @@ -1,82 +0,0 @@ -resourceManager = $resource_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, $base_plugin_id) { - /* @var \Drupal\jsonapi\Configuration\ResourceManagerInterface $resource_manager */ - $resource_manager = $container->get('jsonapi.resource.manager'); - return new static($resource_manager); - } - - /** - * {@inheritdoc} - */ - public function getDerivativeDefinitions($base_definition) { - if (isset($this->derivatives)) { - return $this->derivatives; - } - $this->derivatives = []; - // Add in the default plugin configuration and the resource type. - /* @var \Drupal\jsonapi\Configuration\ResourceConfigInterface[] $resource_configs */ - $resource_configs = $this->resourceManager->all(); - foreach ($resource_configs as $resource) { - $global_config = $resource->getGlobalConfig(); - $prefix = $global_config->get('prefix'); - $id = sprintf('%s.dynamic.%s', $prefix, $resource->getTypeName()); - $this->derivatives[$id] = [ - 'id' => $id, - 'entityType' => $resource->getEntityTypeId(), - 'bundle' => $resource->getBundleId(), - 'hasBundle' => $this->resourceManager->hasBundle($resource->getEntityTypeId()), - 'type' => $resource->getTypeName(), - 'data' => [ - 'prefix' => $prefix, - 'partialPath' => '/' . $prefix . $resource->getPath() - ] - ]; - - $this->derivatives[$id] += $base_definition; - } - return $this->derivatives; - } - -} diff --git a/src/Resource/EntityResourceInterface.php b/src/Plugin/JsonApiResourceTypeInterface.php similarity index 55% rename from src/Resource/EntityResourceInterface.php rename to src/Plugin/JsonApiResourceTypeInterface.php index 4c42bd4..965bb0d 100644 --- a/src/Resource/EntityResourceInterface.php +++ b/src/Plugin/JsonApiResourceTypeInterface.php @@ -1,23 +1,23 @@ $namespaces['Drupal\jsonapi']]); + + parent::__construct('Plugin/jsonapi', $limited_namespaces, $module_handler, 'Drupal\jsonapi\Plugin\JsonApiResourceInterface', 'Drupal\jsonapi\Annotation\JsonApiResourceType'); + $this->setCacheBackend($cache_backend, 'jsonapi_resource_type_plugins'); + } + +} diff --git a/src/Plugin/jsonapi/BundleJsonApiResource.php b/src/Plugin/jsonapi/BundleJsonApiResource.php deleted file mode 100644 index bf97da3..0000000 --- a/src/Plugin/jsonapi/BundleJsonApiResource.php +++ /dev/null @@ -1,24 +0,0 @@ -access('view', NULL, TRUE); if (!$entity_access->isAllowed()) { throw new SerializableHttpException(403, 'The current user is not allowed to GET the selected resource.'); @@ -121,7 +126,8 @@ class EntityResource implements EntityResourceInterface { * @throws \Drupal\jsonapi\Error\SerializableHttpException * If validation errors are found. */ - protected function validate(EntityInterface $entity) { + protected function validate($entity) { + assert($entity instanceof EntityInterface); if (!$entity instanceof FieldableEntityInterface) { return; } @@ -148,7 +154,8 @@ class EntityResource implements EntityResourceInterface { /** * {@inheritdoc} */ - public function createIndividual(EntityInterface $entity, Request $request) { + public function createIndividual($entity, Request $request) { + assert($entity instanceof EntityInterface); $entity_access = $entity->access('create', NULL, TRUE); if (!$entity_access->isAllowed()) { @@ -162,7 +169,8 @@ class EntityResource implements EntityResourceInterface { /** * {@inheritdoc} */ - public function patchIndividual(EntityInterface $entity, EntityInterface $parsed_entity, Request $request) { + public function patchIndividual($entity, EntityInterface $parsed_entity, Request $request) { + assert($entity instanceof EntityInterface); $entity_access = $entity->access('update', NULL, TRUE); if (!$entity_access->isAllowed()) { throw new SerializableHttpException(403, 'The current user is not allowed to GET the selected resource.'); @@ -192,7 +200,8 @@ class EntityResource implements EntityResourceInterface { /** * {@inheritdoc} */ - public function deleteIndividual(EntityInterface $entity, Request $request) { + public function deleteIndividual($entity, Request $request) { + assert($entity instanceof EntityInterface); $entity_access = $entity->access('delete', NULL, TRUE); if (!$entity_access->isAllowed()) { throw new SerializableHttpException(403, 'The current user is not allowed to DELETE the selected resource.'); @@ -242,7 +251,8 @@ class EntityResource implements EntityResourceInterface { /** * {@inheritdoc} */ - public function getRelated(EntityInterface $entity, $related_field, Request $request) { + public function getRelated($entity, $related_field, Request $request) { + assert($entity instanceof EntityInterface); /* @var $field_list \Drupal\Core\Field\FieldItemListInterface */ if (!($field_list = $entity->get($related_field)) || !$this->isRelationshipField($field_list)) { throw new SerializableHttpException(404, sprintf('The relationship %s is not present in this resource.', $related_field)); @@ -266,7 +276,8 @@ class EntityResource implements EntityResourceInterface { /** * {@inheritdoc} */ - public function getRelationship(EntityInterface $entity, $related_field, Request $request, $response_code = 200) { + public function getRelationship($entity, $related_field, Request $request, $response_code = 200) { + assert($entity instanceof EntityInterface); if (!($field_list = $entity->get($related_field)) || !$this->isRelationshipField($field_list)) { throw new SerializableHttpException(404, sprintf('The relationship %s is not present in this resource.', $related_field)); } @@ -277,7 +288,8 @@ class EntityResource implements EntityResourceInterface { /** * {@inheritdoc} */ - public function createRelationship(EntityInterface $entity, $related_field, $parsed_field_list, Request $request) { + public function createRelationship($entity, $related_field, $parsed_field_list, Request $request) { + assert($entity instanceof EntityInterface); if ($parsed_field_list instanceof Response) { // This usually means that there was an error, so there is no point on // processing further. @@ -312,7 +324,8 @@ class EntityResource implements EntityResourceInterface { /** * {@inheritdoc} */ - public function patchRelationship(EntityInterface $entity, $related_field, $parsed_field_list, Request $request) { + public function patchRelationship($entity, $related_field, $parsed_field_list, Request $request) { + assert($entity instanceof EntityInterface); if ($parsed_field_list instanceof Response) { // This usually means that there was an error, so there is no point on // processing further. @@ -371,7 +384,8 @@ class EntityResource implements EntityResourceInterface { /** * {@inheritdoc} */ - public function deleteRelationship(EntityInterface $entity, $related_field, $parsed_field_list, Request $request) { + public function deleteRelationship($entity, $related_field, $parsed_field_list, Request $request) { + assert($entity instanceof EntityInterface); if ($parsed_field_list instanceof Response) { // This usually means that there was an error, so there is no point on // processing further. diff --git a/src/Routing/Routes.php b/src/Routing/Routes.php index f2f1176..606163c 100644 --- a/src/Routing/Routes.php +++ b/src/Routing/Routes.php @@ -7,7 +7,7 @@ use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\EntityReferenceFieldItemList; -use Drupal\jsonapi\Plugin\JsonApiResourceManager; +use Drupal\jsonapi\Plugin\JsonApiResourceTypeManager; use Drupal\jsonapi\Resource\DocumentWrapperInterface; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -29,6 +29,11 @@ class Routes implements ContainerInjectionInterface { const FRONT_CONTROLLER = '\Drupal\jsonapi\RequestHandler::handle'; /** + * @var \Drupal\jsonapi\Plugin\JsonApiResourceTypeManager + */ + protected $jsonApiResourceTypeManager; + + /** * The entity type manager. * * @var \Drupal\Core\Entity\EntityTypeManagerInterface @@ -66,7 +71,8 @@ class Routes implements ContainerInjectionInterface { * @param \Drupal\Core\Authentication\AuthenticationCollectorInterface $auth_collector * The authentication collector. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_manager, AuthenticationCollectorInterface $auth_collector) { + public function __construct(JsonApiResourceTypeManager $json_api_resource_type_manager, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_manager, AuthenticationCollectorInterface $auth_collector) { + $this->jsonApiResourceTypeManager = $json_api_resource_type_manager; $this->entityTypeManager = $entity_type_manager; $this->bundleManager = $bundle_manager; $this->authCollector = $auth_collector; @@ -77,6 +83,7 @@ class Routes implements ContainerInjectionInterface { */ public static function create(ContainerInterface $container) { return new static( + $container->get('plugin.manager.jsonapi.resource_type'), $container->get('entity_type.manager'), $container->get('entity_type.bundle.info'), $container->get('authentication_collector') @@ -89,93 +96,68 @@ class Routes implements ContainerInjectionInterface { public function routes() { $collection = new RouteCollection(); - $resources = []; - $entity_type_ids = array_keys($this->entityTypeManager->getDefinitions()); - foreach ($entity_type_ids as $entity_type_id) { - $bundles = array_keys($this->bundleManager->getBundleInfo($entity_type_id)); - // Also generate a route for the entity type, i.e. for all bundles. - array_unshift($bundles, NULL); - foreach ($bundles as $bundle) { - $route_name_prefix = $bundle - ? sprintf('jsonapi.%s.%s.', $entity_type_id, $bundle) - : sprintf('jsonapi.%s.', $entity_type_id); - - $path_prefix = $bundle - ? sprintf('/api/%s/%s', $entity_type_id, $bundle) - : sprintf('/api/%s', $entity_type_id); - - // Add the collection route. - $defaults = [ - RouteObjectInterface::CONTROLLER_NAME => static::FRONT_CONTROLLER, - ]; - // Options that apply to all routes. - $options = [ - '_auth' => $this->authProviderList(), - '_is_jsonapi' => TRUE, - ]; - - // Collection endpoint, like /api/file/photo. - $route_collection = (new Route($path_prefix)) - ->addDefaults($defaults) - ->setRequirement('_entity_type', $entity_type_id) - ->setRequirement('_format', 'api_json') - ->setRequirement('_custom_parameter_names', 'TRUE') - ->setOption('serialization_class', DocumentWrapperInterface::class) - ->setMethods(['GET', 'POST']); - if ($bundle) { - $route_collection->setRequirement('_bundle', $bundle); - } - $route_collection->addOptions($options); - $collection->add($route_name_prefix . 'collection', $route_collection); - - // Individual endpoint, like /api/file/photo/123. - $parameters = [$entity_type_id => ['type' => 'entity:' . $entity_type_id]]; - $route_individual = (new Route(sprintf('%s/{%s}', $path_prefix, $entity_type_id))) - ->addDefaults($defaults) - ->setRequirement('_entity_type', $entity_type_id) - ->setRequirement('_format', 'api_json') - ->setRequirement('_custom_parameter_names', 'TRUE') - ->setOption('parameters', $parameters) - ->setOption('_auth', $this->authProviderList()) - ->setOption('serialization_class', DocumentWrapperInterface::class) - ->setMethods(['GET', 'PATCH', 'DELETE']); - if ($bundle) { - $route_individual->setRequirement('_bundle', $bundle); - } - $route_individual->addOptions($options); - $collection->add($route_name_prefix . 'individual', $route_individual); - - // Related resource, like /api/file/photo/123/comments. - $route_related = (new Route(sprintf('%s/{%s}/{related}', $path_prefix, $entity_type_id))) - ->addDefaults($defaults) - ->setRequirement('_entity_type', $entity_type_id) - ->setRequirement('_format', 'api_json') - ->setRequirement('_custom_parameter_names', 'TRUE') - ->setOption('parameters', $parameters) - ->setOption('_auth', $this->authProviderList()) - ->setMethods(['GET']); - if ($bundle) { - $route_related->setRequirement('_bundle', $bundle); - } - $route_related->addOptions($options); - $collection->add($route_name_prefix . 'related', $route_related); - - // Related endpoint, like /api/file/photo/123/relationships/comments. - $route_relationship = (new Route(sprintf('%s/{%s}/relationships/{related}', $path_prefix, $entity_type_id))) - ->addDefaults($defaults + ['_on_relationship' => TRUE]) - ->setRequirement('_entity_type', $entity_type_id) - ->setRequirement('_format', 'api_json') - ->setRequirement('_custom_parameter_names', 'TRUE') - ->setOption('parameters', $parameters) - ->setOption('_auth', $this->authProviderList()) - ->setOption('serialization_class', EntityReferenceFieldItemList::class) - ->setMethods(['GET', 'POST', 'PATCH', 'DELETE']); - if ($bundle) { - $route_relationship->setRequirement('_bundle', $bundle); - } - $route_relationship->addOptions($options); - $collection->add($route_name_prefix . 'relationship', $route_relationship); - } + foreach ($this->jsonApiResourceTypeManager->getDefinitions() as $id => $definition) { + $route_name_prefix = 'jsonapi.' . implode('.', $definition['type_hierarchy']); + $path_prefix = '/api/' . implode('/', $definition['type_hierarchy']); + + // Add the collection route. + $defaults = [ + RouteObjectInterface::CONTROLLER_NAME => static::FRONT_CONTROLLER, + ]; + // Options that apply to all routes. + $options = [ + '_auth' => $this->authProviderList(), + '_is_jsonapi' => TRUE, + ]; + + // Collection endpoint, like /api/file/photo. + $route_collection = (new Route($path_prefix)) + ->addDefaults($defaults) + ->addRequirements($definition['route_requirements']) + ->setRequirement('_format', 'api_json') + ->setRequirement('_custom_parameter_names', 'TRUE') + ->setOption('serialization_class', DocumentWrapperInterface::class) + ->setMethods(['GET', 'POST']); + $route_collection->addOptions($options); + $collection->add($route_name_prefix . '.collection', $route_collection); + + // Individual endpoint, like /api/file/photo/123. + $route_individual = (new Route(sprintf('%s/{%s}', $path_prefix, $definition['route_path_part_for_individual_resource']))) + ->addDefaults($defaults) + ->addRequirements($definition['route_requirements']) + ->setRequirement('_format', 'api_json') + ->setRequirement('_custom_parameter_names', 'TRUE') + ->addOptions($definition['route_options']) + ->setOption('_auth', $this->authProviderList()) + ->setOption('serialization_class', DocumentWrapperInterface::class) + ->setMethods(['GET', 'PATCH', 'DELETE']); + $route_individual->addOptions($options); + $collection->add($route_name_prefix . '.individual', $route_individual); + + // Related resource, like /api/file/photo/123/comments. + $route_related = (new Route(sprintf('%s/{%s}/{related}', $path_prefix, $definition['route_path_part_for_individual_resource']))) + ->addDefaults($defaults) + ->addRequirements($definition['route_requirements']) + ->setRequirement('_format', 'api_json') + ->setRequirement('_custom_parameter_names', 'TRUE') + ->addOptions($definition['route_options']) + ->setOption('_auth', $this->authProviderList()) + ->setMethods(['GET']); + $route_related->addOptions($options); + $collection->add($route_name_prefix . '.related', $route_related); + + // Related endpoint, like /api/file/photo/123/relationships/comments. + $route_relationship = (new Route(sprintf('%s/{%s}/relationships/{related}', $path_prefix, $definition['route_path_part_for_individual_resource']))) + ->addDefaults($defaults + ['_on_relationship' => TRUE]) + ->addRequirements($definition['route_requirements']) + ->setRequirement('_format', 'api_json') + ->setRequirement('_custom_parameter_names', 'TRUE') + ->addOptions($definition['route_options']) + ->setOption('_auth', $this->authProviderList()) + ->setOption('serialization_class', EntityReferenceFieldItemList::class) + ->setMethods(['GET', 'POST', 'PATCH', 'DELETE']); + $route_relationship->addOptions($options); + $collection->add($route_name_prefix . '.relationship', $route_relationship); } return $collection;