Problem/Motivation

Duplicating a paragraph whose nested (child) paragraphs are still new/unsaved causes the original and the duplicate to share the same child entity after save, editing one changes the other.

This is a regression introduced in #3495373: Support duplicate hook. That issue prepared paragraphs for Drupal core 11.2+, where core invokes hook_entity_duplicate() from EntityBase::createDuplicate() for entity duplication. To avoid duplicating them twice on 11.2+, it added a !$item->entity->isNew() check.

The problem is that this check is not specific to 11.2+ and could run on legacy Drupal 10 sites, that's the reason for this regression. The isNew() check ends up skipping any nested paragraph that hasn't been saved yet, so it's never duplicated, the original and the copy end up pointing at the same one instead of getting their own.

Steps to reproduce

1. Go to https://simplytest.me
2. Launch a site using Paragraphs 8.x-1.x-dev with Drupal version < 11.2
3. Create 2 new paragraph types, a) Text containing a text field and b) Section containing a paragraph reference field of Text
4. Update the Basic page content type to contain a Content field that's a paragraph reference field of Section
5. Add a new Basic page
6. Add some text to the first Section > Text, e.g. "Hello World"
7. Now click on the "..." of the Section and click "Duplicate"
8. Update the text of cloned Section > Text to something else, e.g. "Hello World2"
9. Save the node
10. Both text will display "Hello World2"

Proposed resolution

Version-gate the manual nested duplication so it only runs where core does not fire hook_entity_duplicate() (<11.2), and remove the !isNew() guard from that path. Two options:

A: Guard the isNew() check for 11.2+ sites only, or simply early return before the loop: a smaller scoped fix, basically reverting to what it was for Drupal 10 sites but keeping it working for 11.2+ sites. Lowest risk with no new code paths on Drupal 10.

B: Replace the for loop with the new entity_duplicate hook and guard it for Drupal 10 sites only: one duplication implementation + translation-consistency, but it expands the scope of a bug fix ticket, since the entity_duplicate hook was only designed/tested for the newer flow and we'd be bringing it into an environment it wasn't validated against.

Either way, add test coverage for the <11.2 path with a dedicated kernel test that duplicates a paragraph with an unsaved nested child and asserts that the original and duplicate get independent children.

Issue fork paragraphs-3510842

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

peterwcm created an issue. See original summary.

arunsahijpal’s picture

Assigned: Unassigned » arunsahijpal

arunsahijpal’s picture

Assigned: arunsahijpal » Unassigned
Status: Active » Needs review

Hi @peterwcm,
I've updated the createDuplicate method please check.

Also @berdir pls check is this a good solution?

Thanks,
Arun

peterwcm’s picture

Status: Needs review » Needs work

Hi @arunsahijpal,

Thanks for working on this. The patch fixes the issue, but it causes orphaned paragraphs to be created before users save the node. I think there could be issues with translations as well.

I'm puzzled about the !$item->entity->isNew() condition, as it seemed to work fine without it before. Could we consider removing that condition instead? @berdir

Regards

Peter

arunsahijpal’s picture

Status: Needs work » Needs review

@peterwcm,

I've updated the code and now no more orphaned paragraphs and everything saves when the node saves by referencing the entity directly instead of manually setting target_id.
and I also think this condition !$item->entity->isNew() is not necessary.
For the translation thing there is one limitation that - Paragraphs are referenced entities, not simple text fields.
Drupal does not natively support translating entity reference fields.
When I tried to translate it shows this error

* (unsupported) Paragraphs fields do not support translation

. Yo can checkout this link for more info .

I think there is one tot check translation with Paragraphs Library module but I'm not sure.
Could you pls check it, if it works then I think we can safely remove that condition.

peterwcm’s picture

Status: Needs review » Reviewed & tested by the community

Hi @arunsahijpal,

Thanks for addressing this so quickly.
I just did some quick tests, and the latest patch has fixed the issues.

alorenc changed the visibility of the branch 3510842-cloning-nested-paragraph to hidden.

alorenc changed the visibility of the branch 3510842-cloning-nested-paragraph to active.

berdir’s picture

Status: Reviewed & tested by the community » Needs work

The check is in preparation for the new hook in Drupal 11.2 to avoid additional duplication. The changes remove the relevant todo and there is no test coverage for the bug that this seems to fix.

