From @yched in #1497366-54: Introduce Plugin System to Core:
Discovery classes should only involve implementation discovery, and that plugin types would be better off registered independently (event at bootstrap). That would allow the discovery class name to be a property of the plugin type, just like the factory and mapper.
I (effulgentsia) agree. This patch removes the concept of plugin type definitions entirely, and makes plugin types into classes. It changes the plugin() function to "discover" the plugin type via a naming convention. The patch includes a PluginTypeInterface and a Drupal\plugins_test\Plugin\Type\plugin class that implements the interface.
Comment | File | Size | Author |
---|---|---|---|
#32 | plugins-1529162-32.patch | 5.91 KB | effulgentsia |
#30 | plugins-1529162-30.patch | 5.73 KB | effulgentsia |
#14 | plugins-kernel-cleanup.patch | 509 bytes | effulgentsia |
#10 | plugins-decouple-type-and-discovery.patch | 22.68 KB | effulgentsia |
#5 | plugins-decouple-type-and-discovery.patch | 21.07 KB | effulgentsia |
Comments
Comment #1
yched CreditAttribution: yched commentedYay, thanks eff !
I was thinking of registering plugin types through an HTTPKernel event at bootstrap, but that is not possible before the Kernel patch lands. Relying on a hardcoded "Drupal\\$scope\\Plugin\\Type\\$type" namespace pattern is another, possibly better option, but this means :
- $type should be a capitalized string.
- $scope == module, if we want the current registered autoload paths to fire - or the namespace needs to be registered first; where ? event at bootstrap ;-) ? chicken and egg...
That's a restriction, dunno if it's an acceptable one (if yes, should we keep $scope as a variable name ?)
Minor : should be "class pluginType implements ..." ?
Comment #2
effulgentsia CreditAttribution: effulgentsia commentedI'm hoping to discuss this with EclipseGc, merlinofchaos, and maybe others tomorrow. I'm not aware of why we'd need anything other than this in Drupal, but I'm not opposed to making it swappable either. I added a @todo in this patch to make it swappable when we want to (and have a DI container with which to do it).
No, it shouldn't. But the class name derived from it should be. That's fixed in this patch, and I expanded the docblock of plugin() to explain it more.
Modules can drupal_classloader()->registerNamespace() in hook_init(), and at some point, we might fire early bootstrap events that can be listened to as well. But I think most modules should stick to their own namespaces, and use their name for the $scope of plugin types they define.
I think so. It can be 'core' as well as a module name. In the future, it could be a profile or theme name. And as per above, modules could also choose to not follow the convention of using their own name for scope, though I don't see why they would choose to go rogue like that.
I changed it to TestPluginType. I also added a base abstract class to make plugin type classes even smaller.
And I also added a getType() method to PluginKernel, which simply returns the plugin type object, so client code can access it in situations where plugin type objects do more than what the kernel needs.
Comment #3
effulgentsia CreditAttribution: effulgentsia commentedSomething only tangentially related, but that came up in IRC when discussing this: #1530510: Decide whether to rename PluginKernel.
Comment #4
neclimdulThis is some really weird PSR-0 magic... I'm not comfortable with forcing PSR-0 structures and doing magic like this with it. I don't really see the point of doing it anyways, I really think plugin types should provide their own singleton if they have any specialized logic anyways. e.g. cache()
This class_key bit is kinda weird. Its leaking implementation details in a weird way.
This was by design to enforce consistent factory instantiation by the kernel.
This is subtly different then something that was novel about the previous implementation. Its possible its still doable but it was also proving that with a method like this you could interact with the discovery after the fact and register additional plugins.
This is possible with a test module but requires a full WebTest. To keep the unit test you can call the classloader singleton and register a namespace root. Just de-register it on tearDown.
It feels like we've moved away from the generic enough for anything kernel to specialized logic in the plugin type and that seems like a lose. It does have some useful idea though so I'm going to play with the concept. We should get you commit access so you can make a branch for me to pull some of those comment and other cleanups that aren't related.
Comment #5
effulgentsia CreditAttribution: effulgentsia commentedneclimdul, EclipseGc, merlinofchaos, and I discussed this today on a phone call, and here's an updated patch based on the outcome of that conversation. The major changes are:
We also discussed some other stuff unrelated to this issue, and therefore, not included in this patch. neclimdul might directly commit some code related to some of that conversation, and I'll make issues for the rest.
Regarding the feedback in #4:
How so? If the plugin type wants to use the default factory, then it needs to decide which key in the definitions of its plugins specifies the instance class. At least that's how the default factory is currently coded. This patch just migrates this information from existing in a "plugin type definition" (which this patch removes as a concept), into the plugin type directly.
Why do we want that? The factory's only job is to create plugin instances. Each factory can do it in the way it sees fit, and the plugin type should create the factory that's appropriate. If different factories want different constructor params, why is that a bad thing? Unless we want application code being able to tell a plugin type to use a different factory than what that plugin type usually uses? Which we can find a way to do if we want that as a design goal, but I haven't heard this being a design goal yet.
The implementation of StaticDiscovery's plugin discovery related methods is unchanged, so a plugin type can still expose that functionality to application code if it wants to. Or, it can make its $discovery member public if it wants to. But I don't see why we'd want to enable either of that for a plugin type by default.
I added a link to #1477218: Convert Tracker tests to PSR-0 / PSR-0 test class discovery. I'd rather let that issue land, then reroll for it, than mess with custom registration.
Comment #10
effulgentsia CreditAttribution: effulgentsia commentedNot sure if you'll like this, but this also removes the constructor from mapper's interface, and makes PluginTypeInterface simply inherit from DiscoveryInterface, FactoryInterface, and MapperInterface. I think this makes a lot of sense, because a plugin type really is just a composition of these 3 interfaces, and it means you can pass the type object to any code that wants any of the other 3 interfaces (e.g., if a plugin type wants to, it can pass $this instead of $this->discovery to DefaultFactory's constructor, which is useful if it wants to delay creation of its discovery object until it's needed).
Comment #11
neclimdulI don't know what to think...
Comment #12
effulgentsia CreditAttribution: effulgentsia commentedOh, I should have mentioned, if you prefer #5 to #10, feel free to commit that instead, and I can rebase #10 to it for further discussion.
Comment #13
neclimdul#5 committed and pushed. lets see where this goes.
Comment #14
effulgentsia CreditAttribution: effulgentsia commentedYay. I'll rebase #10 and post that to a new issue. Meanwhile, here's a tiny cleanup that removes unused "use" statements from bootstrap.inc.
[Edit: Link to the new issue for #10: #1538692: Change PluginTypeInterface to extend Discovery, Factory, and Mapper interfaces]
Comment #15
effulgentsia CreditAttribution: effulgentsia commentedA very important follow-up to the decision in #5 of plugin types being instantiated from their class names directly: #1538798: Make mappers runtime swappable.
Comment #16
neclimdulFixed in plugins-next. Since we've moved to follow up issues marking fixed.
Comment #17
yched CreditAttribution: yched commentedSide note on drupal_object() : problem with such a generic function is that we cannot type hint its return value. Given that this is now the entry point to the plugin system for any external client code willing to instantiate a plugin, not being able rely on IDEs autocompletion for the (longish) method names is unfortunate.
I also vaguely suspect that this very genericity will raise endless bikeshed discussions when a core patch is up for review. Could we aim lower and simply go with a plugin_type($class_name) function, that we can properly hint with PluginTypeInterface ?
Comment #18
yched CreditAttribution: yched commentedAlso, after porting field API branch to the current plugins code, I do find that
is not too legible :-(.
More seriously, It basically prevents refatcors. If you find you want to rename the plugin type class (or refactor the namespace hierarchy), you'd need to change all the places in code that interact with this plugin type, and some of them might be in 3rd party modules, which you can't know anyway.
This is the same reason why some people (including me) winced when EclipseGC mentioned on IRC he considered storing class names in config data.
So, yay for moving plugin types out of the Discovery class, but I do miss the
plugin($scope, $type)->getPluginInstance()
indirection...Comment #19
effulgentsia CreditAttribution: effulgentsia commentedThat's a cool idea.
We should discuss this with merlinofchaos. His concern was that the indirection requires there to exist some map somewhere between type and class. He currently has hook_ctools_plugin_type_info() (or something like that) and hates that. But you raise a good point on legibility and refactoring.
Comment #20
neclimdulWell the class name can't be type hinted but we can do tests. We can also call getInstance in the function so that's good. We can't pass arbitrary information to the type constructor though. that means if your type goes though the plugin_type method you can't say pass a DI object into it.
Comment #21
yched CreditAttribution: yched commentedYeah, a mapping is tedious to collect, but it's a condition for loose coupling. Also, it's really lightweight, since the 'discovery', 'factory', 'mapper' properties have been moved to the runtime plugin type class.
Comment #23
yched CreditAttribution: yched commented@effulgentsia : BTW, #1538798: Make mappers runtime swappable is a non-issue with a plugin type registry - just pull 'mapper' back into the registry.
Comment #24
neclimdulHow would we register types? Honestly, the fact that they are not registered right now I see as a feature, I've never really been a fan of a singleton every plugin type goes through. I see plugins more as a set of tools for building systems so just passing a class around within that system or providing your own singleton seems reasonable.
Comment #25
yched CreditAttribution: yched commentedAs I proposed earlier in this thread, one possibility is event / listeners early at bootstrap, like other early / critical systems are apparently going to do. Plugins (types) are sufficiently low-level and ubiquitous to deserve such special casing. And it allows for mapper swappability.
Passing classes around is a no-go IMO for the refactor reasons explained above. And leaving it up to each subsystem (that is, each subsystem whose maintainer figured out by himself that letting 3rd party code reference class names directly is wrong) provide some disparate variants around :
sounds unappealing IMO.
I'd much rather have all code (both internal to a subsystem and 3rd party) standardize on calling
plugin('scope', 'type')->theMethod()
directly.Comment #26
merlinofchaos CreditAttribution: merlinofchaos commentedIMO, there's no really good reason for this, and it places artificial limitations on plugins forcing them to all act the same way when different kinds of plugins, in the real world, often need to behave very differently. This is one of the little things I have always been against other people pushing onto the plugin system, and I remain against it. In fact, I would go so far as to consider the plugin() function to be unnecessary cruft.
Comment #27
neclimdulI'm struggling with how to respond since I'm apparently not making my point.
Something I can throw out I guess is I would remove drupal_object()/plugin() or any generalized entry point. I had it originally because people wanted it but I planned on proving in our use cases its useless. Since plugin() is gone and drupal_object() is really weird I figure we just get rid of it. As far as the code, there would just be something like this:
The large amount of hand-waving and incomplete code there is indicative of the fact that all this logic is very specific to the system and can't/shouldn't be generalized.
Comment #28
effulgentsia CreditAttribution: effulgentsia commented#27 could also be:
Then each module can choose whether to use the DI container for its plugin type registry (as per above), or whether to hard-code an instantiation of a fixed plugin type class (as per #27). The former might be more appealing to yched (for Field API) while the latter might be more appealing to merlinofchaos (for some of Views).
I think the point merlinofchaos and neclimdul are making is that there shouldn't be any global plugin() function forcing, or even encouraging, a particular practice, and that each plugin type provider should figure out how to expose access to it. However, I think the point yched is making is that making each module come up with its own wrapper functions to plugin types will result in more cruft overall than providing a global plugin() or plugin_type() function. For example, I think in core alone we'll end up having a dozen or more plugin types.
Would it make sense to start without any plugin(), plugin_type(), or drupal_object() function, use something like the code above for Field API, and then see what core looks like after we have a bunch of plugin types, and decide then if we want to add some global plugin system function?
Comment #29
yched CreditAttribution: yched commentedOK, works for me, I guess.
Not forcing the use of a standard entry point on all plugin types sounds legitimate, but having a standard that should fit 80% is still worthy IMO - even the standard is "expose yourself to drupal_container()", which is fine by me.
Comment #30
effulgentsia CreditAttribution: effulgentsia commentedThis patch removes drupal_object() and per #27, adds a simple wrapper function to plugin_test.module (requiring PluginBaseTestCase to be a web test). While we could make the test call "new" directly instead of using the wrapper function, given that the test is our only use-case in the plugins-next branch so far, I think it should reflect our consensus on best default practice, which as I read this issue so far, involves a per-module wrapper function (since we're not yet far enough along on best practices for drupal_container() usage). Does that sound right, or am I off-base here?
This patch also removes the test that uses TestPluginTypeDefaults, since that class doesn't exist in the plugins-next branch.
Comment #31
neclimdulDo we need the wrapper to do the tests? Seems like we can just new then keep the unit testing.
Comment #32
effulgentsia CreditAttribution: effulgentsia commentedIndeed. How's this?
Comment #33
Crell CreditAttribution: Crell commentedA unit test can/should call new directly, and inject whatever mocked objects are appropriate.
Integration tests can/should use the higher-level front-end routines, to test that they all work correctly.
Comment #34
effulgentsia CreditAttribution: effulgentsia commentedJust for clarity, #32 does what #33.1 says. That alone doesn't necessarily mean Crell approves all of #32, but just saying, I think #32 is ready for commit unless someone feels otherwise.
Comment #35
Crell CreditAttribution: Crell commentedThis should be camelCase.
Otherwise I'm all down for getting rid of drupal_object(), as it's a horrid idea. :-) I haven't been following recent developments closely enough to feel comfortable RTBCing this, but I saw nothing in #32 specifically that made me freak out.
Comment #36
neclimdulI left the abstract test in because I was using that for the derivative test I'd been working on and I'd hoped that sort of logic would/could make it into simpletest someday and the tests where failing with PDOExceptions with DrupalUnitTestCase. Also, the one-off loader means that namespace won't be loadable in other tests which is possibly desirable. I liked your docs though so I took them. :)
Also made the camelCase change.
I think the is done now!
Comment #37.0
(not verified) CreditAttribution: commentedUpdated issue summary.