diff --git a/core/lib/Drupal/Core/Entity/EntityResolverManager.php b/core/lib/Drupal/Core/Entity/EntityResolverManager.php index e9a87d6..10ad93a 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 (!isset($this->getEntityTypes()[$entity_type]) || !$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/ParamConverter/EntityConverter.php b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php index 67f6a89..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; @@ -60,14 +62,59 @@ 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)) { + $storage = $this->entityManager->getStorage($entity_type_id); + $entity_definition = $this->entityManager->getDefinition($entity_type_id); + + $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()) { + $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; + } + + // 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 new file mode 100644 index 0000000..34b53cb --- /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(); + } + + /** + * Tests 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); + } + + /** + * Tests 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()); + } + + /** + * Tests 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()); + } + + /** + * Tests 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/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, + ], + ], + ], + ]; + } + } /**