Problem/Motivation
More unit testing goodness for #2052109: [meta] Expand phpunit tests for \Drupal\Component\Plugin classes.
This time we're testing Drupal\Component\Plugin\Derivative\DeriverBase:: getDerivativeDefinition(), which it turns out is buggy.
public function getDerivativeDefinition($derivative_id, $base_plugin_definition) {
if (!empty($this->derivatives) && !empty($this->derivatives[$derivative_id])) {
return $this->derivatives[$derivative_id];
}
$this->getDerivativeDefinitions($base_plugin_definition);
return $this->derivatives[$derivative_id];
}
This method dutifully checks if the array elements exist on $derivatives, but then fails to check them again after calling getDerivativeDefinitions(). Furthermore, it's likely that the return value of getDerivativeDefinitions() should be stored in $derivatves, but the method doesn't do this.
Also, the behavior of DeriverInterface can't really be deduced from this base class. That is, don't look to this class for clues about how derivers should work, because it is inconsistent.
Proposed resolution
- Analyze the need for storing the results of
getDerivativeDefinitions(). - Add a test which covers these cases.
Remaining tasks
User interface changes
API changes
Beta phase evaluation
| Issue category | This issue is a bug because DeriverBase is the base class for many other classes. |
|---|---|
| Unfrozen changes | Potentially unfrozen because it adds automated regression tests. |
| Comment | File | Size | Author |
|---|---|---|---|
| #40 | 2380029-40.patch | 24.41 KB | jofitz |
| #29 | drupal-2380029-29.patch | 24.56 KB | yogeshmpawar |
| #22 | drupal_2380029_22.patch | 23.17 KB | xano |
| #22 | interdiff.txt | 516 bytes | xano |
Comments
Comment #1
mile23These patches add a unit test for
getDerivativeDefinition(). One passes, one fails, and the interdiff shows you how to make the passing test fail.This demonstrates that if
getDerivativeDefinition()is at the point where$derivativesdoes not contain the$derivative_idkey, the method will always error out.Comment #3
dawehnerDOn't we still need a @group?
Comment #4
mile23@dawehner: Yes, still needs coding standards stuff, but I'm just demoing the bug. I'm not sure what the fix is.
Comment #5
jhedstromMight one potential fix would be for
getDerivativeDefinitions()to actually update$this->derivatives, and then ensure the$derivative_idexists?Comment #6
xanoWhat about doing it like this? Less confusing logic, with a single point of failure (
getDefinitions()).Comment #7
xanoThis actually makes more sense, test-wise.
Comment #8
jhedstromThis fix makes sense to me. @dawehner, @Mile23?
Comment #9
mile23I'm still unclear what Derivatives are. I just found what looked like a bug.
Based on @jhedstrom's endorsement, I'll say RTBC. Someone with more knowledge can shoot me down if needed. :-)
Comment #10
mile23OK, Xano went out of his way to help me understand Derivatives, and this seems all reasonable and good, so RTBC++.
Comment #11
xanoThanks for the reviews, and to @Mile23 for catching this bug in the first place!
Comment #12
alexpottStill missing an
@group.Also this will result in a performance hit for things like
SelectionBase::getDerivativeDefinition()- since that will now always callSelectionBase::getDerivativeDefinitions().Comment #13
alexpottNeeds work for #12
Comment #14
xanoTheoretically that's a performance hit, but that's why most, if not all, plugin managers cache the processed plugin definitions. Even if a piece of code calls these deriver methods more than once, it's always been the deriver's responsibility to provide internal caching. Should we add that to
SelectionBase?Comment #15
xanoIf you look closely, there's something else wrong here.
DeriverBase::getDerivativeDefinition()statically caches derivatives, not per base plugin ID. This means that if you'd use the derivers asDeriverInterfacepermits by callinggetDerivativeDefinition()for different base plugins, you'd keep getting the same results as the derivative definitions of the first base plugin are cached and returned on subsequent calls. The only reason we've never had this problem in core, is thatDerivativeDiscoveryDecoratorcreates a new deriver every time it needs to use one.Comment #16
xanoThis patch fixes the other bug as well, and makes sure that all overrides of
DeriverBase::getDefinitions()use static caching.Comment #20
xanoRe-roll.
Comment #22
xanoComment #27
mile23Comment #29
yogeshmpawarI have rerolled the patch for 8.2.x version.
Comment #31
manuel garcia commentedComment #32
manuel garcia commentedComment #34
yogeshmpawarAny Update on this issue ?
Comment #37
idebr commentedPatch no longer applies, mostly due to #2776975: March 3, 2017: Convert core to array syntax coding standards for Drupal 8.3.x RC phase
Comment #38
jofitzRe-roll (based the patch in #22 due to the test failures of the patch in #29).
Comment #40
jofitzWell that was an abject failure! Let's try that again...
Comment #52
donquixote commentedI agree with the problem - the current DeriverBase is definitely flawed.
But I think one of our observations is wrong.
I would say this is wrong with the current way that deriver classes are used.
Deriver classes get the base plugin id injected in the constructor or the ::create() method.
See ContainerDerivativeDiscoveryDecorator::getDeriver().
This means one deriver object is only ever used for the same base plugin definition.
But now the $base_plugin_definition is passed as a parameter to the ->getDerivativeDefinitions() method, which falsely suggests that the same deriver object could be used for different base plugin definitions.
Technically, even with the proposed patch, this is still not fully supported.
In theory, you can poison the cache if you have two different base definitions with the same id.
This should normally not happen, but this is relying a lot on the context in which this is called.
A cleaner solution would be to inject the base definition in the constructor and ::create(), alongside the base plugin id, and not pass it to the ->getDerivativeDefinitions() and ->getDerivativeDefinition() method.
Ofc changing this would cause BC issues.
-----
The sections below may become obsolete if we apply changes as proposed above.
---------
Originally, before writing the section above, I was going to say the proposed solution is BC-breaking.
There are subclasses in contrib that write directly to
$this->derivatives.Now we are changing the structure of the array, which breaks these subclasses.
In practice, this is not really as big a problem as I originally thought.
The new version of ->getDerivativeDefinition() no longer looks into
$this->derivatives. So the subclass can basically do what it wants with this property.It only becomes BC-breaking if we start using the deriver in a way where the poisoned cache actually matters.
I don't currently see that we plan to do that.
A problem is that we don't really push contrib modules to upgrade their subclasses, unless developers "read the manual", which most of them won't.
To be more recognizable, we could deprecate this base class and create a new one with different name.
(at the cost of wasting a nice class name)
BUT
All I wrote in this section kind of becomes obsolete with the earlier section.
-------
Now some other feedback.
Can we use
??null coalesce operator?I don't think this provides a nice DX for the subclass.
The subclass still needs to write to the protected property, and then call the parent method. Not convenient.
What about this instead?
Comment #53
donquixote commentedActually, I wrote a new version of this class long ago, and shared it here :)
#2727011-34: [policy, no patch] Private vs protected, and the role of inheritance
I have used this base class successfully in a project.