Problem/Motivation
ConfigurablePluginInterface currently extends DependentPluginInterface in the assumption that a configured plugin is going to be stored inside a config entity, and that therefore that config entity needs to ask the plugin for any dependencies to add to itself.
This is not always the case:
- Migrate - that wants to implement ConfigurablePluginInterface in #2937177: Migrate plugin base classes should implement ConfigurablePluginInterface - doesn't store these plugins in a config entity.
- Some contrib modules, like Commerce, use configurable plugins on content entities (which don't care about config dependencies).
In these situations, having to implement the interface DependentPluginInterface is redundant.
This issue came up in #2937177-12: Migrate plugin base classes should implement ConfigurablePluginInterface.
Proposed resolution
#26 of this thread summarized three possible options to handle this issue:
- Keep it as is (and accept that some modules need to implement redundant methods);
- Make existing ConfigurablePluginInterface no longer extend DependentPluginInterface;
- Create a second ConfigurablePluginInterface that does not extend DependentPluginInterface.
Option 2 was found not feasible because of BC issues (see #21 why), the proposed solution is therefore option 3: create a second ConfigurablePluginInterface that does not extend DependentPluginInterface.
This means the following:
-
Create a new interface called
\Drupal\Component\Plugin\ConfigurableInterface, which contains all the methods from\Drupal\Component\Plugin\ConfigurablePluginInterface(minus parent interfaces). These are:getConfiguration()setConfiguration()defaultConfiguration()
- Leave
\Drupal\Component\Plugin\DependentPluginInterfacealone. - Have
\Drupal\Component\Plugin\ConfigurablePluginInterfaceextend DependentPluginInterface and ConfigurableInterface and deprecate it. - Replace all implementations of ConfigurablePluginInterface in core with either ConfigurableInterface, DependentPluginInterface, or just ConfigurableInterface as needed.
Remaining tasks
- Review the patch.
Update the change record.
User interface changes
None.
API changes
- A new interface called
\Drupal\Component\Plugin\ConfigurableInterfaceis added. \Drupal\Component\Plugin\ConfigurablePluginInterfacegets deprecated.
Data model changes
None.
Release notes snippet
ConfigurablePluginInterface, which is used by many, many plugins, is deprecated in favour of a combination of two interfaces: ConfigurableInterface & DependentPluginInterface. If the plugin does not have external module dependencies, then developers may opt to just implement ConfigurableInterface by itself and not implement DependentPluginInterface. See change record for more information.
Comments
Comment #2
heddnI'm not sure I like the name of the class. We can bikeshed it for a while. But here's something for us to debate about.
Comment #3
phenaproximaI don't think we need to do this. Why bother adding yet another interface when we can just implement an empty calculateDependencies() method? Currently, Migrate is the only use case for such an interface. If another subsystem needed similar functionality, that might be a reason to decouple DependentPluginInterface from ConfigurablePluginInterface. But until that happens, this seems like overkill to me.
Comment #5
phenaproximaDiscussed with @heddn on IRC.
The real problem here is the fact that ConfigurablePluginInterface extends DependentPluginInterface. These two interfaces should be decoupled. And I think we can just go ahead and do that without breaking BC. Why? Because any existing plugin which uses ConfigurablePluginInterface must already have a calculateDependencies() method, thus fulfilling DependentPluginInterface.
So all we need to do is two things:
This will also benefit existing core plugins by allowing us to remove bogus implementations of calculateDependencies() that return an empty array, solely in order to fulfill DependentPluginInterface.
How about we try that, and see how much stuff breaks? Let's also get framework manager sign-off on this approach.
Comment #6
alexpottI don't think we can break that now. What I think we need to do is to make ConfigurablePluginInterface implement DependentPluginInterface, ANotherInterface. Where ANotherInterface is all the methods from ConfigurablePluginInterface. That way new things can implement said interface and we can change places where ConfigurablePluginInterface is checked and swap for DependentPluginInterface / ANotherInterface accordingly because we know that ConfigurablePluginInterface implements both.
Comment #7
heddnCommerce in contrib does this pretty often. And I think in general, there are a fair number of contrib projects that could use this functionality.
Here's another approach. We'd need a CR, but reducing the scope of a interface won't actually break anything. So I think this is BC. It will just make type-hinting more difficult.
Comment #8
heddnCross posted with #6. If I'm reading #6 correctly, that is what I started with in #2. But I'm more of the opinion that #5 seems more logical.
Comment #9
alexpottYes #5 is more logical but I think it is impossible to do in backwardsly compatible manner for D8. D9 this makes total sense.
Comment #10
heddnFrom #2, file name is wrong, should be ConfigurablePluginWithoutDependenciesInterface
Comment #11
heddnI've got the first part of our discussion from slack implemented. And updated the IS with actionable steps.
Involved in the discussion: alexpott, phenaproxima, @mikelutz, heddn.
Comment #12
heddnI also tagged as needing a plugin system subsystem maintainer to weigh in.
Comment #13
heddnComment #14
phenaproximaMinor verbiage changes in the IS, and tagging for a change record.
Comment #16
phenaproximaChange record written. https://www.drupal.org/node/2946161
Comment #17
joachim commentedShouldn't that say 'and will be removed in 9.0.0' or something like that?
"One must"...
Docs are usually in the imperative, pronounless form, e.g. 'Implement a yada yada'.
Finally, the last sentence is more or less saying the same as the first sentence: they could be combined:
To have a plugin that is both configurable and declares dependencies, implement DPI in addition to CPI.
Comment #18
joachim commentedI couldn't make it to the slack discussion, so sorry if this is rehashing old ground...
But is this really the best fix?
It's laying groundwork for D9, but it's not changing *anything* for modules that use this right now. If you want configurable plugins, but you don't care about config dependencies, you still have to pointlessly implement calculateDependencies() with an empty return.
I think the solution to add a new interface for ConfigurablePluginInterface to inherit from is far prefererable, as it has immediate benefit. Even if it does pose the rather tricky problem of what to call it.
Comment #19
mikelutz@joachim. That was where we got stuck. It was a tradeoff between being able to get rid of the calculateDependencies() now, or keep the ConfigurablePluginInterface name for the eventually decoupled interface. The consensus was do it this way so that eventually the decoupled plugin could still be called CPI, in combination with #2852463: Create a trait to implement \Drupal\Component\Plugin\ConfigurablePluginInterface so that the useless methods could be reduced to one copy in a single trait that could be easily cleaned up for 9.x.
Comment #21
mikelutzSo, I think we have a problem with this plan.
If we have:
This will cause a PHP Fatal Error, Class D cannot implement previously implemented interface A.
Technically, "Class D implements A, B" will not throw the error, but this is a quirk of PHP, and I'm not sure I'm comfortable with using it.
Even if we did, I am unsure if it's possible to implement the final step in this list, though I'm hardly an expert in reflection classes and such.
I don't know of any way to tell the difference between Class C and D; They both implement A, and the only means I can find to possibly tell them apart is through
In the above, the interface lists will be in different orders, i.e. the reflection for D will have DependantPluginInterface before ConfigurablePluginInterface in the interface array, a for C the order will be reversed, but again, it's a quirk that they are in that particular order, there is no php documentation that declares they should be in that order, it just happens to be the order that they are processed.
Thoughts?
Comment #22
heddn#6, what if we do just this. Then start throwing trigger_errors on plugins that aren't config entities.
Comment #23
mikelutzThat's fine for the test, but it's not the primary problem. The primary problem is that we can't leave ConfigurablePluginInterface extending DependantPluginInterface for BC reasons AND have classes directly implementing both. It's one or the other, which means there would be no way to have a plugin that implements both work with both Drupal 8 and 9, other than the quirky interface order hack I mentioned above.
Which means to separate them in a backwards compatible way, I think you have to rename one or the other or both (which we wanted to avoid), unless I'm not thinking of something (entirely possible).
Comment #24
heddn@larowlan has asked if we can unblock this core plugin issue and then after it lands, move forward with migrate. Which leaves us in a tough spot. We want configurable plugins in migrate, but we cannot get them until we resolve the upstream issue. Either Yes, move this along. Or No, it ain't happening or Yes, it will happen, but it will take too long and then the pragmatic approach is Fine for migrate.
What should we do so #2937177: Migrate plugin base classes should implement ConfigurablePluginInterface is unblocked.
Comment #25
andypostComment #26
megachrizIn trying to understand this issue, I noticed that there are three options discussed to deal with this issue.
Options:
ConfigurablePluginInterface keeps extending DependentPluginInterface and this issue is closed as won't fix.
Cons:
calculateDependencies().All plugin types implementing ConfigurablePluginInterface that need to have dependencies calculated need to be updated to also implement DependentPluginInterface.
Cons:
A new class called ConfigurablePluginWithoutDependenciesInterface is added with all the methods ConfigurablePluginInterface now has. ConfigurablePluginInterface itself will then become empty and it will extend ConfigurablePluginWithoutDependenciesInterface and DependentPluginInterface.
Cons:
It seems that option 2 is out of the question, because in that case there is no way for contributes modules to provide configurable plugins with dependency calculation that will work with both the current situation and the new situation. This would mean that these contributed modules will be forced to raise their core dependency. And/or provide a new major version to keep supporting both core versions.
So that leaves us to decide between option 1 and 3. Personally, I don't see why having some plugin types need to implement a redundant method is that much of a problem compared to what issues get blocked by this issue now. Option 3 might have the result that some subsystems need to check for ConfigurablePluginWithoutDependenciesInterface instead. So the ones that want to, will not be compatible with older core versions. As I see it, not that much of an issue either: these contributes modules will just raise their core dependency. Something that Commerce already does regularly.
So I'm fine with either option 1 or option 3. But we need to make a decision to unblock other issues.
This issue blocks:
#2852463: Create a trait and base class to implement \Drupal\Component\Plugin\ConfigurableInterface
#2937177: Migrate plugin base classes should implement ConfigurablePluginInterface
Feeds Migrate
Comment #27
mikelutzIn discussions in slack with various managers and maintainers, I feel like we keep coming back to option 3, and then get hung up on what to call the new interface. In the interests of trying to move things forward, I would specifically propose the following:
In Drupal 8.7.0 (and specifically as part of this issue):
In a follow up issue (i.e. #2852463: Create a trait and base class to implement \Drupal\Component\Plugin\ConfigurableInterface) I propose we add \Drupal\Component\Plugin\ConfigurablePluginTrait and/or \Drupal\Core\Plugin\ConfigurablePluginBase which would use the trait. There is still debate on base class vs trait vs both. and that is a debate for the other issue, not this one.
In Drupal 9.0.0 we remove ConfigurablePluginInterface. If we want at that point, I think we can immediately deprecate ConfigurableInterface and re-implement it as ConfigurablePluginInterface if we really really wanted to have the name back, but that also would be a debate for another issue.
Comment #28
phenaproxima+1 to #27. If a framework manager agrees, let’s do it!
Comment #29
tim.plunkettSigning off on #27 as one of the plugin subsystem maintainers
Comment #30
mikelutzHere's a starter patch to see what #27 would look like. From scratch, so no interdiff.
Comment #31
mikelutzComment #33
mikelutzComment #34
mikelutzComment #36
phenaproximaThis looks pretty weird, did something go wrong here?
Comment #37
mikelutzYes. Something went very wrong there. Thanks.
Comment #38
phenaproximaStatus change.
Comment #39
mikelutzOne other question I had was whether we wanted to add an optional $key parameter to getConfiguration, to retrieve a specific configuration value. It could accept nested configuration keys as an array. I realize this is not specifically in scope for this, and adds complexity, but if it's something we want to do, It should be done now, as we create a new interface. Since this interface is basically api level, we can't really change the method signature later without breaking implementations.
Comment #40
phenaproximaI like the idea and I think it would improve the DX even more. However, you're certainly right that it has complicated BC implications. I defer to the august judgment of the framework managers and subsystem maintainers on this one.
But personally, +1 for #39.
Comment #41
megachriz@mikelutz
Thanks for working on this!
I prefer to not add an optional
$keyparameter togetConfiguration(), for the following reasons:getConfiguration()call to an other plugin will be forced to pass the optional parameter. So contrib modules need to update their code to become compatible with the new situation. DefaultLazyPluginCollection for example does this:$plugin->getConfiguration()['some_key'];.So -1 for #39 for me.
If we do want that optional parameter, let's decide that in a follow-up instead.
Comment #42
heddnI'm also much happier with the DX of
getConfiguration($key = NULL), but only if it doesn't slow things down. So +1 on #39.-1 on DX.
+1 on DX.
Comment #43
mikelutz@MegaChriz
You make some good points, and I'm certainly happy to not do this now. I think you are right, If we change the interface that the core base classes use, then we break BC for extensions, so scratch it. I wish there was a way to do it, because I hate the $this->getConfiguration()['key'] pattern, but if that's the way we are going, I'll get used to it.
Comment #44
andypostThat reminds me similar discussion about how field settings should return values (they do merge them from field storage). Cant find the issue but it was the same dx bikeshed
Comment #45
megachrizI've updated the issue summary:
Comment #46
mikelutzUpdated the CR.
Comment #47
tim.plunkettLooks great, thanks! And thanks for sticking with this one.
Comment #48
tim.plunkettAdding credit
Comment #49
alexpottI don't we lose the
implements ConfigurablePluginInterfacefrom all the core plugins. This means that anything in contrib that does something like$this instanceof ConfigurablePluginInterface ? $this->getConfiguration() : $this->configuration;asContextAwarePluginBasewill suddenly not work.I think we need to do something even more tricky which is to change the plugins to do something like
abstract class AggregatorPluginSettingsBase extends PluginBase implements PluginFormInterface, ConfigurableInterface, DependentPluginInterface, ConfigurablePluginInterface {and remove the@trigger_error()from the interface and add it to the block plugin manager that reads all the block plugins and trigger if something implements ConfigurablePluginInterface and not ConfigurableInterface. Then in Drupal 9 we can remove ConfigurablePluginInterface and update all the blocks.Putting back to needs review to get more opinions. I might be wrong though as BC is hard to work out all the implications.
Comment #50
mikelutzAlright, I think that only works if ConfigurablePluginInterface does not extend ConfigurableInterface and DependentPluginInterface, and defines it's own methods. Otherwise we run into "Cannot implement previously implemented interface" Fatals, and there's no way to check if something implements ConfigurablePluginInterface without ConfigurableInterface, because anything that implements ConfigurablePluginInterface would automatically implement ConfigurableInterface.
At that point, I *think* we can add a test that scans all classes in core and fails on ones that implement CPI without CI.
I'm not sure how best to trigger a reliable deprecation error for contrib though. BlockPluginManager wouldn't cover all the plugins that use this.
Comment #51
tim.plunkettBlocks are one of nine core plugin types that use CPI.
https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Component%21Plug...
\Drupal\Core\Block\BlockPluginInterface
\Drupal\Core\Condition\ConditionInterface
\Drupal\Core\Display\VariantInterface
\Drupal\Core\Layout\LayoutInterface
\Drupal\filter\Plugin\FilterInterface
\Drupal\image\ImageEffectInterface
\Drupal\media\MediaSourceInterface
\Drupal\search\Plugin\ConfigurableSearchPluginInterface
\Drupal\workflows\WorkflowTypeInterface
Comment #52
tim.plunkettLet's just go back in time to before CPI was ruined without the sign-off of any plugin subsystem maintainers
Comment #53
mikelutzWhat if we just put the trigger_error in the PluginBase constructor?
Comment #55
mikelutzComment #56
mikelutzComment #57
heddnIn general, I like where this is going. Only really one point of feedback.
This should switch to an actual follow-up issue, yes?
Comment #59
mikelutzWhat's 781 fails between friends?
#57: Probably, if that is the only point of contention, I'll be ecstatic.
Comment #60
mikelutzComment #62
mikelutzAdded follow up issue and addressed feedback in #57.
I think the CR can stay as it is, no need to advertise the BC layer.
Comment #63
mikelutzChanged issue title.
Comment #64
alexpott@mikelutz this is looking really great.
Core plugins still implement the old interface. Our generic plugin instanceof checks support both and if you have a plugin that doesn't yet implmement ConfigurableInterface you will get a warning.
This gives contrib and custom time to move over. It might be tricky for us to remember to add both ConfigurableInterface and ConfigurablePluginInterface to new plugins / or decide if that is the correct course of action but at least contrib code that does the instanceof check can be updated the same way as core. It seems this might be missing from the change record. Ie.
We need something in the CR that says in order to support all configurable plugins in Drupal 8 you need to do this.
Comment #65
phenaproximaI updated the change record. Cheers!
Comment #66
mikelutzIf we are going to recommend contrib check for both interfaces, then we need a core helper method somewhere to do the check. Otherwise we are telling contrib to check for an interface in D8 which won't exist in D9 and they won't have a way to write a module to be compatible with both. There needs to be a method on PluginBase or a static method somewhere in core that returns ($foo instanceof CPI || instanceof CI) in D8 and just instanceof CI in D9.
I don't have time to code it right now, but would love recommendations as to where best to put it.
Comment #67
phenaproximaI'd put it on the PluginBase (mostly because I can't think of another place to put it, except maybe the Plugin annotation class). Something like:
Comment #68
mikelutzI think that would be the ideal spot, I'm just concerned that that is a very common base class, and adding a method to it with a name as common as isConfigurable() is going to break somebody, particularly if it's final. I may be wrong. Drupal/system/Action implements an isConfigurable, but that is on Entity, not Plugin so I guess I don't see any isConfigurable's on PluginBase in core.
I really don't want to add a static class just for this one check.
Comment #69
mikelutzTrying it as a method on PluginBase
Comment #70
mikelutzComment #72
mikelutzComment #74
mikelutzAlmost feels like a random test failure...
Comment #75
alexpottI'm wondering if we should make this static method to show that that is makes no change to object state and it should accept $plugin which is an object. That way any of these check outside the plugin system can use this. Not sure. Hmmm... on reviewing more code I became convinced that the non-static way is potentially better... so let's not do this.
I think this should state that for maximum compatibility in Drupal 8 plugins should implement both ConfigurablePluginInterface and ConfigurableInterface since this is a bit complex.
I don't think we should add this @todo everywhere. I think the commitment to remove the interface is enough. Since removing it will make this code break.
This hints that (a) the non-static isConfigurable() on the base class is a good way to go and (b) we should deprecate this helper method in a follow-up.
Comment #76
mikelutzI opened a followup for #4, and removed all the todos. I expanded the deprecation error, But I don't like telling contrib that they have to implement a deprecated interface for maximum compatibility. We are telling them to implement an interface that won't exist in Drupal 9.
I think we need to either NOT deprecate ConfigurablePluginInterface, and just keep it, or create a
ConfigurableDependentPluginInterface extends ConfigurableInterface, DependentPluginInterface, ConfigurablePluginInterface {}that we intend to keep in Drupal 9 and tell contrib to use that. That way they will pass 'instanceof ConfigurablePluginInterface' checks without actually implementing the deprecated interface.In Drupal 9.0 we remove ConfigurablePluginInterface, change ConfigureableDependentPluginInterface to extend DependentPluginInterface, ConfigurableInterface.
In 9.1 we could deprecate ConfigurableInterface in favor of a NEW ConfigurablePluginInterface and do the whole dance again, and by Drupal 10 we could have a sane hierarchy, lol.
Comment #77
heddnBumping to RTBC. Tests are all passing. Feedback from last patch is now addressed. It would be nice to get feedback on the latest changes here.
Comment #78
tim.plunkettThe subsystem maintainer review tag was removed a bit prematurely, but I took the time to rereview this since the last time I signed off (which was on the patch in #37).
The changes since then make a lot of sense.
Here's an interdiff from then for anyone else curious.
+1 to RTBC
Comment #79
tim.plunkettActually the CR needs to be updated to account for
PluginBase::isConfigurable(). NW for that real quickComment #80
mikelutzCR Updated
Comment #81
phenaproximaComment #82
alexpottCrediting @joachim, @MegaChriz, @phenaproxima and myself for issue reviews. I think we've ended up in the best place with the right compromises in order to get to a better structure for Drupal 9 (rather than Drupal 10)
Committed b1f4962 and pushed to 8.7.x. Thanks!
Comment #84
heddnThank you everyone for seeing this through to completion!
Comment #85
joelpittetThis check assumes that the new interface is implemented on the plugins. Ctools is failing tests because of this at the moment. Any suggestions on how to move forward with support for 8.7? It's a strong possibility this is not a big deal and my understanding on prophecy is weak...
#3030158: Fix test fails for 8.7
Comment #86
joelpittetMaybe this method just needs to be added to the ConfigurableInterface?
Comment #88
xjmReverted for #85. Thanks @joelpittet for reporting this!
Comment #89
xjmComment #90
mikelutzAssigning to myself to replace ->isConfigurable with a static helper
Comment #91
mikelutzHere is the original patch with a new failing test highlighting the issue discovered in ctools. Because we don't have ->isConfigurable() on an interface, it can't be mocked in a prophecy, making it very hard to write tests when the pluginCollections need to access that method.
Comment #92
mikelutzAnd a refactor using a static helper method to do the check rather than an undeclared method on PluginBase.
Comment #93
joelpittetThanks a bunch @mikelutz, this fixes ctools test error.
With the latest patch in #92
With reverted code in HEAD at moment.
With the previous patch in #76
Comment #96
mikelutzI find it hard to believe that that patch has Drupal\Tests\media_library\FunctionalJavascript\MediaLibraryTest::testWidgetUpload as its one and only failing test..
requeuing..
Comment #97
mikelutzI updated the CR to reference the new static helper.
Comment #98
tim.plunkettThese can now be simplified back to just the check, as it can safely accept a NULL (it was needed before for the method call)
Comment #99
mikelutzGood catch, I missed that...
Comment #100
tim.plunkett+1 to RTBC! Thanks for the quick turnaround @mikelutz and thanks for catching it @joelpittet!
Comment #101
knyshuk.vova commentedThe patch looks good and applies successfully. +1 for RTBC.
Comment #103
mikelutzComment #105
mikelutzThe random JavaScript test failures seem to be getting worse lately..
Comment #107
heddnComment #108
alexpottSecond time lucky.
Committed 4bb82a8 and pushed to 8.7.x. Thanks!
Comment #110
alexpottIt'd be great if someone could update the release notes snippet for this.
Comment #111
heddnTook a shot at the release notes. And tagged it.