Problem/Motivation

If display are created with a theme and this theme is uninstalled, display will fail with a fatal component error, which is ok.
But all display with a component from this theme will be deleted (Page layout, entity view, views).
Probably same with a module.

If an entity display is created with a field and this field is deleted, display will fail with a fatal error.

Steps to reproduce

Proposed resolution

Should we alert or keep the instances?

Remaining tasks

User interface changes

API changes

Data model changes

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

mogtofu33 created an issue. See original summary.

mogtofu33’s picture

Title: Theme or module uninstalled » Field, Theme or module uninstalled
Issue summary: View changes
pdureau’s picture

pdureau’s picture

But all display with a component from this theme will be deleted (Page layout, entity view, views).

Maybe we need to implement ConfigEntityInterface::onDependencyRemoval():

This method allows configuration entities to remove dependencies instead of being deleted themselves. Configuration entities can use this method to avoid being unnecessarily deleted during an extension uninstallation. For example, entity displays remove references to widgets and formatters if the plugin that supplies them depends on a module that is being uninstalled.

pdureau’s picture

Issue tags: +Novice
pdureau’s picture

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

prince.wang’s picture

Assigned: Unassigned » prince.wang
Status: Active » Needs review
pdureau’s picture

Thanks, i will have a look

pdureau’s picture

Assigned: pdureau » Unassigned
Status: Needs review » Needs work

Hello,

Thanks for your proposal and sorry for my late answer (i was in a Drupal event in Greece).

So, first what I understand. You have used ConfigEntityInterface::onDependencyRemoval() on 2 config entities:

  • EntityView: on EntityViewDisplay config entity
  • PageLayout: on PageLayout config entity

There, you call the newly added DisplayBuildableInterface::removeDependency() method, implemented in DisplayBuildablePluginBase. That's nice to centralize the logic in the plugin like that.

Also, I believe it was a good decision to add a ::removeDependency() method instead of implementing RemovableDependentPluginInterface::onCollectionDependencyRemoval() because the "buildable" plugins are not attached to PageLayout and EntityView displays as "plugin collections".

Feedbacks:

  • Most of the logic resides in src/SourceTree.php. SourceTree has been created to isolate the tree alteration logic from the Drupal logic, it must stay very agnostic of what is happening CMS side. Can we do differently? (i am asking but i honestly have no clue)
  • EntityViewOverride: is not covered, and that's OK, I guess, because i am not sure it is possible for content entities and it belongs to UI Patterns scope anyway
  • ViewDisplay: a specific removeDependency() method has been implemented. How is it triggered?
prince.wang’s picture

Hi pdureau ,
Thanks for your review:

