Problem/Motivation
Implementing the \Iterator class is problematic when there can be the potential for a nested loop.
See http://php.net/manual/en/class.iterator.php#104720
In places we internally loop through a plugin, and then call a method on the plugin.
In Views, the plugins have access to the plugin collection itself, and they may need to loop over all of the plugins.
See ViewExecutable::validate().
Original report
While working on #2428643: Always display the more link causes exception: cannot create a URL to a display without routes I noticed a problem with the ViewExecutable::validate() method. The problem is that it loops through a lazy loaded /Iterable array (DisplayPluginCollection) in order to call the validate() method on each display plugin of the view. The problem is that each display plugin has these handy methods of retrieving the display plugin that has a path (is routed). This happens by looping through all the existing plugins till it finds one that has a path (see DisplayPluginBase::getLinkDisplay()).
Currently all works because none of these methods are used inside of the display plugin's validate() method. But try using one and the whole looping thing breaks down because the pointer gets reset for each plugin so you end up not validating all the display plugins.
Proposed resolution
Remove the \Iterator implementation, and add two new methods. LazyPluginCollection now has getInstances() and getInstanceIds(), which should be used instead.
Remaining tasks
Write patch and test.
User interface changes
None.
API changes
All places that loop over a plugin collection must call ->getInstances()
.
Original report by @Upchuk
Comment | File | Size | Author |
---|---|---|---|
#32 | 2431379-lazyplugincollection-32.patch | 4.44 KB | tim.plunkett |
#30 | 2431379-lazyplugincollection-30.patch | 34.06 KB | tim.plunkett |
#30 | interdiff.txt | 737 bytes | tim.plunkett |
#1 | 2431379-0.patch | 639 bytes | Upchuk |
#6 | 2431379-test-6.patch | 1.29 KB | Upchuk |
Comments
Comment #1
Upchuk CreditAttribution: Upchuk commentedComment #2
dawehnerYes that fixes the problem but its an underlying problem of the architecture, is there any way to do it properly?
One example to solve that would be to disallow iterating but you can just request all IDs so you have to do the
->get()
for your own.Comment #3
Upchuk CreditAttribution: Upchuk commentedEither that or create a getter for all the instances and loop in there. If we do this, does it still make sense to implement the /Iterator and lazy loading?
Comment #4
tim.plunkettWow. Can we write tests for this first?
Comment #5
dawehnerI think the only sane thing to do is to remove iterator support from the lazy plugin collection and instead expose a getAllIds method which allows you to safely travel over all entries.
Comment #6
Upchuk CreditAttribution: Upchuk commented@tim.plunkett Here is a failing test for ya :) Could maybe be moved to a unit test. But it's just demonstrating in code the problem I wrote about in the issue summary.
This part:
is there just to make sure the loop doesn't go on forever.
Comment #8
tim.plunkettI'm assuming this will be completely broken.
Comment #10
tim.plunkettRight, that breaks the lazy-load. It'll be really sad to lose the Iterator-ability, but I don't know what else to do.
Comment #11
Upchuk CreditAttribution: Upchuk commentedWell, the iterator ability can be replaced by a getter method that does the iteration. So instead of :
It can be :
The problem is that it's difficult to figure out where the first type of iteration takes place and fix them all. Many a failing tests I see down this path :)
Comment #12
xjmBrr.
Comment #13
dawehnerI'd honestly get rid of iterators, if possible. Its magic which makes things hard to understand AND its problematic in terms of potential bugs.
Here is a patch which removes the iterator support entirely, let's see see how many failures are left.
Comment #15
tim.plunkettFixing a couple obvious places just to see some test results.
Comment #17
tim.plunkettComment #19
tim.plunkettFixing more failures.
Comment #20
tim.plunkettComment #21
tim.plunkettUpdating the issue summary.
Comment #23
tim.plunkettFixing filters
Comment #25
tim.plunkettI'm worried about the implementation of getInstances(), but let's get this green first.
Comment #26
Upchuk CreditAttribution: Upchuk commentedNice one Tim. What is your worry regarding getIntances()?
Comment #27
Upchuk CreditAttribution: Upchuk commentedAlso, do we need any further tests for this?
Comment #28
tim.plunkettIdeally calling getInstances() twice in a row wouldn't cause all of those method calls. But I can't figure out a way to avoid that without causing more problems.
We still need tests that prove this is a problem in real code, without manually nesting two loops in the test.
Comment #29
tim.plunkettUnless anyone else has ideas about how to make subsequent calls to getInstances() faster, I think this is done code-wise.
It needs a CR draft and the beta evaluation.
Comment #30
tim.plunkettOops, left in some code from a previous approach.
Comment #32
tim.plunkettHere is an alternate version of the patch, with \IteratorAggregate instead. This is less explicit, and still has the issue of not iterating lazily, but it's not an API break.
Comment #33
effulgentsia CreditAttribution: effulgentsia commentedI think #32 makes a lot of sense. I see no downside with it.
I think that's ok. How often do we have performance-critical code that starts iterating a plugin collection and exits part-way through? And if we do have that, then we can always have a follow-up to implement an alternate iterator that is lazy. So I think this non-API-changing approach is better than #30 where the lack of laziness becomes baked into the API for no reason and is then harder to change later.
Comment #34
tim.plunkettA semantic point in favor of #32: the name *Collection is a strong implication that it can be iterated over.
Finally, my worry is that removing any iteration support will cause
foreach ($collection as...)
to fail silently. It will do nothing and emit no warning or error.Comment #35
Upchuk CreditAttribution: Upchuk commentedI also like #32 mainly because it doesn't break the existing API (exactly the final point in #34). I'm glad it worked out after #8 :)
Comment #36
dawehnerWTFH
Comment #37
dawehnerI still hate iterators, given their additional complexlity.
For now though #2431379-32: LazyPluginCollection should not implement \Iterator removes the complexity a bit and fixes the underlying bug.
Comment #38
alexpottNice amount of simplification between #30 and #32. @tim.plunkett++
This issue addresses a major bug and is allowed per https://www.drupal.org/core/beta-changes. Committed 9defbd4 and pushed to 8.0.x. Thanks!
Comment #40
mpdonadioThis is a nit, but is the IS accurate for what got committed (and also the commit message)? Shouldn't the it be something like:
Proposed Resolution
Change LazyPluginCollection to implement \IteratorAggregate instead of \Iterator. This retain will foreach compatibility over collections, but lose the fine control over iteration (which is not used by core).
Or am I missing something?