We have fairly extensive duplication tests, if there is a bug then that should be covered by tests.

dxvargas’s picture

This problem is not happening when using drupal core 11.2 In this case, the \Drupal\paragraphs\Hook\EntityHooks::duplicate is doing the job.
@peterwcm, @arunsahijpal can you please tell us if you're using a version of drupal prior to 11.2?

dxvargas’s picture

Still, I want to confirm that there is a bug when using Drupal core prior to 11.2 (when the new hook "entity_duplicate" is not run).

It happens when we duplicate a new introduced paragraph (with nested paragraphs).
In that case the new condition !$item->entity->isNew() is FALSE and the nested paragraphs are not duplicated. They end up being used in the original and in the duplicated paragraph.
Sorry that I can't provide a test ATM.

Just removing the new condition !$item->entity->isNew() fixes this.

peterwcm’s picture

Hi @dxvargas,

Yes, we are now using Drupal 10.4.6. I couldn't remember the version I tested when using simplytest.me, but looking at the options, it would also be prior to Drupal 11.

vikramsaini1609 changed the visibility of the branch 3510842-cloning-nested-paragraph to hidden.

vikramsaini1609 changed the visibility of the branch 3510842-cloning-nested-paragraph to hidden.

vikramsaini1609’s picture

I am not able to add the https://www.drupal.org/project/paragraphs/issues/3079729 patch with this patch. Instead of writing whole function createDuplicate(). Can we simply remove the !$item->entity->isNew() condition line?

vikramsaini1609 changed the visibility of the branch unsaved-nested-paragraphs-3510842 to active.

vikramsaini1609 changed the visibility of the branch unsaved-nested-paragraphs-3510842 to hidden.

vikramsaini1609 changed the visibility of the branch unsaved-nested-paragraphs-3510842 to active.

vikramsaini1609 changed the visibility of the branch paragraph-nested-nay-entity-3510842 to hidden.

herved’s picture

Per #11-12, what about centralizing code then?
Update \Drupal\paragraphs\Entity\Paragraph::createDuplicate to something like

$duplicate = parent::createDuplicate();
if (version_compare(\Drupal::VERSION, '11.2.0', '<')) {
  /** @var \Drupal\paragraphs\Hook\EntityHooks $service */
  $service = \Drupal::service(\Drupal\paragraphs\Hook\EntityHooks::class);
  $service->duplicate($duplicate, $this);
}

Then it's clear that this code can be removed once <11.2 is no longer supported.

mediabounds’s picture

I tried the suggestion in #29—I had to manually instantiate the class since it is not a service, but it resolved the problem we were having with duplicated child references (as of paragraphs 1.19.0 in Drupal 10.5).

Here's the full implementation of createDuplicate we're using:

/**
 * {@inheritdoc}
 */
public function createDuplicate() {
  $duplicate = parent::createDuplicate();
  if (version_compare(\Drupal::VERSION, '11.2.0', '<')) {
    $hooks = new \Drupal\paragraphs\Hook\EntityHooks();
    $hooks->duplicate($duplicate, $this);
  }

  return $duplicate;
}
andreasderijcke’s picture

Title: Cloning nested paragraph creates shared child reference » Cloning a new nested paragraph creates shared child reference
Issue summary: View changes

I've updated the issue title and description to emphasize the fact that the paragraph that is being cloned must be new aka not yet saved to the database.

This detail eluded myself and predecessor looking into this issue in some projects, making us doubt if the proposed patch here would fix our issue.

Without the patch, when cloning a new nested paragraph, the respective child paragraphs will be created as different revision of the same paragraph, each referring to different parent paragraph.
You can verify this when comparing the records in paragraphs_item_field_data table versus paragraphs_item_revision_field_data for given node and parent paragraphs.

With patch, you can see each child paragraph being created as separate entities.

andreasderijcke’s picture

Also, after more testing, we noticed issues when the nested paragraphs contained entity reference fields to Media, and in our case Bynder Media.
In the duplicates, the Media references disappeared saving the cloned paragraphs with the patch applied.

I didn't debug what happens in de creatDuplicate function, but it made sense that entity references to anything other than the (child) paragraphs, should not be duplicated (like nodes, users, terms,...).

For our project scope, an additional check on the entity being a paragraph, fixed the unwanted side behaviour:

          ...
          // Duplicate child paragraphs, remove when requiring 10.2+.
          // @see \Drupal\paragraphs\Hook\EntityHooks::duplicate()
