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
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
Comment #2
arunsahijpal commentedComment #4
arunsahijpal commentedHi @peterwcm,
I've updated the createDuplicate method please check.
Also @berdir pls check is this a good solution?
Thanks,
Arun
Comment #5
peterwcm commentedHi @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? @berdirRegards
Peter
Comment #6
arunsahijpal commented@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
. 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.
Comment #7
peterwcm commentedHi @arunsahijpal,
Thanks for addressing this so quickly.
I just did some quick tests, and the latest patch has fixed the issues.
Comment #10
berdirThe 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.
Comment #11
dxvargas commentedThis problem is not happening when using drupal core 11.2 In this case, the
\Drupal\paragraphs\Hook\EntityHooks::duplicateis doing the job.@peterwcm, @arunsahijpal can you please tell us if you're using a version of drupal prior to 11.2?
Comment #12
dxvargas commentedStill, 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.Comment #13
peterwcm commentedHi @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.
Comment #16
vikramsaini1609 commentedI 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?Comment #29
herved commentedPer #11-12, what about centralizing code then?
Update \Drupal\paragraphs\Entity\Paragraph::createDuplicate to something like
Then it's clear that this code can be removed once <11.2 is no longer supported.
Comment #30
mediabounds commentedI 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:
Comment #31
andreasderijckeI'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.
Comment #32
andreasderijckeAlso, 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:
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).
Comment #33
berdirAre 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.
Comment #34
herved commentedFor #30 the
EntityHooksclass 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.Comment #37
peterwcm commentedComment #38
peterwcm commentedComment #39
peterwcm commentedThanks 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 relevantcreateDuplicate()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.
Comment #44
peterwcm commentedComment #45
berdir> @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.
Comment #46
anybodyComment #47
benstallings commentedPasses 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.
Comment #48
peterwcm commentedThought I’d add some context around my PR, as I meant to explain this earlier:
EntityHookspath for older Drupal 10 versions for two reasons: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
EntityHooksto older Drupal versions can be handled in a separate issue if needed.