diff --git a/core/core.services.yml b/core/core.services.yml index 777cc34..1c02e0b 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -979,7 +979,7 @@ services: class: Drupal\Core\EventSubscriber\EntityRouteAlterSubscriber tags: - { name: event_subscriber } - arguments: ['@resolver_manager.entity', '@entity_type.manager'] + arguments: ['@resolver_manager.entity'] ajax_response.subscriber: class: Drupal\Core\EventSubscriber\AjaxResponseSubscriber arguments: ['@ajax_response.attachments_processor'] diff --git a/core/lib/Drupal/Core/Entity/EntityResolverManager.php b/core/lib/Drupal/Core/Entity/EntityResolverManager.php index e9a87d6..e4eaf1f 100644 --- a/core/lib/Drupal/Core/Entity/EntityResolverManager.php +++ b/core/lib/Drupal/Core/Entity/EntityResolverManager.php @@ -197,6 +197,30 @@ protected function setParametersFromEntityInformation(Route $route) { } /** + * Ensure revisionable entities load the latest revision on entity forms. + * + * @param \Symfony\Component\Routing\Route $route + * The route object. + */ + protected function setLatestRevisionFlag(Route $route) { + if (!$entity_form = $route->getDefault('_entity_form')) { + return; + } + // Only set the flag on entity types which are revisionable. + list($entity_type) = explode('.', $entity_form, 2); + if (!$this->getEntityTypes()[$entity_type]->isRevisionable()) { + return; + } + $parameters = $route->getOption('parameters') ?: []; + foreach ($parameters as &$parameter) { + if ($parameter['type'] === 'entity:' . $entity_type && !isset($parameter['load_latest_revision'])) { + $parameter['load_latest_revision'] = TRUE; + } + } + $route->setOption('parameters', $parameters); + } + + /** * Set the upcasting route objects. * * @param \Symfony\Component\Routing\Route $route @@ -212,6 +236,7 @@ public function setRouteOptions(Route $route) { // Try to use _entity_* information on the route. $this->setParametersFromEntityInformation($route); + $this->setLatestRevisionFlag($route); } /** diff --git a/core/lib/Drupal/Core/EventSubscriber/EntityRouteAlterSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/EntityRouteAlterSubscriber.php index 9b72c67..bca8f68 100644 --- a/core/lib/Drupal/Core/EventSubscriber/EntityRouteAlterSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/EntityRouteAlterSubscriber.php @@ -3,7 +3,6 @@ namespace Drupal\Core\EventSubscriber; use Drupal\Core\Entity\EntityResolverManager; -use Drupal\Core\Entity\EntityTypeManagerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Drupal\Core\Routing\RoutingEvents; use Drupal\Core\Routing\RouteBuildEvent; @@ -29,21 +28,13 @@ class EntityRouteAlterSubscriber implements EventSubscriberInterface { protected $resolverManager; /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - - /** * Constructs an EntityRouteAlterSubscriber instance. * * @param \Drupal\Core\Entity\EntityResolverManager $entity_resolver_manager * The entity resolver manager. */ - public function __construct(EntityResolverManager $entity_resolver_manager, EntityTypeManagerInterface $entity_type_manager) { + public function __construct(EntityResolverManager $entity_resolver_manager) { $this->resolverManager = $entity_resolver_manager; - $this->entityTypeManager = $entity_type_manager; } /** @@ -59,37 +50,10 @@ public function onRoutingRouteAlterSetType(RouteBuildEvent $event) { } /** - * Set the latest revision flag for entity forms. - * - * @param \Drupal\Core\Routing\RouteBuildEvent $event - * The event to process. - */ - public function onRoutingRouteAlterSetLatestRevision(RouteBuildEvent $event) { - foreach ($event->getRouteCollection() as $route) { - if (!$entity_form = $route->getDefault('_entity_form')) { - continue; - } - // Only set the flag on entity types which are revisionable. - list($entity_type) = explode('.', $entity_form); - if (!$this->entityTypeManager->getDefinition($entity_type)->isRevisionable()) { - continue; - } - $parameters = $route->getOption('parameters') ?: []; - foreach ($parameters as &$parameter) { - if ($parameter['type'] === 'entity:' . $entity_type && !isset($parameter['load_latest_revision'])) { - $parameter['load_latest_revision'] = TRUE; - } - } - $route->setOption('parameters', $parameters); - } - } - - /** * {@inheritdoc} */ public static function getSubscribedEvents() { $events[RoutingEvents::ALTER][] = ['onRoutingRouteAlterSetType', -150]; - $events[RoutingEvents::ALTER][] = ['onRoutingRouteAlterSetLatestRevision', -149]; return $events; } diff --git a/core/lib/Drupal/Core/ParamConverter/EntityConverter.php b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php index be2a354..8f03770 100644 --- a/core/lib/Drupal/Core/ParamConverter/EntityConverter.php +++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php @@ -4,6 +4,8 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\TypedData\TranslatableInterface; use Symfony\Component\Routing\Route; @@ -62,38 +64,57 @@ public function convert($value, $definition, $name, array $defaults) { $entity_type_id = $this->getEntityTypeFromDefaults($definition, $name, $defaults); $storage = $this->entityManager->getStorage($entity_type_id); $entity_definition = $this->entityManager->getDefinition($entity_type_id); - if ($storage && $definition) { - $entity = NULL; - // If the entity type is revisionable and the parameter has the - // "load_latest_revision" flag, load the latest revision. - if (!empty($definition['load_latest_revision']) && $entity_definition->isRevisionable()) { - // @todo, replace this query with a standardized way of getting the - // latest revision in https://www.drupal.org/node/2784201. - $entity_revisions = $storage - ->getQuery() - ->allRevisions() - ->condition($entity_definition->getKey('id'), $value) - ->sort($entity_definition->getKey('revision'), 'DESC') - ->range(0, 1) - ->accessCheck(FALSE) - ->execute(); - if (!empty($entity_revisions)) { - $revision_ids = array_keys($entity_revisions); - $latest_revision_id = array_shift($revision_ids); - $entity = $storage->loadRevision($latest_revision_id); - } - } - else { - $entity = $storage->load($value); - } + $entity = NULL; - // If the entity type is translatable, ensure we return the proper - // translation object for the current context. - if ($entity instanceof EntityInterface && $entity instanceof TranslatableInterface) { - $entity = $this->entityManager->getTranslationFromContext($entity, NULL, ['operation' => 'entity_upcast']); - } - return $entity; + // If the entity type is revisionable and the parameter has the + // "load_latest_revision" flag, load the latest revision. + if (!empty($definition['load_latest_revision']) && $entity_definition->isRevisionable()) { + $entity = $this->loadLatestRevision($storage, $entity_definition, $value); + } + else { + $entity = $storage->load($value); + } + + // If the entity type is translatable, ensure we return the proper + // translation object for the current context. + if ($entity instanceof EntityInterface && $entity instanceof TranslatableInterface) { + $entity = $this->entityManager->getTranslationFromContext($entity, NULL, ['operation' => 'entity_upcast']); + } + + return $entity; + } + + /** + * Load the latest revision of an entity. + * + * @param \Drupal\Core\Entity\EntityStorageInterface $storage + * The entity storage. + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_definition + * The entity definition. + * @param mixed $value + * The raw value. + * + * @return \Drupal\Core\Entity\EntityInterface|null + * The loaded entity if it exists, NULL otherwise. + */ + protected function loadLatestRevision(EntityStorageInterface $storage, EntityTypeInterface $entity_definition, $value) { + // @todo, replace this query with a standardized way of getting the + // latest revision in https://www.drupal.org/node/2784201. + $entity_revisions = $storage + ->getQuery() + ->allRevisions() + ->condition($entity_definition->getKey('id'), $value) + ->sort($entity_definition->getKey('revision'), 'DESC') + ->range(0, 1) + // The entity converter is not concerned with access checking, skip the + // access check when looking up the latest revision. + ->accessCheck(FALSE) + ->execute(); + if (!empty($entity_revisions)) { + $revision_ids = array_keys($entity_revisions); + $latest_revision_id = array_shift($revision_ids); + return $storage->loadRevision($latest_revision_id); } } diff --git a/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php b/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php index 9f14d44..34b53cb 100644 --- a/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php +++ b/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php @@ -7,7 +7,7 @@ use Drupal\language\Entity\ConfigurableLanguage; /** - * Test the entity converter when the "load_latest_revision" flag is set. + * Tests the entity converter when the "load_latest_revision" flag is set. * * @group ParamConverter * @coversDefaultClass \Drupal\Core\ParamConverter\EntityConverter @@ -49,7 +49,7 @@ protected function setUp() { } /** - * Test with no matching entity. + * Tests with no matching entity. */ public function testNoEntity() { $converted = $this->converter->convert(1, [ @@ -60,7 +60,7 @@ public function testNoEntity() { } /** - * Test with no pending revision. + * Tests with no pending revision. */ public function testEntityNoPendingRevision() { $entity = EntityTestMulRev::create(); @@ -74,7 +74,7 @@ public function testEntityNoPendingRevision() { } /** - * Test with a pending revision. + * Tests with a pending revision. */ public function testEntityWithPendingRevision() { $entity = EntityTestMulRev::create(); @@ -93,7 +93,7 @@ public function testEntityWithPendingRevision() { } /** - * Test with a translated pending revision. + * Tests with a translated pending revision. */ public function testWithTranslatedPendingRevision() { $entity = EntityTestMulRev::create(); diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityResolverManagerTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityResolverManagerTest.php index ee02cae..d9a53b4 100644 --- a/core/tests/Drupal/Tests/Core/Entity/EntityResolverManagerTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/EntityResolverManagerTest.php @@ -10,6 +10,7 @@ use Drupal\Core\Entity\Entity; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityResolverManager; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormInterface; use Drupal\Core\Form\FormStateInterface; @@ -445,23 +446,153 @@ protected function setupEntityTypes() { $definition->expects($this->any()) ->method('getClass') ->will($this->returnValue('Drupal\Tests\Core\Entity\SimpleTestEntity')); + $definition->expects($this->any()) + ->method('isRevisionable') + ->willReturn(FALSE); + $revisionable_definition = $this->getMock('Drupal\Core\Entity\EntityTypeInterface'); + $revisionable_definition->expects($this->any()) + ->method('getClass') + ->will($this->returnValue('Drupal\Tests\Core\Entity\SimpleTestEntity')); + $revisionable_definition->expects($this->any()) + ->method('isRevisionable') + ->willReturn(TRUE); $this->entityManager->expects($this->any()) ->method('getDefinitions') ->will($this->returnValue([ 'entity_test' => $definition, + 'entity_test_rev' => $revisionable_definition, ])); $this->entityManager->expects($this->any()) ->method('getDefinition') - ->will($this->returnCallback(function ($entity_type) use ($definition) { + ->will($this->returnCallback(function ($entity_type) use ($definition, $revisionable_definition) { if ($entity_type == 'entity_test') { return $definition; } + elseif ($entity_type === 'entity_test_rev') { + return $revisionable_definition; + } else { return NULL; } })); } + /** + * @covers ::setLatestRevisionFlag + * + * @dataProvider setLatestRevisionFlagTestCases + */ + public function testSetLatestRevisionFlag($defaults, $parameters, $expected_parameters = FALSE) { + $route = new Route('/foo/{entity_test}', $defaults, [], [ + 'parameters' => $parameters, + ]); + $this->setupEntityTypes(); + $this->entityResolverManager->setRouteOptions($route); + // If expected parameters have not been provided, assert they are unchanged. + $this->assertEquals($expected_parameters ?: $parameters, $route->getOption('parameters')); + } + + /** + * Data provider for ::testSetLatestRevisionFlag. + */ + public function setLatestRevisionFlagTestCases() { + return [ + 'Entity parameter not on an entity form' => [ + [], + [ + 'entity_test' => [ + 'type' => 'entity:entity_test_rev', + ], + ], + ], + 'Entity parameter on an entity form' => [ + [ + '_entity_form' => 'entity_test_rev.edit' + ], + [ + 'entity_test_rev' => [ + 'type' => 'entity:entity_test_rev', + ], + ], + [ + 'entity_test_rev' => [ + 'type' => 'entity:entity_test_rev', + 'load_latest_revision' => TRUE, + ], + ], + ], + 'Multiple entity parameters on an entity form' => [ + [ + '_entity_form' => 'entity_test_rev.edit' + ], + [ + 'entity_test_rev' => [ + 'type' => 'entity:entity_test_rev', + ], + 'node' => [ + 'type' => 'entity:node', + ], + ], + [ + 'entity_test_rev' => [ + 'type' => 'entity:entity_test_rev', + 'load_latest_revision' => TRUE, + ], + 'node' => [ + 'type' => 'entity:node', + ], + ], + ], + 'Overriden load_latest_revision flag does not change' => [ + [ + '_entity_form' => 'entity_test_rev.edit' + ], + [ + 'entity_test_rev' => [ + 'type' => 'entity:entity_test_rev', + 'load_latest_revision' => FALSE, + ], + ], + ], + 'Non-revisionable entity type will not change' => [ + [ + '_entity_form' => 'entity_test.edit' + ], + [ + 'entity_test' => [ + 'type' => 'entity:entity_test', + ], + ], + FALSE, + FALSE, + ], + 'Overriden load_latest_revision flag does not change with multiple parameters' => [ + [ + '_entity_form' => 'entity_test_rev.edit' + ], + [ + 'entity_test_rev' => [ + 'type' => 'entity:entity_test_rev', + ], + 'node' => [ + 'type' => 'entity:node', + 'load_latest_revision' => FALSE, + ], + ], + [ + 'entity_test_rev' => [ + 'type' => 'entity:entity_test_rev', + 'load_latest_revision' => TRUE, + ], + 'node' => [ + 'type' => 'entity:node', + 'load_latest_revision' => FALSE, + ], + ], + ], + ]; + } + } /** diff --git a/core/tests/Drupal/Tests/Core/EventSubscriber/EntityRouteAlterSubscriberTest.php b/core/tests/Drupal/Tests/Core/EventSubscriber/EntityRouteAlterSubscriberTest.php deleted file mode 100644 index 8d59b6c..0000000 --- a/core/tests/Drupal/Tests/Core/EventSubscriber/EntityRouteAlterSubscriberTest.php +++ /dev/null @@ -1,198 +0,0 @@ -prophesize(EntityTypeInterface::class); - $entity_definition->isRevisionable()->willReturn($entity_type_revisionable); - - $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); - $entity_type_manager->getDefinition('entity_test')->willReturn($entity_definition->reveal()); - - $subscriber = new EntityRouteAlterSubscriber($this->prophesize(EntityResolverManager::class)->reveal(), $entity_type_manager->reveal()); - - $route = new Route('/foo', $defaults, [], [ - 'parameters' => $parameters, - ]); - $collection = new RouteCollection(); - $collection->add('foo', $route); - - $a = new EntityResolverManager($entity_type_manager->reveal()); - - $subscriber->onRoutingRouteAlterSetLatestRevision(new RouteBuildEvent($collection)); - // If expected parameters have not been provided, assert they are unchanged. - $this->assertEquals($expected_parameters ?: $parameters, $route->getOption('parameters')); - } - - /** - * Data provider for ::testLatestRevisionAlter. - */ - public function latestRevisionAlterTestCases() { - return [ - 'Entity parameter not on an entity form' => [ - [], - [ - 'entity_test' => [ - 'type' => 'entity:entity_test', - ], - ], - ], - 'Entity parameter on an entity form' => [ - [ - '_entity_form' => 'entity_test.edit' - ], - [ - 'entity_test' => [ - 'type' => 'entity:entity_test', - ], - ], - [ - 'entity_test' => [ - 'type' => 'entity:entity_test', - 'load_latest_revision' => TRUE, - ], - ], - ], - 'Multiple entity parameters on an entity form' => [ - [ - '_entity_form' => 'entity_test.edit' - ], - [ - 'entity_test' => [ - 'type' => 'entity:entity_test', - ], - 'node' => [ - 'type' => 'entity:node', - ], - ], - [ - 'entity_test' => [ - 'type' => 'entity:entity_test', - 'load_latest_revision' => TRUE, - ], - 'node' => [ - 'type' => 'entity:node', - ], - ], - ], - 'Overriden load_latest_revision flag does not change' => [ - [ - '_entity_form' => 'entity_test.edit' - ], - [ - 'entity_test' => [ - 'type' => 'entity:entity_test', - 'load_latest_revision' => FALSE, - ], - ], - ], - 'Non-revisionable entity type will not change' => [ - [ - '_entity_form' => 'entity_test.edit' - ], - [ - 'entity_test' => [ - 'type' => 'entity:entity_test', - ], - ], - FALSE, - FALSE, - ], - 'Overriden load_latest_revision flag does not change with multiple parameters' => [ - [ - '_entity_form' => 'entity_test.edit' - ], - [ - 'entity_test' => [ - 'type' => 'entity:entity_test', - ], - 'node' => [ - 'type' => 'entity:node', - 'load_latest_revision' => FALSE, - ], - ], - [ - 'entity_test' => [ - 'type' => 'entity:entity_test', - 'load_latest_revision' => TRUE, - ], - 'node' => [ - 'type' => 'entity:node', - 'load_latest_revision' => FALSE, - ], - ], - ], - ]; - } - - /** - * Test the subscriber with multiple routes with varying entity types. - */ - public function testMultipleRoutes() { - $non_revisionable_entity_type = $this->prophesize(EntityTypeInterface::class); - $non_revisionable_entity_type->isRevisionable()->willReturn(FALSE); - - $revisionable_entity_type = $this->prophesize(EntityTypeInterface::class); - $revisionable_entity_type->isRevisionable()->willReturn(TRUE); - - $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); - $entity_type_manager->getDefinition('non_revisionable')->willReturn($non_revisionable_entity_type); - $entity_type_manager->getDefinition('revisionable')->willReturn($revisionable_entity_type->reveal()); - - $non_revisionable_route = new Route('/foo', ['_entity_form' => 'non_revisionable.edit'], [], [ - 'parameters' => [ - 'non_revisionable' => [ - 'type' => 'entity:non_revisionable', - ], - ], - ]); - $revisionable_route = new Route('/bar', ['_entity_form' => 'revisionable.edit'], [], [ - 'parameters' => [ - 'revisionable' => [ - 'type' => 'entity:revisionable', - ], - ], - ]); - $collection = new RouteCollection(); - $collection->add('foo', $non_revisionable_route); - $collection->add('bar', $revisionable_route); - - $subscriber = new EntityRouteAlterSubscriber($this->prophesize(EntityResolverManager::class)->reveal(), $entity_type_manager->reveal()); - $subscriber->onRoutingRouteAlterSetLatestRevision(new RouteBuildEvent($collection)); - - // If expected parameters have not been provided, assert they are unchanged. - $this->assertEquals([ - 'revisionable' => [ - 'type' => 'entity:revisionable', - 'load_latest_revision' => TRUE, - ], - ], $revisionable_route->getOption('parameters')); - - $this->assertEquals([ - 'non_revisionable' => [ - 'type' => 'entity:non_revisionable', - ], - ], $non_revisionable_route->getOption('parameters')); - } - -}