Most of the logic resides in src/SourceTree.php. SourceTree has been created to isolate the tree alteration logic from the Drupal logic, it must stay very agnostic of what is happening CMS side. Can we do differently? (i am asking but i honestly have no clue)
EntityViewOverride: is not covered, and that's OK, I guess, because i am not sure it is possible for content entities and it belongs to UI Patterns scope anyway
ViewDisplay: a specific removeDependency() method has been implemented. How is it triggered?

  • 1. SourceTree has all nodes with flat structure , maybe is the best place to make change
  • 2. is not covered
  • 3. it's not triggered indeed and i have an idea not too good:
    1. DisplayBuilderArea (area plugin) — A Views area handler (#[ViewsArea("display_builder_area")]) implementing DependentWithRemovalPluginInterface. It renders nothing; it's a sentinel that lives in the View's header section only when Display Builder is enabled for that display.
    2. display_builder_views.views.inc — hook_views_data() registers the area handler in $data['views']['display_builder_area'] so ViewsHandlerManager::getHandler() can find it. Without this, Views falls back to a broken handler.
    3. DisplayExtender::submitOptionsForm() — When a user enables Display Builder on a display, it calls addDisplayBuilderArea(); when disabling, removeDisplayBuilderArea().
    4. addDisplayBuilderArea() — Calls $this->displayHandler->setOverride('header', FALSE) to mark the header as overridden (otherwise getOption('header') inherits from the default display and ignores our area). Then adds the area handler config via setOption('header', ...).
    5. removeDisplayBuilderArea() — Checks isDefaulted() per section first — skips if inherited from default. Otherwise removes the area from header/footer/empty.
    6. Dependency handling via the area plugin:
    - calculateDependencies() → Gets ViewDisplay buildable from $this->displayHandler->getExtenders(), iterates getSources(), calls SourcePluginManager::getSource() on each, merges source dependencies into the View.
    - onDependencyRemoval() → Calls ViewDisplay::removeDependency() which runs SourceTree::onDependencyRemoval() to prune orphaned source nodes.
pdureau’s picture

resolved all threads

So is it back to review?

pdureau’s picture

prince.wang’s picture

Can't find a good way to handle views dependencies.

andres alvarez’s picture

MR !269 is 128 commits behind 1.0.x and no longer merges: it conflicts in ViewDisplay.php, since the sources storage moved to ConfigBuildablePluginBase and saveSources() is gone from that class.

I rebased it locally. ViewDisplay::removeDependency() now uses saveDefaultSources() instead of writing to the view storage by hand. The existing SourceTree, EntityViewDisplay, PageLayout and DisplayExtender kernel tests pass. The original MR did not ship tests of its own, so I will add them (entity view, page layout and views, plus a node with children in slots).

To avoid rewriting @ipumpkin's history I will push it to a separate branch of the fork (3562750-rebase) unless you would rather have a force push on the MR branch.

andres alvarez’s picture

Update on MR !462 (the rebase of !269 on 1.0.x, on a separate branch to keep @ipumpkin's history intact). Kernel tests are in for SourceTree, entity view, page layout and views, including a real theme uninstall and a field deletion. They exposed three flaws in the original approach, now fixed: calculateDependencies() read stale stored sources, so deleting a field still deleted the display; source plugins were created without contexts, which is a fatal error with entity_field sources; and getPluginRemovedDependencies() read an unchecked array key.

Checked on a real site by uninstalling a module that provides a component used by an entity view display: on 1.0.x the display is deleted; with this MR it is kept and only loses the dependent source. php-cs-fixer style is applied and the pipeline is green.

Still open: Views. View has no onDependencyRemoval() here, so uninstalling something does not prune a view's sources; only ViewDisplay::removeDependency() is covered, by a direct call. @pdureau, one design question: when a node with a broken dependency is removed, its slot children go with it even if they are independent. Should they be kept?

andres alvarez’s picture

Status: Needs work » Needs review
mogtofu33’s picture

Assigned: Unassigned » mogtofu33
mogtofu33’s picture

Assigned: mogtofu33 » Unassigned
Status: Needs review » Needs work

Thanks @andres-alvarez, the tests make this reviewable. !462 rebases cleanly on 1.0.x after #3627285: Profile, page layout, override and display interfaces: setters return static, revert() split, docblocks and the four kernel test classes pass.

1. Translation leak. DisplayBuildablePluginBase::removeDependency() starts from $this->getSources(). For config buildables that is ConfigBuildablePluginBase::getSources(), which merges the language override of the URL language. Uninstalling a theme or deleting a field from a /de/ admin page writes the German values into the default config. Reproduced in a kernel test, both with a direct onDependencyRemoval() call and through ConfigManager::getConfigEntitiesToChangeOnDependencyRemoval().

Prune the sources the entity already holds in memory instead. That is also what gets saved:

public function onDependencyRemoval(array $dependencies) {
  $changed = parent::onDependencyRemoval($dependencies);
  $tree = new SourceTree($this->getSources());

  if ($tree->onDependencyRemoval($dependencies, $this->getInstance()?->getAvailableContexts() ?? [])) {
    $this->setSources($tree->getTree());
    $changed = TRUE;
  }

  return $changed;
}

Same in EntityViewDisplayTrait, from getThirdPartySetting('display_builder', DisplayBuildableInterface::SOURCES_PROPERTY, []), as calculateDependencies() already does in this MR.

2. With that, DisplayBuildableInterface::removeDependency() has no caller left. Please drop it from the interface, the base class and ViewDisplay. It would be a new public contract right before RC1, and its meaning differs per implementation: the base returns sources, ViewDisplay saves them.

3. Views. Please drop ViewDisplay::removeDependency() and its test from this MR. Nothing calls it. The View does not depend on the component providers either: the display extender declares no dependencies, so an uninstall leaves the View in place with broken sources. Saving inside a dependency removal would also write config during the dry run of the uninstall confirm form. Views needs its own follow-up.

4. Draft. Only the published sources are pruned. The instance draft keeps the removed node, so the builder still shows it and the next publish writes it back. This can't happen in onDependencyRemoval() because of the dry run, so it is a follow-up too.

5. Children in slots (#18): keep the current behavior. Core does the same in LayoutBuilderEntityViewDisplay::onDependencyRemoval(), which drops a whole section with its components when the layout plugin loses its dependency. A line in the docblock is enough.

Minor:

  • SourceTree::onDependencyRemoval(): $changed can go, return $node_ids_to_remove !== [];
  • getPluginRemovedDependencies() copies EntityDisplayBase::getPluginRemovedDependencies(), add a @see.
  • The removed() test helper is copied in four test classes.
andres alvarez’s picture

Assigned: Unassigned » andres alvarez
andres alvarez’s picture

I applied the review from Oct 5 (commit 2f01763) and fixed the PHPStan errors in 0c6284a.

Changes from the review
1. Translation leak: PageLayout::onDependencyRemoval() and EntityViewDisplayTrait::onDependencyRemoval() now prune a SourceTree built from the entity's in-memory sources instead of going through the buildable. The URL language override can no longer end up in the default config. I added testDependencyRemovalInGermanKeepsDefaultConfigUntranslated to PageLayoutMultilingualPublishTest and EntityViewMultilingualPublishTest. It fails on the previous code ('Hallo' instead of 'Hello') and passes now.
2. Removed DisplayBuildableInterface::removeDependency() and its implementation in DisplayBuildablePluginBase.
3. Removed ViewDisplay::removeDependency() and its tests. Views is left for a follow-up.
4. Draft sources are still not pruned (follow-up).
5. Slot children: behavior is unchanged and now documented in the SourceTree::onDependencyRemoval() docblock.
6. Cleanups: simpler return in SourceTree, added @see to getPluginRemovedDependencies(), and the removed() helper now lives in a shared trait (DependencyRemovalTrait).

Pipeline
- The 5 PHPStan errors were pre-existing, in code this MR doesn't touch (assign.overwritten and assign.unused). I fixed them in a separate commit (0c6284a) by removing the dead assignments, with no behavior change. If you prefer to keep them out of this MR, that commit can be dropped without affecting the rest.
- phpunit still fails on 9 Views tests (ViewsSourceOptionsTest and ViewsSourceRenderTest). They were failing before this MR and are unrelated to it. No SourceTree, entity_view or page_layout test fails.

Note: LayoutSource::calculateDependencies() had a line that looked meant to record the layout_discovery dependency but never reached $dependencies. I removed the dead line without changing behavior. If the intent was to emit that dependency, it should go in a separate issue.

andres alvarez’s picture

Status: Needs work » Needs review
andres alvarez’s picture

Assigned: andres alvarez » Unassigned
pdureau’s picture

Status: Needs review » Needs work

Thanks Andres for the update.

However, I am not able to do a proper review for 2 reasons:

  • There are a few phpunit fails in Drupal\Tests\display_builder_views\Kernel\ViewsSourceOptions, can you check them locally?
  • There are some merge conflicts, can you rebase your branch?
andres alvarez’s picture

Oops, sorry—I didn't check the PHPUnit code. I'll fix it right away.

andres alvarez’s picture

Assigned: Unassigned » andres alvarez
andres alvarez’s picture

Status: Needs work » Needs review
andres alvarez’s picture

Assigned: andres alvarez » Unassigned
pdureau’s picture

Thanks. I guess this review ill be easier than the previous ones because the changes are now located in only 3 places:

  • EntityViewDisplayTrait::onDependencyRemoval()
  • PageLayout::onDependencyRemoval()
  • SourceTree::onDependencyRemoval()
csakiistvan’s picture

Assigned: Unassigned » csakiistvan
csakiistvan’s picture

Assigned: csakiistvan » Unassigned
Status: Needs review » Reviewed & tested by the community
StatusFileSize
new315.76 KB
new256.03 KB
new405.08 KB

✅ Tested and works — on 1.0.x, a Basic page view display whose Display Builder sources used a string field was listed under "Configuration deletions" on the field's delete confirm page; after applying MR !462 and rebuilding caches, the same page lists it only under "Configuration updates", and deleting the field kept the display with only the textfield source left in its stored sources. Field deletion no longer deletes the display.

pdureau’s picture

Assigned: Unassigned » mogtofu33

mogtofu33’s picture

Assigned: mogtofu33 » Unassigned
Status: Reviewed & tested by the community » Fixed

Rebased with #3594228: Remove explicit references to Layout Builder, which removed the entity class replacement.
Entity view displays no longer depend on their sources, so a field deletion already keeps them, and this MR now covers page layouts only.
A pruned page layout also gets disabled: it loses its theme dependency, and would otherwise show on every theme.

Follow-up for small gaps:

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

pdureau’s picture

Status: Fixed » Closed (fixed)

Shipped in beta10 release.