Commit d0518c25 authored by catch's avatar catch

Issue #2865616 by Sam152, plach, dawehner, timmillwood, amateescu,...

Issue #2865616 by Sam152, plach, dawehner, timmillwood, amateescu, tim.plunkett: Add an option for EntityConverter to load the latest entity revision and fix all entity forms to use this option
parent 99025a10
......@@ -196,6 +196,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.
*
......@@ -212,6 +236,7 @@ public function setRouteOptions(Route $route) {
// Try to use _entity_* information on the route.
$this->setParametersFromEntityInformation($route);
$this->setLatestRevisionFlag($route);
}
/**
......
......@@ -2,8 +2,11 @@
namespace Drupal\Core\ParamConverter;
use Drupal\Core\Entity\ContentEntityInterface;
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,15 +63,54 @@ 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);
// 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']);
$storage = $this->entityManager->getStorage($entity_type_id);
$entity_definition = $this->entityManager->getDefinition($entity_type_id);
$entity = $storage->load($value);
// If the entity type is revisionable and the parameter has the
// "load_latest_revision" flag, load the latest revision.
if ($entity instanceof ContentEntityInterface && !empty($definition['load_latest_revision']) && $entity_definition->isRevisionable()) {
$latest_revision_id = $this->getLatestRevisionId($storage, $entity_definition, $value);
if ($entity->getLoadedRevisionId() !== $latest_revision_id) {
$entity = $storage->loadRevision($latest_revision_id);
}
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;
}
/**
* Get the latest revision ID.
*
* @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 int
* The latest revision ID for a given entity.
*/
protected function getLatestRevisionId(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.
$result = $storage
->getQuery()
->latestRevision()
->condition($entity_definition->getKey('id'), $value)
// The entity converter is not concerned with access checking, skip the
// access check when looking up the latest revision.
->accessCheck(FALSE)
->execute();
return key($result);
}
/**
......
<?php
namespace Drupal\KernelTests\Core\ParamConverter;
use Drupal\entity_test\Entity\EntityTestMulRev;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests the entity converter when the "load_latest_revision" flag is set.
*
* @group ParamConverter
* @coversDefaultClass \Drupal\Core\ParamConverter\EntityConverter
*/
class EntityConverterLatestRevisionTest extends KernelTestBase {
/**
* Modules to install.
*
* @var array
*/
public static $modules = [
'entity_test',
'user',
'language',
'system',
];
/**
* The entity converter service.
*
* @var \Drupal\Core\ParamConverter\EntityConverter
*/
protected $converter;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->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());
}
}
......@@ -445,23 +445,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,
],
],
],
];
}
}
/**
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment