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

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Upchuk’s picture

Status: Active » Needs review
FileSize
639 bytes
dawehner’s picture

Yes 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.

Upchuk’s picture

Either 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?

tim.plunkett’s picture

Issue tags: +Needs tests, +VDC

Wow. Can we write tests for this first?

dawehner’s picture

I 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.

Upchuk’s picture

FileSize
1.29 KB

@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:

 if ($count == 5) {
   break 1;
 }

is there just to make sure the loop doesn't go on forever.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
3.53 KB

I'm assuming this will be completely broken.

tim.plunkett’s picture

Right, that breaks the lazy-load. It'll be really sad to lose the Iterator-ability, but I don't know what else to do.

Upchuk’s picture

Well, the iterator ability can be replaced by a getter method that does the iteration. So instead of :

foreach ($view->displayHandlers as $handler) {
  ...
}

It can be :

foreach ($view->displayHandlers->getHandlers() as $handler) {
  ...
}

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 :)

xjm’s picture

Priority: Normal » Major

Brr.

dawehner’s picture

Status: Needs work » Needs review
FileSize
4.48 KB

I'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.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
6.16 KB
1.68 KB

Fixing a couple obvious places just to see some test results.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
6.15 KB
615 bytes
tim.plunkett’s picture

Fixing more failures.

tim.plunkett’s picture

Status: Needs work » Needs review
tim.plunkett’s picture

Title: DisplayPluginCollection is problematic inside the ViewExecutable validation » LazyPluginCollection should not implement \Iterator
Component: views.module » plugin system
Issue summary: View changes
Issue tags: +Plugin system

Updating the issue summary.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
31.87 KB
7.36 KB

Fixing filters

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
32.27 KB
680 bytes

I'm worried about the implementation of getInstances(), but let's get this green first.

Upchuk’s picture

Nice one Tim. What is your worry regarding getIntances()?

Upchuk’s picture

Also, do we need any further tests for this?

tim.plunkett’s picture

Status: Needs review » Needs work

Ideally 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.

tim.plunkett’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests
FileSize
2.23 KB
34.12 KB
2.1 KB

Unless 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.

tim.plunkett’s picture

Oops, left in some code from a previous approach.

tim.plunkett’s picture

Here 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.

effulgentsia’s picture

I think #32 makes a lot of sense. I see no downside with it.

still has the issue of not iterating lazily

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.

tim.plunkett’s picture

A 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.

Upchuk’s picture

Assigned: Upchuk » Unassigned

I 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 :)

dawehner’s picture

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.

WTFH

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

I 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.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Nice 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!

  • alexpott committed 9defbd4 on 8.0.x
    Issue #2431379 by tim.plunkett, Upchuk, dawehner: LazyPluginCollection...
mpdonadio’s picture

This 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?

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.