Problem/Motivation
EntityFormDisplay::getPluginCollections() and EntityViewDisplay::getPluginCollections() enable widget/formatter plugin dependencies to be added to the dependency list of the form/view display. This is supposed to prevent a deleted field dependency from breaking the display. The issue is that plugin collection is keyed by the type of the plugin. Here is the code for the view display, the form display code is nearly identical:
$configurations[$configuration['type']] = $configuration + [
'field_definition' => $field_definition,
'view_mode' => $this->originalMode,
];
Multiple instances of a widget/formatter plugin will all have the same type, which means that if you have more than one instance on a display then the configuration for later instances will overwrite that of earlier instances. Only the configuration of the last instance will be added to the collections array. Therefore, only the dependencies of the last instance will be registered with the display. If a missing dependency is then deleted, the form/view display page will be broken due to a missing dependency exception.
I believe the comment formatter (CommentDefaultFormatter) is an example in core that could have this issue too, as its calculateDependencies() method will add a dependency on the view mode that the formatter is set to use. So, this would probably replicate the issue:
1. Enable comment module.
2. Add a custom view mode for comment entities.
3. Set up two comment fields on a node type.
4. Configure both fields to display in a view mode of the node type, but set each one to show the Comments in a different view mode: one to use the custom one from step 2, and the other to use the default 'Full comment' view mode.
5. Export configuration, or inspect the node type's view mode, and you should find that it is dependent on only one of the comment view modes, rather than both.
That problem in step 5 there could result in configuration being incorrectly synchronised, because a dependency would be missed.
Entity browsers may be the most common other example, as media formatters depend on them.
Proposed resolution
The patch in #9 keys the plugin collection array by the field name to ensure the key is unique in a display. According to the review in #12, re-keying the array should not have an impact on other code. The calculateDependencies() method that aggregates these dependencies does not use the keys at all.
Remaining tasks
Add update hook and tests.
User interface changes
None.
API changes
None.
Data model changes
None.
Issue fork drupal-2865710
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
hchonovThe whole method looks messy. Take a look at it :
What we see is that if two fields use the same plugin the later will be the only one for which configuration of the field definition will be included.
I think here we have to use as key the field name and then at the second level the plugin ID or at first level something like field_name.plugin_id, but this will not be a BC change.
Comment #3
james.williamsI'm glad someone else agrees :-) I need to read up on the BC policy - it seems odd that an actual bug should be allowed to stay just to preserve its existence for BC.
As for keying the array - does it even need keying by plugin ID at all? Why not just field/component name?
Comment #4
james.williamsFrom the '@internal' section of https://www.drupal.org/core/d8-bc-policy :
So maybe fixing this within the two plugin classes would be allowed without waiting for a patch version?
Anyway, more importantly, we need a patch & tests :-) I've attached a really basic attempt at a patch just to make a start on this, no tests yet.
Comment #5
james.williamsFor anyone that needs it (e.g. me!), here's a version that applies to 8.2.x (which was before the great short array syntax change!).
Setting status to needs work, as a test is still needed.
Comment #6
james.williamsTests! One is just tests to demonstrate the current issue, one includes the fix from comment 4.
Comment #9
james.williamsAh... of course adding the test plugins affects the existing tests too. This version adjusts the tests to account for them.
Comment #11
dcam commentedI ran into this issue while developing a new module, Entity Reference Facet Link. Basically, the module provides a field formatter that renders ER fields as links to a faceted search page. To do this, the entity display becomes dependent on one or more facet config entities.* If there is more than one field on an entity that uses this formatter plugin, then only the last one has its dependent facet entity added to the list of display dependencies due to this bug.
FYI, I tracked down the issue where EntityViewDisplay::getPluginCollections() was added: #2271419: Allow field types, widgets, formatters to specify config dependencies.
I'll do a review of the patch next.
*I have not committed the calculateDependencies() code yet because until now I wasn't sure if this was a problem with my implementation or a bug in core.
Comment #12
dcam commentedThis all looks good to me. Great job writing these tests, @james.williams!
I don't think there's any issue with changing the array keys for a couple of reasons. First, the parent issue shows that the getPluginCollections() methods were added specifically to allow plugins to declare their dependencies to these entities. So updating them to fix a bug with that functionality shouldn't be an issue. Second, ConfigEntityBase::calculateDependencies() is the method that calls getPluginCollections() to incorporate those plugins' dependencies. It ignores the collection keys entirely.
Minor nitpicks - "aggregator" is misspelled on lines 326 and 331.
It's misspelled twice here too.
I'm not going to hold up the issue by setting it back to NW or by rolling a new patch myself just for that though. This is something that could easily be fixed on commit. The tests only check for those array values to be the same. There's no reason to try and load those dependencies, which means those strings could have any value.
#9 gets an RTBC from me.
Comment #13
dcam commentedOh. Yeah. For the record I tested to see if this definitely fixes the issue I was having with ERFL. It does. After applying the patch all of the facet dependencies get added.
Comment #14
james.williamsThanks dcam! I'm glad someone else out there has delved deep down far enough into the world of plugins, config and dependencies to run into this too :-)
Comment #16
dcam commentedUnrelated failure.
Comment #17
dcam commentedApplied the issue summary template and rewrote the problem section so that it will be easier to get this some attention.
Comment #18
dcam commentedFormatted the code section correctly. Sorry for the noise.
Comment #19
dcam commentedThis qualifies as a major bug because it can result in a PHP error through the user interface. If a missing dependency is deleted then the form/view display will WSOD. The only workarounds to restore functionality that I know of are to re-add a plugin instance of the dependency with the same machine name or manually edit the configuration.
Comment #20
james.williamsJust added entity browsers as an example to the IS. I'm not sure how they're being implemented on core, but if they use dependencies similarly to the contrib implementation, this will become more common very soon.
Comment #22
larowlanTest fail looks related to me - its to do with field config dependencies being in incorrect order
Comment #23
james.williamsThe test fail looks like it was just a result of an unrelated issue that was solved in #2894499: Rename 'Editorial workflow' to 'Editorial'. Queued for retesting.
Comment #24
james.williamsOh, sorry, I missed this one https://www.drupal.org/pift-ci-job/750198 . So many other unrelated failures!
Comment #25
james.williamsLooks like the test failed because multiple things had the same weight, which came out ordered differently in different environments / versions of PHP. No need for that though, so I've given the test formatter & widget a unique weight so that the ordering can be reliably tested.
I trust this updated patch will resolve that :-)
Comment #26
dcam commentedThis seems likely. I did some digging and the plugin manager uses uasort() on the weight column. The sort order when two elements have the same value is undefined. Assuming the 8.4.x patch comes back green #25 gets an RTBC from me.
Comment #27
larowlanSome duplication here that could be in a protected method called from both tests.
same here
Comment #28
james.williamsI'm not sure if I've gone overboard here.
createField()method to cover the first part you identified as duplicated code.getDefaultTestDisplay()method to the base class, which is one of only two bits that need to be different between the two test classes.testMultipleFieldComponentDependencies()method, calledmultipleFieldComponentDependenciesHelper(). The formatter/widget ID is the second thing that needs to differ between the two test classes.createField()andgetDefaultTestDisplay()base methods, as the classes were littered with code doing those things. If my code containing duplicate code was worth refactoring to use helper methods, I figured that should be cleared up throughout.Maybe I've gone too far now, I don't know, sorry?!
Comment #30
tacituseu commentedComment #31
james.williamsWow, that was obscure. The @group tag on my abstract class had told the tests script to use it. (Which hadn't been a problem when running just the class(es) locally, because that picked them specifically.) Sorry!
I've fixed the code style warning too whilst at it.
Comment #32
dcam commentedPersonally, I would say "yes." I know you were asked to do this, but in fulfilling the request you ended up touching other code that is unrelated to this issue. That can be kind of bad. Now this patch might cause a dozen others to need to be rerolled. Maybe we don't care and duplicate code elimination is more important. I feel like I can't make that call. It would be nice if @larowlan would review it. I'm going to tag it for subsystem maintainer review and hope that gets it some attention from someone who can make the determination as to whether or not this should go forward.
Nitpick that may be able to be fixed on commit: the machine name of the plugin wasn't updated here.
Also, "aggregator" was still misspelled in the dependency test helper method, not that it has any effect at all.
Comment #33
james.williamsYup, I thought that might be the case :-)
@larowlan (or someone else) : What would be the most appropriate way to avoid this duplicate code? "a protected method called from both tests." could be in a base class (like I'd done, but gone beyond in getting other code to use), a helper class/etc somewhere else
There's so much existing code that could be shared between the Display & Form mode tests, as well as within them. My original approach was to just continue with that, and maybe then a follow-up ticket could be opened to change to a proper base class or other way of clearing up all of that.
Comment #34
larowlana helper trait is probably best if they're different test classes
Comment #35
james.williams/me jumps hoop :-)
OK, here we go again then. This scraps what I did for patches 28 & 31, to now use a common trait instead of an abstract base class and to only touch the parts of code touched by the patch in the first place. A follow-up could always be introduced to clear up the other very similar duplicated code between
EntityDisplayTestandEntityFormDisplayTest, to use this trait more, and probably to add all sorts of other common things to it.I've provided the interdiff between this and comment 25, which was the version reviewed when larowlan asked for the duplicate code to be addressed.
There's a still a little duplication between each of the two
testMultipleFieldComponentDependencies()methods, but I can't really see a particularly better way around that, because the fields have to be created before the display mode entity is created (the latter of which is the bit that has to be different between the two classes). (The fields are cached inEntityDisplayBase::fieldDefinitionsat the point of creating the display, and there's no way to reset that.)Comment #36
dcam commentedThis seems ok to me. I don't think it's such a big deal to have the little bit of duplicated code in there, since it's mostly setting up the field names. In the future we might need to add another field to one, but not the other for some reason. Maybe. If that ever happened, then it would be a good idea to let the tests define their own field set.
Anyway, I'm more comfortable RTBCing this since it doesn't touch other code like the last patch did. I only have the same minor nitpicks that I've had before, which I hope can be fixed on commit:
This machine name wasn't updated.
Misspelling that is totally irrelevant to the functionality of the patch.
Comment #37
jofitzFixed the nit-picks in #36.
Comment #38
jofitzThe changes are so minor that we can safely set this back to RTBC.
Comment #39
james.williamsThanks dcam for your reviews, and Jo for catching those bits I kept missing! I hope this is now ready to be committed.
Comment #40
larowlanI think this should be protected?
This always feels like a code smell - perhaps this should be two separate methods?
Actually - doesn't look like we're using the return? So probably can just drop the return? If someone needs it in the future, then we cross that bridge then?
What happens with existing config here?
Do we need an update hook to ensure that all config dependencies are updated accordingly?
I'll ping some folks for the subsystem maintainer review.
Comment #41
james.williamsThanks @larowlan. As far as I'm aware, there is no existing config to update. The only formatters & widgets that core provides that declare dependencies dynamically, unless I'm mistaken, are for comments & images. I've checked, and there are no cases of them being used more than once within an entity display, so the dependencies in any existing config are currently correct as far as my understanding & the scope of this bug go.
Here's an updated patch with the changes from your review.
Comment #42
james.williamsOh by the way, sorry, I was thinking of initial config provided by core (e.g. in /config/install directories). Existing configuration would indeed be missing any extra dependencies until it is re-saved. In the case where this bug has allowed an entity display to be installed, when the dependency is missing, how should Drupal behave?
It's not appropriate to just delete the entity display. Setting the individual component to be hidden/disabled in the display is another option. Some widgets/formatters (including all of core's) might cope to an extent without the missing dependencies (e.g. a missing image style or comment view mode), other might cause fatal errors. But it's worth noting that any such fatal errors would already be happening in that scenario. This is 'just' about hardening the config to declare the dependencies.
If those are the only options, the decision about what to do is probably this:
(A) Retain existing behaviour of formatters & widgets that are missing dependencies - i.e. images show, but without using a preset, comments use the default view mode, or a less-robust contrib formatter/widget continues to cause a fatal error.
vs
(B) Remove formatters & widgets that are missing dependencies - i.e. images & comments in that situation would no longer show, but at least any existing fatal errors from less-robust contrib modules get resolved.
My vote goes to (A)! I could be biased because that also means no update hook work is needed ;-)
What does anyone else following this think?
Comment #43
jofitzI would be against option B (therefore in favour of option A, I guess, in the absence of an option C). Option B could make things (mysteriously) disappear while option A doesn't change the situation (so at least it doesn't make matters worse!).
Comment #44
dcam commentedHeh, I was writing a response to #41 and agreeing with @larowlan while these other comments were being posted.
I think that we do need an update function. Any existing configuration needs to be repaired. It would be really simple because all that needs to happen is re-saving the display entities, which will cause them to recalculate their dependencies. Something like this would probably work:
Comment #45
dcam commentedOf course, it might not be as simple as that since it's likely that update tests will need to be written.
Comment #46
james.nugent7 commentedI tested this with @charlotte.b and we confirmed that the patch fixed the issue.
I followed the steps and was able to reproduce the missing dependency, after applying the patch the dependencies were correct.
I have marked this to needs work as it looks like more work is needed to update existing config.
Comment #47
james.williamsI wouldn't mind writing the update hook, but I've never done so with automated tests specifically for the update before. Can anyone point me to some documentation/guidance about how to do so please?
Comment #51
james.williamsHere's a patch that excludes the tests, which still applies. The full patch still needs a re-roll, as tests have changed.
Comment #52
heatherwoz commentedThe latest comments suggest this needs an update hook and related tests, so it looks like more than a simple reroll. I will update the issue summary.
Comment #54
Adev22 commentedrerolled the patch on #51 and the patch cleanly
Comment #55
hctomI also stumbled about this issue while trying to implement a widget third-party setting and to get things forward, I gave it a try and rerolled the patch including the necessary test rewrites. I hope I didn't miss something.
Comment #56
hctomDon't ask me what happened with the interdiff, but I can't get correct output for it - I guess it is because of the changed test file locations.
When this patch is reviewed successfully it can be used as a base for adding the update hook and the update test coverage.
Comment #57
hctomLet's better hide the unreadable interdiff ;)
Comment #59
hctomHere is an updated patch with updated order on expected select options. Let's see what happens...
Comment #60
hctomTrigger testbot.
Comment #62
hctomOkay, there were some more order fixes required.
Comment #63
hctomSo this looks much better and therefore the reroll should be completed. Remaining tasks are still the update hook to resave all displays to recalculate dependencies correctly and tests for this update operation.
Contrary to the text in the "Remaining tasks" of issue description, I think this problem also interferes with correct dependency calculation for third-party settings. In my case, I have a third party setting for a specific widget type that is just a checkbox. If checked, the third-party setting is added to the widget settings, but if not checked, the third-party setting gets removed from the widget settings in order to have no obsolete dependencies for a feature that is not enabled (implemented via
hook_entity_presave()to clean up the third-party settings array where needed).Without this patch this also results in incorrect dependencies if you use the corresponding widget twice in a form display - when the first widget sets the third-party setting and a latter one doesn't, the required dependency is not added, because the latter one does not register it anymore due to no related third-party setting.
Comment #64
james.williamsThanks hctom for that work on re-rolling!
Agreed, I think it's fair to remove that bit from the issue description. This has become a greater issue over time, as more interesting formatters & widgets have been developed. I haven't kept up to speed with the progress of media widgets & entity browsers in core, but it's entirely possible that this is now an issue with core ones (other than the existing example for comments), I'm not sure.
Comment #65
hctomTo get things moving, here is an updated patch that includes the update hook to resave all entity view/form displays with recalculated dependencies. I'll investigate how to get test coverage for this, but if anybody is more familiar with tests like these, feel free to jump in ;)
Comment #72
acbramley commentedHaven't read the full issue comments but the summary reads well.
Needs tests for the update hook and almost certainly needs a reroll.
Comment #73
acbramley commentedForgot the most important tag :P
This was triaged as part of the daily BSI triage targets.
Comment #76
bhanu951 commentedRe-Rolled patch 2865710-65.patch for 10.1.x branch.
Comment #77
smustgrave commentedLeft MR comments
Comment #79
dcam commentedI messed up that MR while trying to update it for 11.x. I'll sort it out tomorrow.
Comment #83
dcam commentedThis is going to fail tests because I excluded all the entity_test lines from the fixture that I created. But I'm out of time to work on this right now and need to save the work. My plan is to move the test plugins out of the
field_testmodule so that we don't have to depend on that anymore, reducing the amount of garbage that has to be in the fixture.Comment #84
dcam commentedAfter evaluating how much work that would take I ended up putting all of the entity_test module stuff in the fixture. It's a pain, but it worked without having to refactor code to work without it.
Comment #86
larowlanJust one minor comment on the MR, I think its ok, but would be good for a second opinion
Comment #87
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #88
dcam commentedComment #89
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #90
dcam commentedI had to do a major reconfiguration of the tests. The test plugins were originally put into a module that had a dependency on
entity_test. It seems likeentity_testhas been updated recently, which caused the fixture to be out of date. That would be a perpetual problem. We can't have that. So I moved the plugins to an isolated test module without a dependency.Comment #91
dcam commentedComment #93
dcam commentedThe original reason I requested subsystem maintainer review over eight years ago is no longer relevant. There were extensive modifications being made to existing test classes. Those modifications were removed from the patches/MR. I've removed the tag.
Comment #94
dcam commentedPostponing on #3577684: uninstall-contact.php doesn't delete entity displays which prevents these update tests from working.
Comment #95
dcam commentedThis just got unblocked.
Comment #96
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #97
acbramley commented