View source
<?php
namespace Drupal\jsonapi\Routing;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\jsonapi\Access\RelationshipFieldAccess;
use Drupal\jsonapi\Controller\EntryPoint;
use Drupal\jsonapi\ParamConverter\ResourceTypeConverter;
use Drupal\jsonapi\ResourceType\ResourceType;
use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
class Routes implements ContainerInjectionInterface {
const CONTROLLER_SERVICE_NAME = 'jsonapi.entity_resource';
const JSON_API_ROUTE_FLAG_KEY = '_is_jsonapi';
const RESOURCE_TYPE_KEY = 'resource_type';
protected $resourceTypeRepository;
protected $providerIds;
protected $jsonApiBasePath;
public function __construct(ResourceTypeRepositoryInterface $resource_type_repository, array $authentication_providers, $jsonapi_base_path) {
$this->resourceTypeRepository = $resource_type_repository;
$this->providerIds = array_keys($authentication_providers);
assert(is_string($jsonapi_base_path));
assert($jsonapi_base_path[0] === '/', sprintf('The provided base path should contain a leading slash "/". Given: "%s".', $jsonapi_base_path));
assert(substr($jsonapi_base_path, -1) !== '/', sprintf('The provided base path should not contain a trailing slash "/". Given: "%s".', $jsonapi_base_path));
$this->jsonApiBasePath = $jsonapi_base_path;
}
public static function create(ContainerInterface $container) {
return new static($container
->get('jsonapi.resource_type.repository'), $container
->getParameter('authentication_providers'), $container
->getParameter('jsonapi.base_path'));
}
public function routes() {
$routes = new RouteCollection();
$upload_routes = new RouteCollection();
foreach ($this->resourceTypeRepository
->all() as $resource_type) {
$routes
->addCollection(static::getRoutesForResourceType($resource_type, $this->jsonApiBasePath));
$upload_routes
->addCollection(static::getFileUploadRoutesForResourceType($resource_type, $this->jsonApiBasePath));
}
$routes
->add('jsonapi.resource_list', static::getEntryPointRoute($this->jsonApiBasePath));
$routes
->addRequirements([
'_content_type_format' => 'api_json',
]);
$upload_routes
->addRequirements([
'_content_type_format' => 'bin',
]);
$routes
->addCollection($upload_routes);
$routes
->addOptions([
'_auth' => $this->providerIds,
]);
$routes
->addDefaults([
static::JSON_API_ROUTE_FLAG_KEY => TRUE,
]);
$routes
->addRequirements([
'_format' => 'api_json',
]);
return $routes;
}
protected static function getRoutesForResourceType(ResourceType $resource_type, $path_prefix) {
if ($resource_type
->isInternal()) {
return new RouteCollection();
}
$routes = new RouteCollection();
if ($resource_type
->isLocatable()) {
$collection_route = new Route("/{$resource_type->getPath()}");
$collection_route
->addDefaults([
RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ':getCollection',
]);
$collection_route
->setMethods([
'GET',
]);
$collection_route
->setRequirement('_access', 'TRUE');
$routes
->add(static::getRouteName($resource_type, 'collection'), $collection_route);
}
if ($resource_type
->isMutable()) {
$collection_create_route = new Route("/{$resource_type->getPath()}");
$collection_create_route
->addDefaults([
RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ':createIndividual',
]);
$collection_create_route
->setMethods([
'POST',
]);
$create_requirement = sprintf("%s:%s", $resource_type
->getEntityTypeId(), $resource_type
->getBundle());
$collection_create_route
->setRequirement('_entity_create_access', $create_requirement);
$collection_create_route
->setRequirement('_csrf_request_header_token', 'TRUE');
$routes
->add(static::getRouteName($resource_type, 'collection.post'), $collection_create_route);
}
$routes
->addCollection(static::getIndividualRoutesForResourceType($resource_type));
foreach ($routes as $route) {
static::addRouteParameter($route, static::RESOURCE_TYPE_KEY, [
'type' => ResourceTypeConverter::PARAM_TYPE_ID,
]);
$route
->addDefaults([
static::RESOURCE_TYPE_KEY => $resource_type
->getTypeName(),
]);
}
$routes
->addPrefix($path_prefix);
return $routes;
}
protected static function getFileUploadRoutesForResourceType(ResourceType $resource_type, $path_prefix) {
$routes = new RouteCollection();
if ($resource_type
->isInternal() || !$resource_type
->isLocatable()) {
return $routes;
}
$has_file_field = array_reduce($resource_type
->getRelatableResourceTypes(), function ($carry, array $target_resource_types) {
return $carry || static::hasNonInternalFileTargetResourceTypes($target_resource_types);
}, FALSE);
if (!$has_file_field) {
return $routes;
}
if ($resource_type
->isMutable()) {
$path = $resource_type
->getPath();
$entity_type_id = $resource_type
->getEntityTypeId();
$new_resource_file_upload_route = new Route("/{$path}/{file_field_name}");
$new_resource_file_upload_route
->addDefaults([
RouteObjectInterface::CONTROLLER_NAME => 'jsonapi.file_upload:handleFileUploadForNewResource',
]);
$new_resource_file_upload_route
->setMethods([
'POST',
]);
$new_resource_file_upload_route
->setRequirement('_csrf_request_header_token', 'TRUE');
$routes
->add(static::getFileUploadRouteName($resource_type, 'new_resource'), $new_resource_file_upload_route);
$existing_resource_file_upload_route = new Route("/{$path}/{entity}/{file_field_name}");
$existing_resource_file_upload_route
->addDefaults([
RouteObjectInterface::CONTROLLER_NAME => 'jsonapi.file_upload:handleFileUploadForExistingResource',
]);
$existing_resource_file_upload_route
->setMethods([
'POST',
]);
$existing_resource_file_upload_route
->setRequirement('_csrf_request_header_token', 'TRUE');
$routes
->add(static::getFileUploadRouteName($resource_type, 'existing_resource'), $existing_resource_file_upload_route);
$routes
->addOptions([
'parameters' => [
'entity' => [
'type' => 'entity:' . $entity_type_id,
],
],
]);
foreach ($routes as $route) {
static::addRouteParameter($route, static::RESOURCE_TYPE_KEY, [
'type' => ResourceTypeConverter::PARAM_TYPE_ID,
]);
$route
->addDefaults([
static::RESOURCE_TYPE_KEY => $resource_type
->getTypeName(),
]);
}
}
$routes
->addPrefix($path_prefix);
return $routes;
}
public static function isJsonApiRequest(array $defaults) {
return isset($defaults[RouteObjectInterface::CONTROLLER_NAME]) && strpos($defaults[RouteObjectInterface::CONTROLLER_NAME], static::CONTROLLER_SERVICE_NAME) === 0;
}
protected static function getIndividualRoutesForResourceType(ResourceType $resource_type) {
if (!$resource_type
->isLocatable()) {
return new RouteCollection();
}
$routes = new RouteCollection();
$path = $resource_type
->getPath();
$entity_type_id = $resource_type
->getEntityTypeId();
$individual_route = new Route("/{$path}/{entity}");
$individual_route
->addDefaults([
RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ':getIndividual',
]);
$individual_route
->setMethods([
'GET',
]);
$individual_route
->setRequirement('_access', 'TRUE');
$routes
->add(static::getRouteName($resource_type, 'individual'), $individual_route);
if ($resource_type
->isMutable()) {
$individual_update_route = new Route($individual_route
->getPath());
$individual_update_route
->addDefaults([
RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ':patchIndividual',
]);
$individual_update_route
->setMethods([
'PATCH',
]);
$individual_update_route
->setRequirement('_entity_access', "entity.update");
$individual_update_route
->setRequirement('_csrf_request_header_token', 'TRUE');
$routes
->add(static::getRouteName($resource_type, 'individual.patch'), $individual_update_route);
$individual_remove_route = new Route($individual_route
->getPath());
$individual_remove_route
->addDefaults([
RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ':deleteIndividual',
]);
$individual_remove_route
->setMethods([
'DELETE',
]);
$individual_remove_route
->setRequirement('_entity_access', "entity.delete");
$individual_remove_route
->setRequirement('_csrf_request_header_token', 'TRUE');
$routes
->add(static::getRouteName($resource_type, 'individual.delete'), $individual_remove_route);
}
foreach ($resource_type
->getRelatableResourceTypes() as $relationship_field_name => $target_resource_types) {
$relationship_route = new Route("/{$path}/{entity}/relationships/{$relationship_field_name}");
$relationship_route
->addDefaults([
'_on_relationship' => TRUE,
]);
$relationship_route
->addDefaults([
'related' => $relationship_field_name,
]);
$relationship_route
->setRequirement(RelationshipFieldAccess::ROUTE_REQUIREMENT_KEY, $relationship_field_name);
$relationship_route
->setRequirement('_csrf_request_header_token', 'TRUE');
$relationship_route_methods = $resource_type
->isMutable() ? [
'GET',
'POST',
'PATCH',
'DELETE',
] : [
'GET',
];
$relationship_controller_methods = [
'GET' => 'getRelationship',
'POST' => 'addToRelationshipData',
'PATCH' => 'replaceRelationshipData',
'DELETE' => 'removeFromRelationshipData',
];
foreach ($relationship_route_methods as $method) {
$method_specific_relationship_route = clone $relationship_route;
$method_specific_relationship_route
->addDefaults([
RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ":{$relationship_controller_methods[$method]}",
]);
$method_specific_relationship_route
->setMethods($method);
$routes
->add(static::getRouteName($resource_type, sprintf("%s.relationship.%s", $relationship_field_name, strtolower($method))), $method_specific_relationship_route);
}
if (static::hasNonInternalTargetResourceTypes($target_resource_types)) {
$related_route = new Route("/{$path}/{entity}/{$relationship_field_name}");
$related_route
->setMethods([
'GET',
]);
$related_route
->addDefaults([
RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ':getRelated',
]);
$related_route
->addDefaults([
'related' => $relationship_field_name,
]);
$related_route
->setRequirement(RelationshipFieldAccess::ROUTE_REQUIREMENT_KEY, $relationship_field_name);
$routes
->add(static::getRouteName($resource_type, "{$relationship_field_name}.related"), $related_route);
}
}
$routes
->addOptions([
'parameters' => [
'entity' => [
'type' => 'entity:' . $entity_type_id,
],
],
]);
return $routes;
}
protected function getEntryPointRoute($path_prefix) {
$entry_point = new Route("/{$path_prefix}");
$entry_point
->addDefaults([
RouteObjectInterface::CONTROLLER_NAME => EntryPoint::class . '::index',
]);
$entry_point
->setRequirement('_access', 'TRUE');
$entry_point
->setMethods([
'GET',
]);
return $entry_point;
}
protected static function addRouteParameter(Route $route, $name, $parameter) {
$parameters = $route
->getOption('parameters') ?: [];
$parameters[$name] = $parameter;
$route
->setOption('parameters', $parameters);
}
public static function getRouteName(ResourceType $resource_type, $route_type) {
return sprintf('jsonapi.%s.%s', $resource_type
->getTypeName(), $route_type);
}
protected static function getFileUploadRouteName(ResourceType $resource_type, $route_type) {
return sprintf('jsonapi.%s.%s.%s', $resource_type
->getTypeName(), 'file_upload', $route_type);
}
protected static function hasNonInternalTargetResourceTypes(array $resource_types) {
return array_reduce($resource_types, function ($carry, ResourceType $target) {
return $carry || !$target
->isInternal();
}, FALSE);
}
protected static function hasNonInternalFileTargetResourceTypes(array $resource_types) {
return array_reduce($resource_types, function ($carry, ResourceType $target) {
return $carry || !$target
->isInternal() && $target
->getEntityTypeId() === 'file';
}, FALSE);
}
public static function getResourceTypeNameFromParameters(array $parameters) {
if (isset($parameters[static::JSON_API_ROUTE_FLAG_KEY]) && $parameters[static::JSON_API_ROUTE_FLAG_KEY]) {
return isset($parameters[static::RESOURCE_TYPE_KEY]) ? $parameters[static::RESOURCE_TYPE_KEY] : NULL;
}
return NULL;
}
public static function rebuild() {
\Drupal::service('cache_tags.invalidator')
->invalidateTags([
'jsonapi_resource_types',
]);
\Drupal::service('router.builder')
->setRebuildNeeded();
}
}