-         if ($item->entity && !$item->entity->isNew()) {
+        if ($item->entity && $item->entity->entityTypeId === 'paragraph') {
              $fieldItemList[$delta] = $item->entity->createDuplicate();
          }
          ...

After checking the entity_duplicate hook, it seems that the if above could be the same as https://git.drupalcode.org/project/paragraphs/-/blob/8.x-1.x/src/Hook/En... (same check, just different way to do it and actually prefer the 'instance of' approach).

berdir’s picture

Are you on D10? Per recent comments, this should not happen on D11.2+, nor should it happen when using the replicate module.

The MR is definitely not correct and should not be used. Feel free to create a new MR with the proposal from #29/#30 as a workaround for sites stuck on older core versions.

herved’s picture

For #30 the EntityHooks class can be registered as service, see #3079729: Remove file on original/duplicated paragraph removes the file on all original/duplicated paragraphs's MR for example.

siva01 made their first commit to this issue’s fork.

peterwcm’s picture

Issue summary: View changes
peterwcm’s picture

Issue summary: View changes
peterwcm’s picture

Thanks everyone for contributing to the discussion. I’ve updated the ticket with clearer details on the issue and the proposed solutions.

To summarise, there appear to be two options:
1. Keep the isNew() workaround, but apply it only to Drupal 11.2+, or remove the check while skipping the relevant createDuplicate() loop for 11.2+.
2. Replace the loop with the new EntityHooks->duplicate() implementation, while still skipping it for 11.2+.

There seems to be growing support for reusing the duplicate hook. While this would be cleaner and improve consistency, it also increases the scope, as test coverage would need to be added for the hook itself. The existing tests currently only run against Drupal 11.2+.

Given that this bug can cause significant data corruption, my preference would still be a smaller, lower-risk fix such as option 1: restoring the previous working behaviour for Drupal 10 sites while retaining compatibility with 11.2+. That said, as the issue has already been open for more than a year, I understand the preference to take the opportunity to consolidate the implementation. @berdir, let me know what you think.

@herved While we could manually register the hook class as a service, the module already supports Drupal 11, where hook classes are automatically registered as services from 11.1+. Manually registering it would double-define it on newer sites and leave more code to clean up once Drupal 10 support is removed. I think @mediabounds’ suggestion would introduce less technical debt.

peterwcm changed the visibility of the branch unsaved-nested-paragraphs-3510842 to hidden.

peterwcm changed the visibility of the branch 8.x-1.x to hidden.

peterwcm changed the visibility of the branch cloning-nested-paragraph-3510842 to hidden.

peterwcm’s picture

Status: Needs work » Needs review
berdir’s picture

> @herved While we could manually register the hook class as a service, the module already supports Drupal 11, where hook classes are automatically registered as services from 11.1+. Manually registering it would double-define it on newer sites and leave more code to clean up once Drupal 10 support is removed. I think @mediabounds’ suggestion would introduce less technical debt.

No, manually registering does not duplicate the service if it's using the same name. But that alone would not do anything. The reason this is not done here is deliberate, this hook does not exist in D10.

anybody’s picture

Issue summary: View changes
benstallings’s picture

Version: 8.x-1.19 » 8.x-1.x-dev
Status: Needs review » Reviewed & tested by the community

Passes review and does what it says.
No functional problems found. The change is minimal, correctly scoped to the reported regression, and the regression test is sound for what it can test in this environment.

peterwcm’s picture

Thought I’d add some context around my PR, as I meant to explain this earlier:

  1. Added a unit test to reproduce the issue.
  2. Took the idea from #29 and #30 and added a version-check safeguard. However, I intentionally didn’t use the same EntityHooks path for older Drupal 10 versions for two reasons:
    1. We’d need to manually register the service, which adds some YAML that becomes redundant in Drupal 11 where the service is auto-registered.
    2. Looking at EntityHooks, it does more than the current loop and introduces additional logic. Bringing that into Drupal 10 as part of this bug fix would expand the scope and increase the risk of unrelated regressions.

So for Drupal 10, I’ve simply kept it on the previously working behaviour instead.

The original intent of this issue is a bug fix, so I think it would be better to keep the scope and impact as small as possible. Bringing EntityHooks to older Drupal versions can be handled in a separate issue if needed.