diff --git a/core/core.services.yml b/core/core.services.yml index bcf528b..f210151 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -986,7 +986,7 @@ services: class: Drupal\Core\EventSubscriber\EntityRouteAlterSubscriber tags: - { name: event_subscriber } - arguments: ['@resolver_manager.entity'] + arguments: ['@resolver_manager.entity', '@entity_type.manager'] ajax_response.subscriber: class: Drupal\Core\EventSubscriber\AjaxResponseSubscriber arguments: ['@ajax_response.attachments_processor'] diff --git a/core/lib/Drupal/Core/EventSubscriber/EntityRouteAlterSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/EntityRouteAlterSubscriber.php index bca8f68..721eb7c 100644 --- a/core/lib/Drupal/Core/EventSubscriber/EntityRouteAlterSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/EntityRouteAlterSubscriber.php @@ -3,6 +3,7 @@ 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; @@ -28,13 +29,21 @@ 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) { + public function __construct(EntityResolverManager $entity_resolver_manager, EntityTypeManagerInterface $entity_type_manager) { $this->resolverManager = $entity_resolver_manager; + $this->entityTypeManager = $entity_type_manager; } /** @@ -50,10 +59,37 @@ 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()) { + 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); + } + } + + /** * {@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 67f6a89..be2a354 100644 --- a/core/lib/Drupal/Core/ParamConverter/EntityConverter.php +++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php @@ -60,8 +60,34 @@ public function __construct(EntityManagerInterface $entity_manager) { */ public function convert($value, $definition, $name, array $defaults) { $entity_type_id = $this->getEntityTypeFromDefaults($definition, $name, $defaults); - if ($storage = $this->entityManager->getStorage($entity_type_id)) { - $entity = $storage->load($value); + $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); + } + // If the entity type is translatable, ensure we return the proper // translation object for the current context. if ($entity instanceof EntityInterface && $entity instanceof TranslatableInterface) { diff --git a/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php b/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php new file mode 100644 index 0000000..9f14d44 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php @@ -0,0 +1,125 @@ +installEntitySchema('user'); + $this->installEntitySchema('entity_test_mulrev'); + $this->installConfig(['system', 'language']); + + $this->converter = $this->container->get('paramconverter.entity'); + + ConfigurableLanguage::createFromLangcode('de')->save(); + } + + /** + * Test with no matching entity. + */ + public function testNoEntity() { + $converted = $this->converter->convert(1, [ + 'load_latest_revision' => TRUE, + 'type' => 'entity:entity_test_mulrev', + ], 'foo', []); + $this->assertEquals(NULL, $converted); + } + + /** + * Test with no pending revision. + */ + public function testEntityNoPendingRevision() { + $entity = EntityTestMulRev::create(); + $entity->save(); + + $converted = $this->converter->convert(1, [ + 'load_latest_revision' => TRUE, + 'type' => 'entity:entity_test_mulrev', + ], 'foo', []); + $this->assertEquals($entity->getLoadedRevisionId(), $converted->getLoadedRevisionId()); + } + + /** + * Test with a pending revision. + */ + public function testEntityWithPendingRevision() { + $entity = EntityTestMulRev::create(); + $entity->save(); + + $entity->isDefaultRevision(FALSE); + $entity->setNewRevision(TRUE); + $entity->save(); + + $converted = $this->converter->convert(1, [ + 'load_latest_revision' => TRUE, + 'type' => 'entity:entity_test_mulrev', + ], 'foo', []); + + $this->assertEquals($entity->getLoadedRevisionId(), $converted->getLoadedRevisionId()); + } + + /** + * Test with a translated pending revision. + */ + public function testWithTranslatedPendingRevision() { + $entity = EntityTestMulRev::create(); + $entity->save(); + + // Create a translated pending revision. + $translated_entity = $entity->addTranslation('de'); + $translated_entity->isDefaultRevision(FALSE); + $translated_entity->setNewRevision(TRUE); + $translated_entity->save(); + + // Change the site language so the converters will attempt to load entities + // with 'de'. + $this->config('system.site')->set('default_langcode', 'de')->save(); + + // The default loaded language is still 'en'. + EntityTestMulRev::load($entity->id()); + $this->assertEquals('en', $entity->language()->getId()); + + // The converter will load the latest revision in the correct language. + $converted = $this->converter->convert(1, [ + 'load_latest_revision' => TRUE, + 'type' => 'entity:entity_test_mulrev', + ], 'foo', []); + $this->assertEquals('de', $converted->language()->getId()); + $this->assertEquals($translated_entity->getLoadedRevisionId(), $converted->getLoadedRevisionId()); + } + +} diff --git a/core/tests/Drupal/Tests/Core/EventSubscriber/EntityRouteAlterSubscriberTest.php b/core/tests/Drupal/Tests/Core/EventSubscriber/EntityRouteAlterSubscriberTest.php new file mode 100644 index 0000000..f53912f --- /dev/null +++ b/core/tests/Drupal/Tests/Core/EventSubscriber/EntityRouteAlterSubscriberTest.php @@ -0,0 +1,163 @@ +entityDefinition = $this->prophesize(EntityTypeInterface::class); + $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); + $entity_type_manager->getDefinition('entity_test')->willReturn($this->entityDefinition->reveal()); + $this->subscriber = new EntityRouteAlterSubscriber($this->prophesize(EntityResolverManager::class)->reveal(), $entity_type_manager->reveal()); + } + + /** + * @covers ::onRoutingRouteAlterSetLatestRevision + * + * @dataProvider latestRevisionAlterTestCases + */ + public function testLatestRevisionAlter($defaults, $parameters, $expected_parameters = FALSE, $entity_type_revisionable = TRUE) { + $this->entityDefinition->isRevisionable()->willReturn($entity_type_revisionable); + $route = new Route('/foo', $defaults, [], [ + 'parameters' => $parameters, + ]); + $collection = new RouteCollection(); + $collection->add('foo', $route); + + $this->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, + ], + ], + ], + ]; + } + +}