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.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

yched’s picture

Status: Needs review » Needs work

Yay, thanks eff !

+++ b/core/includes/bootstrap.incundefined
@@ -3479,32 +3479,27 @@ function _drupal_shutdown_function() {
-  if (empty($kernels[$scope][$type])) {
-    if (!isset($discovery)) {
-      $discovery = new ConfigDiscovery($scope, $type);
-    }
-
-    $kernels[$scope][$type] = new PluginKernel($discovery);
+  if (!isset($kernels[$scope][$type])) {
+    $type_class = "Drupal\\$scope\\Plugin\\Type\\$type";
+    $kernels[$scope][$type] = new PluginKernel(new $type_class());

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

+++ b/core/modules/simpletest/tests/plugins_test.incundefined
@@ -0,0 +1,54 @@
+class plugin implements PluginTypeInterface {

Minor : should be "class pluginType implements ..." ?

effulgentsia’s picture

Status: Needs work » Needs review
FileSize
22.21 KB

Relying on a hardcoded Drupal\$scope\Plugin\Type\$type namespace pattern is another, possibly better option

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

$type should be a capitalized string.

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.

$scope == module, if we want the current registered autoload paths to fire - or the namespace needs to be registered first; where

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.

should we keep $scope as a variable name ?

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.

should be "class pluginType implements ..."

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.

effulgentsia’s picture

Something only tangentially related, but that came up in IRC when discussing this: #1530510: Decide whether to rename PluginKernel.

neclimdul’s picture

Status: Needs review » Needs work
+++ b/core/includes/bootstrap.incundefined
@@ -3479,32 +3479,36 @@ function _drupal_shutdown_function() {
+    $scope_namespace = ($scope == 'core') ? 'Core' : $scope;
+    $type_classname = str_replace(' ', '', ucwords(str_replace('_', ' ', $type)));
+    $class = "Drupal\\$scope_namespace\\Plugin\\Type\\$type_classname";

This 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()

+++ b/core/lib/Drupal/Component/Plugin/Factory/DefaultFactory.phpundefined
@@ -18,12 +18,11 @@ use Drupal\Component\Plugin\Derivative\DerivativeInterface;
+  public function __construct(DiscoveryInterface $discovery, $class_key) {
     $this->discovery = $discovery;

This class_key bit is kinda weird. Its leaking implementation details in a weird way.

+++ b/core/lib/Drupal/Component/Plugin/Factory/FactoryInterface.phpundefined
@@ -13,14 +13,6 @@ use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
-   * Construct a plugin factory.
-   *
-   * @param DiscoveryInterface $discovery
-   *   A discovery object for finding out implementation information.
-   */
-  public function __construct(DiscoveryInterface $discovery);
-

This was by design to enforce consistent factory instantiation by the kernel.

+++ b/core/modules/simpletest/tests/plugins.testundefined
@@ -18,36 +15,23 @@ class PluginBaseTestCase extends DrupalUnitTestCase {
-    // Setup a plugin type to test.
-    $discovery = new StaticDiscovery();
-    // Initialize the plugin singleton with a discovery class for our test type.
-    plugin('simpletest', 'plugin', $discovery);
-
-    $discovery->setPluginTypeDefinition(array(
-      'class' => 'class',
-      'factory' => 'Drupal\\Component\\Plugin\\Factory\\DefaultFactory'
-    ));
-    $discovery->setPluginDefinition('coolaid', array(
-      'string' => 'oh yeah',
-      'class' => 'PluginBaseTestPluginClass',

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.

+++ b/core/modules/simpletest/tests/plugins.testundefined
@@ -18,36 +15,23 @@ class PluginBaseTestCase extends DrupalUnitTestCase {
+    // @todo After moving the classes of plugin_test.inc to PSR-0 locations,
+    //   remove this line.

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.

effulgentsia’s picture

Status: Needs work » Needs review
FileSize
21.07 KB

neclimdul, 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:

  • The complete removal of PluginKernel by moving all of its interface into PluginTypeInterface, and all of its implementation into the base abstract class PluginType.
  • The removal of the plugin() function in favor of instantiating the plugin type class directly. This removes the weird magic disliked in #4.1. However, it requires the addition of a drupal_object() function to keep the current behavior of statically caching plugin kernel (now type) objects, so this patch adds that, and I suspect we'll need to debate the name of that function in a separate issue.
  • As a consequence of above, $scope and $type no longer have any system-level meaning, so I removed them from their only remaining place, ConfigDiscovery, in favor of requiring the type object to pass ConfigDiscovery the information it really needs: the definition root.

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:

This class_key bit is kinda weird. Its leaking implementation details in a weird way.

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.

[FactoryInterface::_construct(DiscoveryInterface $discovery)] was by design to enforce consistent factory instantiation by the kernel.

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.

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.

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.

To keep the unit test you can call the classloader singleton and register a namespace root.

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.

effulgentsia’s picture

Not 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).

neclimdul’s picture

I don't know what to think...

effulgentsia’s picture

Oh, 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.

neclimdul’s picture

#5 committed and pushed. lets see where this goes.

effulgentsia’s picture

FileSize
509 bytes

Yay. 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]

effulgentsia’s picture

A very important follow-up to the decision in #5 of plugin types being instantiated from their class names directly: #1538798: Make mappers runtime swappable.

neclimdul’s picture

Status: Needs review » Fixed

Fixed in plugins-next. Since we've moved to follow up issues marking fixed.

yched’s picture

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

yched’s picture

Status: Fixed » Active

Also, after porting field API branch to the current plugins code, I do find that

$formatter = drupal_object('Drupal\field\Plugin\FormatterPluginType')->getPluginInstance($options);

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

effulgentsia’s picture

plugin_type($class_name) function, that we can properly hint with PluginTypeInterface

That's a cool idea.

I do miss the plugin($scope, $type)->getPluginInstance() indirection

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.

neclimdul’s picture

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

yched’s picture

Yeah, 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.

yched’s picture

@effulgentsia : BTW, #1538798: Make mappers runtime swappable is a non-issue with a plugin type registry - just pull 'mapper' back into the registry.

neclimdul’s picture

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

yched’s picture

How would we register types?

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

function mysubsystem_plugin() {
  return drupal_object('The\Path\To\My\PluginType');
}

// and / or 
function mysubsystem_plugins() {
  return drupal_object('The\Path\To\My\PluginType')->getPluginDefinitions();
}

// and / or
function mysubsystem_get_plugin($options) {
  return drupal_object('The\Path\To\My\PluginType')->getPluginInstance($options);
}

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.

merlinofchaos’s picture

I'd much rather have all code (both internal to a subsystem and 3rd party) standardize on calling plugin('scope', 'type')->theMethod() directly.

IMO, 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.

neclimdul’s picture

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

// my system needs a singleton so here it is!
function mysubsystem($options) {
  $foo = drupal_static(__FUNCTION__);
  if (/*cachelogic*/) {
    $foo[$cid] = new MyPluginType($injection)->getPluginInstance($options);
  }

  return $foo[$cid];
}

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.

effulgentsia’s picture

#27 could also be:

// Eventually, there will be some better hook than hook_init() for adding things to the DI container.
function mymodule_init() {
  drupal_container()->register('mymodule.myplugintype', 'Drupal\mymodule\SubNamespaceFoo\MyPluginType');
 // Can also call other functions after register() to add constructor arguments, and other Symfony DI coolness.
}

function mymodule_myplugintype() {
  return drupal_container()->get('mymodule.myplugintype');
}

function mymodule_mysubsystem($options) {
  return mymodule_myplugintype()->getPluginInstance($options);
}

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?

yched’s picture

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?

OK, 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.

effulgentsia’s picture

Status: Active » Needs review
FileSize
5.73 KB

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

neclimdul’s picture

Do we need the wrapper to do the tests? Seems like we can just new then keep the unit testing.

effulgentsia’s picture

FileSize
5.91 KB

Indeed. How's this?

Crell’s picture

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

effulgentsia’s picture

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

Crell’s picture

Status: Needs review » Needs work
+++ b/core/modules/system/tests/plugins.test
@@ -1,63 +1,41 @@
+  protected $test_plugin_type;

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

neclimdul’s picture

Status: Needs work » Fixed

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

Status: Fixed » Closed (fixed)

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

Anonymous’s picture

Issue summary: View changes

Updated issue summary.