diff --git a/core/core.services.yml b/core/core.services.yml index c55526383e3945bcd665af7a4c4265769f7e4d27..75198f28dd19958f252df030a8a11a9ba0925a50 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -311,6 +311,11 @@ services: parent: container.trait tags: - { name: plugin_manager_cache_clear } + entity_route_subscriber: + class: Drupal\Core\EventSubscriber\EntityRouteProviderSubscriber + arguments: ['@entity.manager'] + tags: + - { name: event_subscriber } entity.definitions.installed: class: Drupal\Core\KeyValueStore\KeyValueStoreInterface factory_method: get diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php index df404d8330468bd128f944a081818d3d03392282..06fb6a357ede587955bba46c47a26fcb7f6f24f6 100644 --- a/core/lib/Drupal/Core/Entity/EntityManager.php +++ b/core/lib/Drupal/Core/Entity/EntityManager.php @@ -300,6 +300,20 @@ public function getFormObject($entity_type, $operation) { return $this->handlers['form'][$operation][$entity_type]; } + /** + * {@inheritdoc} + */ + public function getRouteProviders($entity_type) { + if (!isset($this->handlers['route_provider'][$entity_type])) { + $route_provider_classes = $this->getDefinition($entity_type, TRUE)->getRouteProviderClasses(); + + foreach ($route_provider_classes as $type => $class) { + $this->handlers['route_provider'][$entity_type][$type] = $this->createHandlerInstance($class, $this->getDefinition($entity_type)); + } + } + return isset($this->handlers['route_provider'][$entity_type]) ? $this->handlers['route_provider'][$entity_type] : []; + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Entity/EntityManagerInterface.php b/core/lib/Drupal/Core/Entity/EntityManagerInterface.php index 13f9862d5099d2a77ee73c6c5cb6fd345a41b112..d60c47d7366d7396330b9cf5f493e59e8f5364fb 100644 --- a/core/lib/Drupal/Core/Entity/EntityManagerInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityManagerInterface.php @@ -223,6 +223,16 @@ public function getListBuilder($entity_type); */ public function getFormObject($entity_type, $operation); + /** + * Gets all route provider instances. + * + * @param string $entity_type + * The entity type for this route providers. + * + * @return \Drupal\Core\Entity\Routing\EntityRouteProviderInterface[] + */ + public function getRouteProviders($entity_type); + /** * Checks whether a certain entity type has a certain handler. * diff --git a/core/lib/Drupal/Core/Entity/EntityType.php b/core/lib/Drupal/Core/Entity/EntityType.php index c5d90ca3ec03eb8a02ef002f67fe7c6f1f5eec67..9c99d0a513711a688bfff4027080703cf0119ec6 100644 --- a/core/lib/Drupal/Core/Entity/EntityType.php +++ b/core/lib/Drupal/Core/Entity/EntityType.php @@ -433,6 +433,13 @@ public function hasFormClasses() { return !empty($this->handlers['form']); } + /** + * {@inheritdoc} + */ + public function hasRouteProviders() { + return !empty($this->handlers['route_provider']); + } + /** * {@inheritdoc} */ @@ -477,6 +484,13 @@ public function hasViewBuilderClass() { return $this->hasHandlerClass('view_builder'); } + /** + * {@inheritdoc} + */ + public function getRouteProviderClasses() { + return !empty($this->handlers['route_provider']) ? $this->handlers['route_provider'] : []; + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php index e2ba2a9a037d054ea709118da430c0a0c17b58fb..7a87db93ad3eb052350b9cea4a2f2054e84a2c4f 100644 --- a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php @@ -225,6 +225,10 @@ public function getHandlerClass($handler_type); * - access: The name of the class that is used for access checks. The class * must implement \Drupal\Core\Entity\EntityAccessControlHandlerInterface. * Defaults to \Drupal\Core\Entity\EntityAccessControlHandler. + * - route_provider: (optional) A list of class names, keyed by a group + * string, which will be used to define routes related to this entity + * type. These classes must implement + * \Drupal\Core\Entity\Routing\EntityRouteProviderInterface. */ public function getHandlerClasses(); @@ -282,6 +286,22 @@ public function setFormClass($operation, $class); */ public function hasFormClasses(); + /** + * Indicates if this entity type has any route provider. + * + * @return bool + */ + public function hasRouteProviders(); + + /** + * Gets all the route provide handlers. + * + * Much like forms you can define multiple route provider handlers. + * + * @return string[] + */ + public function getRouteProviderClasses(); + /** * Returns the list class. * diff --git a/core/lib/Drupal/Core/Entity/Routing/EntityRouteProviderInterface.php b/core/lib/Drupal/Core/Entity/Routing/EntityRouteProviderInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..a7daa9463e4f0664e166ec27864c5f1c34f11c62 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Routing/EntityRouteProviderInterface.php @@ -0,0 +1,29 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Entity\Routing\EntityRouteProviderInterface. + */ + +namespace Drupal\Core\Entity\Routing; + +use Drupal\Core\Entity\EntityTypeInterface; + +/** + * Allows entity types to provide routes. + */ +interface EntityRouteProviderInterface { + + /** + * Provides routes for entities. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type + * + * @return \Symfony\Component\Routing\RouteCollection|\Symfony\Component\Routing\Route[] + * Returns a route collection or an array of routes keyed by name, like + * route_callbacks inside 'routing.yml' files. + */ + public function getRoutes(EntityTypeInterface $entity_type); + +} diff --git a/core/lib/Drupal/Core/EventSubscriber/EntityRouteProviderSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/EntityRouteProviderSubscriber.php new file mode 100644 index 0000000000000000000000000000000000000000..d5c4def45017314c1cb9ce64313734755382bd1c --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/EntityRouteProviderSubscriber.php @@ -0,0 +1,74 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\EventSubscriber\EntityRouteProviderSubscriber. + */ + +namespace Drupal\Core\EventSubscriber; + +use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Routing\RouteBuildEvent; +use Drupal\Core\Routing\RoutingEvents; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Routing\RouteCollection; + +/** + * Ensures that routes can be provided by entity types. + */ +class EntityRouteProviderSubscriber implements EventSubscriberInterface { + + /** + * The entity manager. + * + * @var \Drupal\Core\Entity\EntityManagerInterface + */ + protected $entityManager; + + /** + * Constructs a new EntityRouteProviderSubscriber instance. + * + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager + * The entity manager. + */ + public function __construct(EntityManagerInterface $entity_manager) { + $this->entityManager = $entity_manager; + } + + /** + * Provides routes on route rebuild time. + * + * @param \Drupal\Core\Routing\RouteBuildEvent $event + * The route build event. + */ + public function onDynamicRouteEvent(RouteBuildEvent $event) { + $route_collection = $event->getRouteCollection(); + foreach ($this->entityManager->getDefinitions() as $entity_type) { + if ($entity_type->hasRouteProviders()) { + foreach ($this->entityManager->getRouteProviders($entity_type->id()) as $route_provider) { + // Allow to both return an array of routes or a route collection, + // like route_callbacks in the routing.yml file. + $routes = $route_provider->getRoutes($entity_type); + if ($routes instanceof RouteCollection) { + $route_collection->addCollection($routes); + } + elseif (is_array($routes)) { + foreach ($routes as $route_name => $route) { + $route_collection->add($route_name, $route); + } + } + } + } + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[RoutingEvents::DYNAMIC][] = ['onDynamicRouteEvent']; + return $events; + } + +} + diff --git a/core/modules/node/node.routing.yml b/core/modules/node/node.routing.yml index bb431fb4490e43e3dac6c2a7288afff550f9132d..3510669f63eef841abf5104247d4dcb360891ad3 100644 --- a/core/modules/node/node.routing.yml +++ b/core/modules/node/node.routing.yml @@ -5,15 +5,6 @@ node.multiple_delete_confirm: requirements: _permission: 'administer nodes' -entity.node.edit_form: - path: '/node/{node}/edit' - defaults: - _entity_form: 'node.edit' - requirements: - _entity_access: 'node.update' - options: - _node_operation_route: TRUE - node.add_page: path: '/node/add' defaults: @@ -48,24 +39,6 @@ entity.node.preview: node_preview: type: 'node_preview' -entity.node.canonical: - path: '/node/{node}' - defaults: - _controller: '\Drupal\node\Controller\NodeViewController::view' - _title_callback: '\Drupal\node\Controller\NodeViewController::title' - requirements: - _entity_access: 'node.view' - -entity.node.delete_form: - path: '/node/{node}/delete' - defaults: - _entity_form: 'node.delete' - _title: 'Delete' - requirements: - _entity_access: 'node.delete' - options: - _node_operation_route: TRUE - entity.node.version_history: path: '/node/{node}/revisions' defaults: diff --git a/core/modules/node/src/Entity/Node.php b/core/modules/node/src/Entity/Node.php index ff4f376681ac162992b784649fc299eb5243bc47..4e7dc84a49a4d797b86e904520b3671ab999bd2a 100644 --- a/core/modules/node/src/Entity/Node.php +++ b/core/modules/node/src/Entity/Node.php @@ -34,6 +34,9 @@ * "delete" = "Drupal\node\Form\NodeDeleteForm", * "edit" = "Drupal\node\NodeForm" * }, + * "route_provider" = { + * "html" = "Drupal\node\Entity\NodeRouteProvider", + * }, * "list_builder" = "Drupal\node\NodeListBuilder", * "translation" = "Drupal\node\NodeTranslationHandler" * }, diff --git a/core/modules/node/src/Entity/NodeRouteProvider.php b/core/modules/node/src/Entity/NodeRouteProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..fa1c02d7fc359e57c8acdd3ebef5eed824d7424d --- /dev/null +++ b/core/modules/node/src/Entity/NodeRouteProvider.php @@ -0,0 +1,51 @@ +<?php + +/** + * @file + * Contains \Drupal\node\Entity\NodeRouteProvider. + */ + +namespace Drupal\node\Entity; + +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\Routing\EntityRouteProviderInterface; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * Provides routes for nodes. + */ +class NodeRouteProvider implements EntityRouteProviderInterface { + + /** + * {@inheritdoc} + */ + public function getRoutes( EntityTypeInterface $entity_type) { + $route_collection = new RouteCollection(); + $route = (new Route('/node/{node}')) + ->addDefaults([ + '_controller' => '\Drupal\node\Controller\NodeViewController::view', + '_title_callback' => '\Drupal\node\Controller\NodeViewController::title', + ]) + ->setRequirement('_entity_access', 'node.view'); + $route_collection->add('entity.node.canonical', $route); + + $route = (new Route('/node/{node}/delete')) + ->addDefaults([ + '_entity_form' => 'node.delete', + '_title' => 'Delete', + ]) + ->setRequirement('_entity_access', 'node.delete') + ->setOption('_node_operation_route', TRUE); + $route_collection->add('entity.node.delete_form', $route); + + $route = (new Route('/node/{node}/edit')) + ->setDefault('_entity_form', 'node.edit') + ->setRequirement('_entity_access', 'node.update') + ->setOption('_node_operation_route', TRUE); + $route_collection->add('entity.node.edit_form', $route); + + return $route_collection; + } + +} diff --git a/core/modules/user/src/Entity/User.php b/core/modules/user/src/Entity/User.php index 2ac5fec2aea2bcfe3749e66a8a7c9cb94e321a10..a32298e64430bb365a40cb7f87b33be2af9a74cf 100644 --- a/core/modules/user/src/Entity/User.php +++ b/core/modules/user/src/Entity/User.php @@ -30,6 +30,9 @@ * "list_builder" = "Drupal\user\UserListBuilder", * "view_builder" = "Drupal\Core\Entity\EntityViewBuilder", * "views_data" = "Drupal\user\UserViewsData", + * "route_provider" = { + * "html" = "Drupal\user\Entity\UserRouteProvider", + * }, * "form" = { * "default" = "Drupal\user\ProfileForm", * "cancel" = "Drupal\user\Form\UserCancelForm", diff --git a/core/modules/user/src/Entity/UserRouteProvider.php b/core/modules/user/src/Entity/UserRouteProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..f4cfd792866581e979d6d1f5884517fefdefb14b --- /dev/null +++ b/core/modules/user/src/Entity/UserRouteProvider.php @@ -0,0 +1,54 @@ +<?php + +/** + * @file + * Contains \Drupal\user\Entity\UserRouteProvider. + */ + +namespace Drupal\user\Entity; + +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\Routing\EntityRouteProviderInterface; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * Provides routes for the user entity. + */ +class UserRouteProvider implements EntityRouteProviderInterface { + + /** + * {@inheritdoc} + */ + public function getRoutes(EntityTypeInterface $entity_type) { + $route_collection = new RouteCollection(); + $route = (new Route('/user/{user}')) + ->setDefaults([ + '_entity_view' => 'user.full', + '_title_callback' => 'Drupal\user\Controller\UserController::userTitle', + ]) + ->setRequirement('_entity_access', 'user.view'); + $route_collection->add('entity.user.canonical', $route); + + $route = (new Route('/user/{user}/edit')) + ->setDefaults([ + '_entity_form' => 'user.default', + '_title_callback' => 'Drupal\user\Controller\UserController::userTitle', + ]) + ->setOption('_admin_route', TRUE) + ->setRequirement('_entity_access', 'user.update'); + $route_collection->add('entity.user.edit_form', $route); + + $route = (new Route('/user/{user}/cancel')) + ->setDefaults([ + '_title' => 'Cancel account', + '_entity_form' => 'user.cancel', + ]) + ->setOption('_admin_route', TRUE) + ->setRequirement('_entity_access', 'user.delete'); + $route_collection->add('entity.user.cancel_form', $route); + + return $route_collection; + } + +} diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml index 4e1b99df6b43ed967cb5d7739e77a0c558880816..d071113e9ef9a1aab895dddc90071fd9764f50f0 100644 --- a/core/modules/user/user.routing.yml +++ b/core/modules/user/user.routing.yml @@ -133,14 +133,6 @@ user.page: requirements: _user_is_logged_in: 'TRUE' -entity.user.canonical: - path: '/user/{user}' - defaults: - _entity_view: 'user.full' - _title_callback: 'Drupal\user\Controller\UserController::userTitle' - requirements: - _entity_access: 'user.view' - user.login: path: '/user/login' defaults: @@ -151,26 +143,6 @@ user.login: options: _maintenance_access: TRUE -entity.user.edit_form: - path: '/user/{user}/edit' - defaults: - _entity_form: 'user.default' - _title_callback: 'Drupal\user\Controller\UserController::userTitle' - options: - _admin_route: TRUE - requirements: - _entity_access: 'user.update' - -entity.user.cancel_form: - path: '/user/{user}/cancel' - defaults: - _title: 'Cancel account' - _entity_form: 'user.cancel' - options: - _admin_route: TRUE - requirements: - _entity_access: 'user.delete' - user.cancel_confirm: path: '/user/{user}/cancel/confirm/{timestamp}/{hashed_pass}' defaults: diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php index ce6cf2798a69120b36b81087f064f9af42974bc6..caea0e566a03b5628afc366d3708828d967596d3 100644 --- a/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php @@ -1366,6 +1366,24 @@ public function testGetEntityTypeFromClassAmbiguous() { $this->entityManager->getEntityTypeFromClass('\Drupal\apple\Entity\Apple'); } + /** + * @covers ::getRouteProviders + */ + public function testGetRouteProviders() { + $apple = $this->getMock('Drupal\Core\Entity\EntityTypeInterface'); + $apple->expects($this->once()) + ->method('getRouteProviderClasses') + ->willReturn(['default' => 'Drupal\Tests\Core\Entity\TestRouteProvider']); + $this->setUpEntityManager(array( + 'apple' => $apple, + )); + + $apple_route_provider = $this->entityManager->getRouteProviders('apple'); + $this->assertInstanceOf('Drupal\Tests\Core\Entity\TestRouteProvider', $apple_route_provider['default']); + $this->assertAttributeInstanceOf('Drupal\Core\Extension\ModuleHandlerInterface', 'moduleHandler', $apple_route_provider['default']); + $this->assertAttributeInstanceOf('Drupal\Core\StringTranslation\TranslationInterface', 'stringTranslation', $apple_route_provider['default']); + } + /** * Gets a mock controller class name. * @@ -1546,6 +1564,14 @@ public static function create(ContainerInterface $container) { } +/** + * Provides a test entity route provider. + */ +class TestRouteProvider extends EntityHandlerBase { + +} + + /** * Provides a test config entity storage for base field overrides. */