Problem/Motivation

Currently EntityConverter has an option to load the latest revision. This is used mainly with entity forms to ensure draft revisions can be edited, instead of the default revision. However, this does not work with translated entities, because every translation draft has a dedicated pending revision, only one of which is the latest revision at any time.

Proposed resolution

To properly support multilingual content moderation, we need to load in entity forms and related routes the latest translation-affecting revision instead of the latest revision, this guarantees every translation draft can be loaded in the related entity form. This is also fully BC with respect to the original behavior, since for non-translated entities the latest translation-affecting revision and the latest revision are the same.

Remaining tasks

  • Validate the proposed solution
  • Write a patch
  • Reviews

User interface changes

None

API changes

None

Data model changes

None

Comments

plach created an issue. See original summary.

wim leers’s picture

Assigned: Unassigned » plach
Issue tags: +blocker
plach’s picture

Status: Active » Needs review
StatusFileSize
new19.68 KB
new1.41 KB

Here's a patch, split-off from #2860097-103: Ensure that content translations can be moderated independently. The interdiff is to address #105.

plach’s picture

Issue tags: +Needs followup, +Needs tests
  1. +++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php
    @@ -46,14 +49,31 @@ class EntityConverter implements ParamConverterInterface {
    +   * @see https://www.drupal.org/node/xxx FIXME
    

    We need a follow-up for this deprecation.

  2. +++ b/core/tests/Drupal/KernelTests/Core/ParamConverter/EntityConverterLatestRevisionTest.php
    @@ -80,8 +80,12 @@ public function testEntityWithPendingRevision() {
    +    $entity = $storage->createRevision($entity, FALSE);
    

    We could use additional test coverage here.

wim leers’s picture

plach’s picture

Issue tags: -Needs tests
StatusFileSize
new22.27 KB
new12.07 KB

This should address the outstanding issues and #2860097-105: Ensure that content translations can be moderated independently.2: Wim and I discussed this and agreed that improving the documentation should be enough to address his concerns.

plach’s picture

Wrong interdiff

wim leers’s picture

Status: Needs review » Needs work

NW mostly for nits, then this is RTBC!

  1. +++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php
    @@ -36,6 +39,29 @@
    + * make the converter load the latest revision affecting the translation
    

    Supernit: s/the converter/this converter/

  2. +++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php
    @@ -36,6 +39,29 @@
    + * it will fall fall back to the latest revision. For instance, if an entity has
    

    s/fall fall/fall/ 😆

  3. +++ b/core/tests/Drupal/Tests/Core/ParamConverter/EntityConverterTest.php
    @@ -130,4 +138,18 @@ public function testConvertWithInvalidDynamicEntityType() {
    +   * Tests that skipping the language manager triggers a deprecation error.
    

    Nit: s/skipping/omitting/

  4. +++ b/core/tests/Drupal/Tests/Core/ParamConverter/EntityConverterTest.php
    @@ -130,4 +138,18 @@ public function testConvertWithInvalidDynamicEntityType() {
    +    $container = $this->createMock('Symfony\Component\DependencyInjection\ContainerInterface');
    

    Nit: ContainerInterface::class would be slightly better.

  5. +++ b/core/tests/Drupal/Tests/Listeners/DeprecationListenerTrait.php
    @@ -325,6 +325,7 @@ public static function getSkippedDeprecations() {
    +      'The language manager parameter has been added to EntityConverter since version 8.5.0 and will be made required in version 9.0.0.',
    

    Do we actually need this? Will a core test fail if we don't add this? (We're only supposed to add this for deprecations that would otherwise make core tests fail. And AFAICT we're already updating all parameter converters that subclass EntityConverter here.)

plach’s picture

Status: Needs work » Needs review
StatusFileSize
new21.45 KB
new3.5 KB
wim leers’s picture

Status: Needs review » Reviewed & tested by the community

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 9: entity-ml_entity_converter-2938895-9.patch, failed testing. View results

plach’s picture

Status: Needs work » Needs review
StatusFileSize
new23.28 KB
new2.76 KB

Let's try this.

plach’s picture

StatusFileSize
new22.23 KB
new1.71 KB

Actually, I meant this.

wim leers’s picture

#13 looks good.

wim leers’s picture

Status: Needs review » Reviewed & tested by the community
effulgentsia’s picture

Status: Reviewed & tested by the community » Needs review
+++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php
@@ -46,14 +72,31 @@ class EntityConverter implements ParamConverterInterface {
-  public function __construct(EntityManagerInterface $entity_manager) {
+  public function __construct(EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager = NULL) {
     $this->entityManager = $entity_manager;
+    $this->languageManager = $language_manager ?: \Drupal::languageManager();
+    if (!isset($language_manager)) {
+      @trigger_error('The language manager parameter has been added to EntityConverter since version 8.5.0 and will be made required in version 9.0.0.', E_USER_DEPRECATED);
+    }

Are we sure we want to make the language manager (effectively) required for all entity converters? It's only needed for entities that are translatable, revisionable, and where there's a route flag asking for the latest revision. In which case, why require the parameter for all other cases, like all config entity param converters? What about adding a protected method, languageManager(), and trigger the deprecation warning from there, so only when used?

plach’s picture

StatusFileSize
new22.8 KB
new3.43 KB

Briefly discussed this with @effulgentsia in Slack, he suggested to trigger the deprecation error at runtime so we don't need to update config entity converters.

Here's a PoC, to be completed.

effulgentsia’s picture

Thanks. The interdiff looks great. I think that means we can also remove the changes to AdminPathConfigEntityConverter, ViewUIConverter, and maybe others from the patch?

  1. +++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php
    @@ -245,4 +238,22 @@ protected function getEntityTypeFromDefaults($definition, $name, array $defaults
    +      if (!isset($language_manager)) {
    

    Not needed.

  2. +++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php
    @@ -245,4 +238,22 @@ protected function getEntityTypeFromDefaults($definition, $name, array $defaults
    +        @trigger_error('The language manager parameter has been added to EntityConverter since version 8.5.0 and will be made required in version 9.0.0.', E_USER_DEPRECATED);
    

    Perhaps change the message to "The language manager parameter has been added to EntityConverter since version 8.5.0 and will be made required in version 9.0.0 when requesting the latest translation-affected revision of an entity."?

Status: Needs review » Needs work

The last submitted patch, 17: entity-ml_entity_converter-2938895-16.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

plach’s picture

Status: Needs work » Needs review
StatusFileSize
new18.09 KB
new11.84 KB

The patch should be now complete.

wim leers’s picture

  1. +++ b/core/core.services.yml
    @@ -976,7 +976,7 @@ services:
    -    arguments: ['@entity.manager', '@config.factory', '@router.admin_context']
    +    arguments: ['@entity.manager', '@config.factory', '@router.admin_context', '@language_manager']
    

    This change is unnecessary.

  2. +++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php
    @@ -132,4 +238,20 @@ protected function getEntityTypeFromDefaults($definition, $name, array $defaults
    +   * @return \Drupal\Core\Language\LanguageManagerInterface|null
    

    When can this return NULL?

  3. +++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php
    @@ -132,4 +238,20 @@ protected function getEntityTypeFromDefaults($definition, $name, array $defaults
    +  protected function languageManager() {
    

    Let's mark this @internal.

  4. +++ b/core/modules/content_moderation/src/ParamConverter/EntityRevisionConverter.php
    @@ -30,7 +30,7 @@ class EntityRevisionConverter extends EntityConverter {
    -    parent::__construct($entity_manager);
    +    parent::__construct($entity_manager, \Drupal::languageManager());
    

    AFAICT thanks to the changes proposed by @effulgentsia and implemented now, this change is also no longer necessary.

    (It will be necessary in #2860097: Ensure that content translations can be moderated independently though AFAICT.)

plach’s picture

StatusFileSize
new17.66 KB
new1.29 KB

1: Fixed
2: Fixed
3: Fixed
4: Correct, let's keep it here, so we don't have to touch this code again over there, where it would be more confusing.

wim leers’s picture

Status: Needs review » Reviewed & tested by the community
plach’s picture

This will need a change record update once committed.

plach’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new17.33 KB
new676 bytes
+++ b/core/tests/Drupal/Tests/Core/ParamConverter/EntityConverterTest.php
@@ -22,6 +28,13 @@ class EntityConverterTest extends UnitTestCase {
+  /**
+   * The mocked language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $languageManager;
+

Reverted a no longer needed change.

effulgentsia’s picture

Status: Needs review » Reviewed & tested by the community

#25 interdiff is trivial, so back to RTBC.

effulgentsia’s picture

Adding reviewer credit for @Wim Leers, and also for @timmillwood and @hchonov for their work on #2860097: Ensure that content translations can be moderated independently from which this was split.

  • effulgentsia committed 1708b46 on 8.6.x
    Issue #2938895 by plach, Wim Leers, hchonov, timmillwood: Make...

  • effulgentsia committed dbeef23 on 8.5.x
    Issue #2938895 by plach, Wim Leers, hchonov, timmillwood: Make...
effulgentsia’s picture

Version: 8.6.x-dev » 8.5.x-dev
Status: Reviewed & tested by the community » Fixed

Pushed to 8.6.x and cherry picked to 8.5.x.

effulgentsia’s picture

+++ b/core/modules/content_translation/src/Routing/ContentTranslationRouteSubscriber.php
@@ -70,6 +70,7 @@ protected function alterRoutes(RouteCollection $collection) {
+              'load_latest_revision' => TRUE,
+              'load_latest_revision' => TRUE,
+              'load_latest_revision' => TRUE,
+              'load_latest_revision' => TRUE,

Per #2860097-111: Ensure that content translations can be moderated independently, this might be a controversial change. I'll await comments from @hchonov and @plach, but I'm considering reverting just these lines if there isn't agreement about them yet.

hchonov’s picture

@plach proposed in #2860097-115: Ensure that content translations can be moderated independently:

Given that loading the latest (translation-affecting) revision is needed only when Content Moderation is enabled, at least in core, we could alter CT's route definitions and add the load_latest_revision flag in the CM's code.

and I've responded to this in #2860097-121: Ensure that content translations can be moderated independently:

If this is the only way, then I am not going to argue. There is only drawback with this - this makes it impossible for other modules making use of forward revisions to work together with content moderation, but I guess this will be not the only point? If so, then we are fine to go, but probably should somehow make it clear that content moderation could not be used together with other modules/approaches relying on forward revisions.

So I think that those changes to the content translation routes should be reverted.


I have one remark and one question about the patch that got in:

  1. +++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php
    @@ -68,14 +104,12 @@ public function convert($value, $definition, $name, array $defaults) {
    +      $entity = $this->getLatestTranslationAffectedRevision($entity, $langcode);
    
    @@ -87,6 +121,78 @@ public function convert($value, $definition, $name, array $defaults) {
    +      $revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $langcode);
    

    As we are executing a query to retrieve the revision ID, then we don't necessary have to load the default entity revision. The code could be easy converted to skip loading the default revision if it isn't needed.

  2. +++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php
    @@ -132,4 +238,22 @@ protected function getEntityTypeFromDefaults($definition, $name, array $defaults
    +  protected function languageManager() {
    +    if (!isset($this->languageManager)) {
    +      $this->languageManager = \Drupal::languageManager();
    

    Why do we provide BC here instead of enforcing the language parameter to be included through DI? I've found the following in https://www.drupal.org/core/d8-bc-policy:

    The constructor for a service object (one in the container), plugin, or controller is considered internal unless otherwise marked, and may change in a minor release. These are objects where developers are not expected to call the constructor directly in the first place. Constructors for value objects where a developer will be calling the constructor directly are excluded from this statement.

    If I am not mistaken, then we don't have to provide BC when altering the constructor.

wim leers’s picture

#34.2: the BC policy is the theory, in practice we also look at "likely real-world disruption". In this case, with half a dozen subclasses in core alone, it is very likely that we'd disrupt contrib/custom modules. In this case, and many cases before it, we've therefore opted to make new constructor arguments optional, and then automatically load them.

plach’s picture

As we are executing a query to retrieve the revision ID, then we don't necessary have to load the default entity revision. The code could be easy converted to skip loading the default revision if it isn't needed.

This was done to keep around an optimization @catch suggested in the parent issue: #2865616-80: Add an option for EntityConverter to load the latest entity revision and fix all entity forms to use this option..

hchonov’s picture

@plach, if it happens that the current default revision and the forward revisions are different, which is pretty likely to be the case when using forward revisions, then it will always happen that we load two entity objects. Optimizations for loading only one entity object by ID and by revisions ID if it is the same revision both point to are done in #2620980: Add static and persistent caching to ContentEntityStorageBase::loadRevision() and could be further optmized. The patch there will load only one entity from the persistent cache if a revision is first loaded by ::loadRevision('revision_ID_of_the_current_default_revision') and then the entity is loaded through ::load('entity_id').

plach’s picture

Good to know :)

I believe that optimization was done in the light of the fact that right now loading the default revision is way less expensive than loading any other one. Of course the issue above will change that.

effulgentsia’s picture

So I think that those changes to the content translation routes should be reverted.

I posted a patch to revert them in #2939247: Revert Content Translation routes to act on default rather than latest revisions.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.