Allow composite entities to opt out of creating duplicate revisions

Created on 3 March 2022, almost 3 years ago
Updated 7 November 2023, about 1 year ago

Problem/Motivation

  • In #2801321: New host revisions do not always create new composite entity revisions , it was decided to always create a new revision of a composite entity if the host entity is saving a new revision, even if the composite entity's needsSave() is false.
  • This means that if you edit a node that has 50 paragraphs, just edit one of those paragraphs, and save the node, it will create new revisions for all 50 paragraphs, including the 49 that have not been modified. This leads to unnecessarily large database sizes.
  • For the regular Paragraphs widget, this might make sense to do, since it's not straightforward how to detect which paragraphs on the form have been modified. However, the UI for the Layout Paragraphs widget is such that it is clear which paragraphs have been edited, because you have to click on the pencil icon for the one you want to edit before you can edit it. Currently, Layout Paragraphs sets needsSave() for all of the paragraphs anyway, but in theory it could be changed to not do that and only set it for the ones that have actually been edited.
  • #2801321: New host revisions do not always create new composite entity revisions says that one of the motivations for wanting composite entity revisions to only be referenced by a single host entity revision is to prevent the situation where one edits the default revision of the host entity, makes a change, and saves that without creating a new host entity revision. In that situation we wouldn't want to update the paragraph revision that is also referenced by prior node revisions, since that would change the history of that node. However, there's got to be other ways to solve that, such as checking to see if we're in that situation, and if we are, then to create a new revision for the composite entity.

Steps to reproduce

Proposed resolution

EntityReferenceRevisionsItem::preSave() currently hard-codes the following:

      $is_affected = !$this->getFieldDefinition()->isTranslatable() || ($host instanceof TranslatableRevisionableInterface && $host->hasTranslationChanges());
      if ($is_affected && !$host->isNew() && $this->entity && $this->entity->getEntityType()->get('entity_revision_parent_id_field')) {
        if ($host->isNewRevision()) {
          $this->entity->setNewRevision();
          $needs_save = TRUE;
        }

My proposal is to move this to a policy pattern, similar to how Drupal core's page_cache_response_policy service works. It's a chained policy service that invokes each registered policy, and if any policy says to deny, then the response is not cached in the page cache.

Similarly, ERR could introduce a entity_reference_revisions.revision_creation_policy service that chains to registered policies, and if any policy says to create a new revision, then the chained policy says to create a new revision. The above code could then be changed to:

if (\Drupal::service('entity_reference_revisions.revision_creation_policy')->shouldCreateNewRevision($this)) {
  $this->entity->setNewRevision();
  $needs_save = TRUE;
}

By default, ERR could register a CompositeEntityReferencedByNewHostRevisionViaUntranslatableReference policy that does:

class CompositeEntityReferencedByNewHostRevisionViaUntranslatableReference implements RevisionCreationPolicyInterface {
  function shouldCreateNewRevision(EntityReferenceRevisionsItem $item) {
    $host = $this->getEntity();
    if (
      // A composite entity
      $this->entity && $this->entity->getEntityType()->get('entity_revision_parent_id_field') &&

      // Referenced by a new host revision
      !$host->isNew() && $host->isNewRevision() && 

      // Via an untranslatable reference
      !$this->getFieldDefinition()->isTranslatable()
    ) {
      return TRUE;
    }
  }
}

and a CompositeEntityReferencedByNewHostRevisionWithTranslationChanges policy that does:

class CompositeEntityReferencedByNewHostRevisionWithTranslationChanges implements RevisionCreationPolicyInterface {
  function shouldCreateNewRevision(EntityReferenceRevisionsItem $item) {
    $host = $this->getEntity();
    if (
      // A composite entity
      $this->entity && $this->entity->getEntityType()->get('entity_revision_parent_id_field') &&

      // Referenced by a new host revision
      !$host->isNew() && $host->isNewRevision() && 

      // With translation changes
      $host instanceof TranslatableRevisionableInterface && $host->hasTranslationChanges()
    ) {
      return TRUE;
    }
  }
}

The combination of these 2 policies would implement that same behavior as is currently hard-coded within preSave(), so would be backwards compatible. But a new module could remove one or both of these policies and instead add a different policy for handling the last item within the Problem/Motivation section.

Remaining tasks

  • Create a patch or MR that does the above.

User interface changes

API changes

Data model changes

Feature request
Status

Active

Version

1.0

Component

Code

Created by

🇺🇸United States effulgentsia

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

Production build 0.71.5 2024