Store information about a paragraph's parent revision

Created on 20 March 2018, almost 7 years ago
Updated 20 March 2024, 10 months ago

Problem/Motivation

We currently store information about a paragraph's parent entity in basefields:

    $fields['parent_id'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Parent ID'))
      ->setDescription(t('The ID of the parent entity of which this entity is referenced.'))
      ->setSetting('is_ascii', TRUE);

    $fields['parent_type'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Parent type'))
      ->setDescription(t('The entity parent type to which this entity is referenced.'))
      ->setSetting('is_ascii', TRUE)
      ->setSetting('max_length', EntityTypeInterface::ID_MAX_LENGTH);

    $fields['parent_field_name'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Parent field name'))
      ->setDescription(t('The entity parent field name to which this entity is referenced.'))
      ->setSetting('is_ascii', TRUE)
      ->setSetting('max_length', FieldStorageConfig::NAME_MAX_LENGTH);

However the current implementation falls short if we want to fully support revisioning scenarios, because we are currently not storing the parent entity's vid. This leads to issues described in πŸ› Paragraph access check using incorrect revision of its parent, leading to issues editing and viewing paragraphs when content moderation is involved. Needs work and πŸ› Paragraph access check via parent entity incorrectly uses the default revision of the parent instead of the latest Closed: duplicate where the paragraph access control check is buggy because it uses the parent's default revision for access checks instead of the correct one.

Proposed resolution

- (DONE in #2904231: Parent fields are not revisionable β†’ )
- Create a new basefield to store the parent's vid
- Implement an upgrade path to migrate the data of existing sites

Remaining tasks

User interface changes

API changes

Data model changes

πŸ“Œ Task
Status

Active

Version

1.0

Component

Code

Created by

πŸ‡ͺπŸ‡ΈSpain marcoscano Barcelona, Spain

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.

  • πŸ‡³πŸ‡±Netherlands Eric_A
  • πŸ‡¨πŸ‡¦Canada dalin Guelph, πŸ‡¨πŸ‡¦, 🌍

    Another place this pops up:

    1. Use getParentEntity() somewhere.
    2. View a non-default revision of the node. `getParentEntity()` is always returning the default revision of the parent, so you see something broken.

    If the parent is a node there's awkward ways to work around this by getting the route object. But if the parent is another paragraph, you're screwed.

  • πŸ‡ΈπŸ‡ͺSweden twod Sweden

    Yeah, this really bites sometimes.

    We've got a use case doing a lot of programmatic edits to individual paragraphs and had to come up with a helper like this instead of calling getParentEntity() directly:

    The extra logging is there because we've hit all those cases with existing content which previously just called getParentEntity() and assumed it "did the right thing".

    
    use Drupal\Core\Entity\ContentEntityInterface;
    use Drupal\Core\Entity\EntityTypeManagerInterface;
    use Drupal\Core\Entity\RevisionableStorageInterface;
    use Drupal\node\NodeInterface;
    use Drupal\paragraphs\ParagraphInterface;
    
    trait ParagraphHelpers {
    
      protected EntityTypeManagerInterface $entityTypeManager;
      
      /**
       * Get parent revision which actually refrences a paragraph.
       *
       * Paragraph parent references do not include the parent revision, so we may
       * need to check multiple parent revisions to find the one which actually
       * references the specific revision of the passed in paragraph.
       *
       * @param \Drupal\node\NodeInterface $node
       *   The top node.
       * @param \Drupal\paragraphs\ParagraphInterface $paragraph
       *   A paragraph.
       * @param string|null $langcode
       *   The language to load, default to the same as the paragraph.
       *
       * @return \Drupal\Core\Entity\ContentEntityInterface|null
       *   The parent entity revision referencing $paragraph, or NULL if not found.
       */
      protected function getParagraphParentRevision(NodeInterface $node, ParagraphInterface $paragraph, ?string $langcode = NULL): ?ContentEntityInterface {
        // Try the latest parent revision.
        /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $parent_storage */
        $parent_storage = $this->entityTypeManager->getStorage($paragraph->get('parent_type')->value);
        assert($parent_storage instanceof RevisionableStorageInterface, 'Can only handle revisionable entities.');
        $latest_parent_revision = $parent_storage->getLatestRevisionId($paragraph->get('parent_id')->value);
        $parent = $parent_storage->loadRevision($latest_parent_revision);
        /** @var \Drupal\Core\Entity\ContentEntityInterface $parent */
        $current_paragraph_delta = $this->getParagraphDelta($paragraph, $parent);
    
        // Try the default parent revision.
        if ($current_paragraph_delta === FALSE) {
          $parent = $paragraph->getParentEntity();
          $current_paragraph_delta = $this->getParagraphDelta($paragraph, $parent);
        }
    
        // Brute force fallback.
        if ($current_paragraph_delta === FALSE) {
          // Sanity checks.
          while ($parent instanceof ParagraphInterface) {
            $parent = $parent->getParentEntity();
          }
          if (!$parent instanceof NodeInterface || $parent->id() !== $node->id()) {
            $this->logger->error('Something is very wrong with paragraph @id (@rid) in node @nid (@vid)!', [
              '@id' => $paragraph->id(),
              '@rid' => $paragraph->getRevisionId(),
              '@nid' => $parent->id(),
              '@vid' => $parent->getRevisionId(),
            ]);
            throw new \LogicException('The passed in node is not the parent of the paragraph.');
          }
          // We may have a broken node structure if it comes to this, but at
          // least we'll be able to edit it and preserve the structure.
          $tree = $this->getFlatParagraphTree($node);
          $paragraph_data = $tree[$paragraph->id()] ?? NULL;
          if ($paragraph_data
            && $paragraph_data['revision'] === $paragraph->getRevisionId()
            && $paragraph_data['parentType'] === $paragraph->get('parent_type')->value
            && $paragraph_data['parentId'] === $paragraph->get('parent_id')->value
          ) {
            if ($node->isLatestRevision() || $node->isDefaultRevision()) {
              $this->logger->notice('Paragraph @id (@rid) in node @nid (@vid) was not referenced from the latest or default', [
                '@id' => $paragraph->id(),
                '@rid' => $paragraph->getRevisionId(),
                '@nid' => $parent->id(),
                '@vid' => $parent->getRevisionId(),
              ]);
            }
            /** @var \Drupal\Core\Entity\ContentEntityInterface|null $parent */
            $parent = $parent_storage->loadRevision($paragraph_data['parentRevision']);
            $current_paragraph_delta = $parent instanceof ContentEntityInterface ? $this->getParagraphDelta($paragraph, $parent) : FALSE;
          }
        }
    
        if ($current_paragraph_delta === FALSE) {
          $this->logger->error('Paragraph @id (@rid) in node @nid (@vid) was not referenced from any parent.', [
            '@id' => $paragraph->id(),
            '@rid' => $paragraph->getRevisionId(),
            '@nid' => $parent->id(),
            '@vid' => $parent->getRevisionId(),
          ]);
        }
    
        return $current_paragraph_delta !== FALSE ? $parent : NULL;
      }
    
      /**
       * Get at which delta in the parent field a paragraph is referenced.
       *
       * @param \Drupal\paragraphs\ParagraphInterface $paragraph
       *   A paragraph.
       * @param \Drupal\Core\Entity\ContentEntityInterface $parent
       *   The parent element.
       *
       * @return false|int|string
       *   The delta as an int/string or FALSE if not found.
       */
      protected function getParagraphDelta(ParagraphInterface $paragraph, ContentEntityInterface $parent) {
        $field_items = $parent->get($paragraph->parent_field_name->value);
        foreach ($field_items as $delta => $item) {
          // Explicitly compare ids first to avoid loading a new instance of the
          // referenced revision, which gives a clone since they are not cached in
          // storage, but may be cached on the parent's reference field item.
          if (
            (
              !$paragraph->isNew()
              && (
                $item->target_revision_id === $paragraph->getRevisionId()
                && $item->target_id = $paragraph->id()
              )
            ) || (
              $paragraph->isNew()
              && !isset($item->target_revision_id)
              && $item->entity
              && $item->entity === $paragraph
            )
          ) {
            return $delta;
          }
        }
        return FALSE;
      }
    
    }
    
  • πŸ‡·πŸ‡΄Romania amateescu

    Just closed πŸ› Nested paragraphs automatically publish, even if the parent is a draft Closed: duplicate as a duplicate of this issue, updating the parent of this one.

  • πŸ‡ΊπŸ‡¦Ukraine abyss

    Hi @amateescu, this is a bit strange, if you think that there is a duplicate problem here, then shouldn't you close this issue (child) instead of the parent issue which is described in #2807371: META Support Content Moderation module β†’ ?

Production build 0.71.5 2024