Comments

effulgentsia’s picture

StatusFileSize
new2.67 KB

There's probably a bunch needed for this, but here's a tiny start.

neclimdul’s picture

Yeah we need to kill the constructor entirely from feedback from eclipsegc. getting the base discovery class right to cover the right logic is proving hard but I'm close.

effulgentsia’s picture

Status: Needs review » Needs work

neclimdul has a plugins-derivatives branch, and EclipseGc is reviewing it. Leaving this issue open so people can track what's happening, but setting to "needs work" since #1 is obsolete.

effulgentsia’s picture

Priority: Normal » Critical

Also upping this to critical since this is the most significant blocker to posting an updated patch to #1497366: Introduce Plugin System to Core.

eclipsegc’s picture

StatusFileSize
new15.66 KB

OK, I don't remember which patch effulgentsia had me apply before I started working, perhaps he can glance at it and make a note to clarify my stupidity for not documenting it myself. That being said, I have a 8 tests for derivatives in this. I _THINK_ this will work, but I'm going to have to actually give blocks a try to see how this pans out, still this is a good first try I think.

Eclipse

effulgentsia’s picture

StatusFileSize
new12.5 KB

This adjusts for #1529162-36: Decouple plugin type discovery from plugin discovery, resolving the comment in #5. My review to follow.

effulgentsia’s picture

+++ b/core/lib/Drupal/Component/Plugin/Derivative/DerivativeInterface.php
@@ -31,6 +28,6 @@ interface DerivativeInterface {
-  public function getDerivatives();
+  public static function getDerivatives();

Why do we want this static? Derivatives should be per-plugin, shouldn't they? If static, 2 different plugins using the same derivative class will each return all derivatives for both plugins? That seems odd to me.

+++ b/core/lib/Drupal/Component/Plugin/Discovery/DiscoveryAbstract.php
@@ -0,0 +1,55 @@
+    if (isset($this->plugin_definitions)) {

I don't think we want the abstract class basing anything off this property, which might only be used by some discovery classes.

+++ b/core/lib/Drupal/Component/Plugin/Discovery/StaticDiscovery.php
@@ -17,14 +17,35 @@ class StaticDiscovery implements DiscoveryInterface {
   public function getPluginDefinition($plugin) {
-    return isset($this->plugin_definitions[$plugin]) ? $this->plugin_definitions[$plugin] : array();
+    list($plugin_id, $derivative) = $this->derivePluginId($plugin);
+    if (isset($this->plugin_definitions[$plugin_id])) {
+      if ($derivative && !empty($this->plugin_definitions[$plugin_id]['derivative'])) {
+        return $this->getDerivative($this->plugin_definitions[$plugin_id], $derivative);
+      }
+      elseif (!empty($this->plugin_definitions[$plugin_id]['derivative'])) {
+        return array();
+      }
+      return $this->plugin_definitions[$plugin_id];
+    }
+    return array();
   }
 
   /**
    * Implements DiscoveryInterface::getPluginDefinitions().
    */
   public function getPluginDefinitions() {
-    return isset($this->plugin_definitions) ? $this->plugin_definitions : array();
+    $definitions = array();
+    if (isset($this->plugin_definitions)) {
+      foreach ($this->plugin_definitions as $id => $definition) {
+        if (isset($definition['derivative'])) {
+          $definitions += $definition['derivative']::getDerivatives();
+        }
+        else {
+          $definitions[$id] = $definition;
+        }
+      }
+    }
+    return $definitions;
   }

The abstract class didn't buy us much if we need to do all this.

+++ b/core/lib/Drupal/Component/Plugin/Factory/DefaultFactory.php
@@ -19,6 +19,7 @@ class DefaultFactory implements FactoryInterface {
+  protected $plugin_definition;
 
   public function __construct(DiscoveryInterface $discovery, $class_key) {
     $this->discovery = $discovery;
@@ -45,7 +46,9 @@ class DefaultFactory implements FactoryInterface {

@@ -45,7 +46,9 @@ class DefaultFactory implements FactoryInterface {
     else {
       $instance = new $plugin_class();
     }
-    $instance->setConfig($configuration);
+    if (!empty($configuration['config'])) {
+      $instance->setConfig($configuration['config']);
+    }
 
     return $instance;
   }
@@ -64,12 +67,12 @@ class DefaultFactory implements FactoryInterface {

@@ -64,12 +67,12 @@ class DefaultFactory implements FactoryInterface {
       throw new PluginException("The plugin type did not specify a valid key for determining the plugin instance class.");
     }
 
-    $plugin_definition = $this->discovery->getPluginDefinition($plugin_id);
-    if (empty($plugin_definition[$this->class_key])) {
+    $this->plugin_definition = $this->discovery->getPluginDefinition($plugin_id);
+    if (empty($this->plugin_definition[$this->class_key])) {
       throw new PluginException("The plugin did not specify an instance class.");
     }
 
-    $class = $plugin_definition[$this->class_key];
+    $class = $this->plugin_definition[$this->class_key];

Is this relevant to this issue?

+++ b/core/lib/Drupal/Core/Plugin/Discovery/ConfigDiscovery.php
@@ -22,7 +22,7 @@ class ConfigDiscovery implements DiscoveryInterface {
-      $derviative_mapper = new $config['derivative']($this->definition_root, $plugin_id, $config);
+      $derviative_mapper = new $config['derivative']();

This seems to be the only change to ConfigDiscovery? If our abstract class is right, we should be able to base ConfigDiscovery on it as well.

+++ b/core/modules/system/tests/plugins.test
@@ -68,7 +68,43 @@ class PluginTestCase extends PluginUnitTestCase {
+  function testPluginDerivativeDefintionFetching() {
+  function testPluginEmptyDerivativeDefintionFetching() {
+  function testPluginInstanceDerivativeFetching() {
+  function testPluginEmptyDerivativeInstanceFetching() {

Thanks for these. They help me understand our needs more.

I haven't yet looked at how this patch compares with neclimdul's work in the plugins-derivatives branch, but I have a feeling we may all need to sync up again soon if we want to avoid diverging too much.

eclipsegc’s picture

StatusFileSize
new13.77 KB
+++ b/core/lib/Drupal/Component/Plugin/Derivative/DerivativeInterface.php
@@ -31,6 +28,6 @@ interface DerivativeInterface {
-  public function getDerivatives();
+  public static function getDerivatives();

This is static because it IS a 1 to 1 relationship between derivative classes and their corresponding plugin definition (which is why I favored consolidating the classes), however, I can't think of a situation where the getDerivatives() method would need anything internal to the class itself, so from a utility standpoint, having the method as a static means we can utilize it in the UI without a full class instance. I'm not sure if that's a good enough reasoning for it to be static, especially since I'm advocating we pick one method or another (either plugins provide their own derivative support, or we have a separate class for that, but both seems to turn the code to spaghetti imo).

In short, no two plugins are likely to ever have the same discovery methodology. The only ones that would come close are field based plugins, but I don't believe I've written two of those with the same getDerivatives() method yet, so I'm pretty sure the logic here is sound.

+++ b/core/lib/Drupal/Component/Plugin/Discovery/DiscoveryAbstract.php
@@ -0,0 +1,55 @@
+    if (isset($this->plugin_definitions)) {

You're totally right, I dunno why I changed that.

+++ b/core/lib/Drupal/Component/Plugin/Discovery/StaticDiscovery.php
@@ -17,14 +17,35 @@ class StaticDiscovery implements DiscoveryInterface {
   public function getPluginDefinition($plugin) {
-    return isset($this->plugin_definitions[$plugin]) ? $this->plugin_definitions[$plugin] : array();
+    list($plugin_id, $derivative) = $this->derivePluginId($plugin);
+    if (isset($this->plugin_definitions[$plugin_id])) {
+      if ($derivative && !empty($this->plugin_definitions[$plugin_id]['derivative'])) {
+        return $this->getDerivative($this->plugin_definitions[$plugin_id], $derivative);
+      }
+      elseif (!empty($this->plugin_definitions[$plugin_id]['derivative'])) {
+        return array();
+      }
+      return $this->plugin_definitions[$plugin_id];
+    }
+    return array();
   }

   /**
    * Implements DiscoveryInterface::getPluginDefinitions().
    */
   public function getPluginDefinitions() {
-    return isset($this->plugin_definitions) ? $this->plugin_definitions : array();
+    $definitions = array();
+    if (isset($this->plugin_definitions)) {
+      foreach ($this->plugin_definitions as $id => $definition) {
+        if (isset($definition['derivative'])) {
+          $definitions += $definition['derivative']::getDerivatives();
+        }
+        else {
+          $definitions[$id] = $definition;
+        }
+      }
+    }
+    return $definitions;
   }

I simplified this further still, but it actually buys us a lot since every discovery method has a different way of getting the plugins, but roughly the same way of getting derivatives, and this helps us there. New code is better, take a look.

+++ b/core/lib/Drupal/Component/Plugin/Factory/DefaultFactory.php
@@ -19,6 +19,7 @@ class DefaultFactory implements FactoryInterface {
+  protected $plugin_definition;

   public function __construct(DiscoveryInterface $discovery, $class_key) {
     $this->discovery = $discovery;
@@ -45,7 +46,9 @@ class DefaultFactory implements FactoryInterface {

@@ -45,7 +46,9 @@ class DefaultFactory implements FactoryInterface {
     else {
       $instance = new $plugin_class();
     }
-    $instance->setConfig($configuration);
+    if (!empty($configuration['config'])) {
+      $instance->setConfig($configuration['config']);
+    }

     return $instance;
   }
@@ -64,12 +67,12 @@ class DefaultFactory implements FactoryInterface {

@@ -64,12 +67,12 @@ class DefaultFactory implements FactoryInterface {
       throw new PluginException("The plugin type did not specify a valid key for determining the plugin instance class.");
     }

-    $plugin_definition = $this->discovery->getPluginDefinition($plugin_id);
-    if (empty($plugin_definition[$this->class_key])) {
+    $this->plugin_definition = $this->discovery->getPluginDefinition($plugin_id);
+    if (empty($this->plugin_definition[$this->class_key])) {
       throw new PluginException("The plugin did not specify an instance class.");
     }

-    $class = $plugin_definition[$this->class_key];
+    $class = $this->plugin_definition[$this->class_key];

Yes, to get tests passing I had to do a little in the factory, so it's definitely relevant.

+++ b/core/lib/Drupal/Core/Plugin/Discovery/ConfigDiscovery.php
@@ -22,7 +22,7 @@ class ConfigDiscovery implements DiscoveryInterface {
-      $derviative_mapper = new $config['derivative']($this->definition_root, $plugin_id, $config);
+      $derviative_mapper = new $config['derivative']();

Quite a few more changes beyond this in the new code base, I still don't have tests for config discovery yet, but I'm pretty confident they could be written at this point easily enough, and the 10 tests I have working against static are a good map of what needs testing.

And, another derivatives patch:

effulgentsia’s picture

StatusFileSize
new13.43 KB

Thanks!

This is a reroll (with some very minor tweaks) that rebases to #1540206-14: Allow self inspection on instantiated plugin objects. In other words, to see this in context, apply that patch first, then this one on top. My reasoning is I think that patch is close to being committed, and I think there's things there we want to leverage in this issue. I'll post another patch here soon (as a way to express some ideas I have) with an interdiff relative to this one, so we can pick it apart and cherry pick as needed.

effulgentsia’s picture

Ok, I went further with this than I expected, so these patches are large. Some key things in here:

- I made both getDerivative() and getDerivatives() static, not just the latter.
- Because they're static, I made them take $pluginType and $pluginId as parameters, because even if 99% of implementations won't need them (1 DerivativeInterface class per plugin), I don't think we should require that assumption.
- I removed DiscoveryAbstract and moved that logic to DerivativeDiscovery.
- I removed the test derivative class and moved that logic to TestPlugin, since you said above that that makes more sense, and I agree.

Perhaps we can chat about it tomorrow if that helps. This is all untested and needs polish, but I wanted to get these ideas in front of you to review.

neclimdul’s picture

I'm still not convinced of the static methods. It seems like we're making them static because ctools did and I can't find a good reason to do it.

I also like the the approach in my branch which seems to leave a much simpler discovery class and which the change I just pushed a simple conversion process of extending DerivativeDiscoveryBase and renaming your getPluginDefinition/getPluginDefinitions functions to loadPluginDefinition/loadPluginDefinitions.

effulgentsia’s picture

Status: Needs work » Needs review
StatusFileSize
new25.05 KB
new26.19 KB

Here's a patch rebased to #1540206-14: Allow self inspection on instantiated plugin objects as in #9, along with an interdiff relative to #9. Tests are passing.

It seems like we're making them static because ctools did and I can't find a good reason to do it.

A reason for making them static is to allow the plugin class itself to implement DerivativeInterface for itself, instead of requiring a separate class. This makes sense if you think about blocks for example, where it's logical for the SystemMenuBlock class to implement its functionality and also implement methods that return its derivatives (a block per system menu). But, we shouldn't need to instatiate a SystemMenuBlock instance to ask the class for its derivatives. This patch includes a test for this.

I also like the the approach in my branch which seems to leave a much simpler discovery class

I think the approach in this patch also leaves a pretty simple StaticDiscovery and ConfigDiscovery implementations. The main difference here is that instead of inheriting from DiscoveryAbstract, it composes a DerivateDiscovery object. And I prefer composition here to inheritance, because DerivativeDiscovery (or in your branch, DiscoveryAbstract) contains some arbitrary decisions (e.g., using a colon delimiter) that in my opinion should be possible to swap out without needing to make changes to StaticDiscovery/ConfigDiscovery.

The patch is large, but I think it's mostly just about these 2 design goals.

neclimdul’s picture

I'm still not convinced. committing to a static method means we can never do anything like pass the global container or a database object to the constructor.

eclipsegc’s picture

Hmmm... can we solve this by just passing an options array to both static methods that we know we won't need 99% of the time? I hear what you're saying here though, but Alex's point about us not being able to use this on the plugin class w/o static methods is really valid, and I'd REALLY like to not lose that now that I've got it :-D

Eclipse

effulgentsia’s picture

How about supporting both a non-static interface and a static one? Like in this patch. The full patch is still relative to #1540206-14: Allow self inspection on instantiated plugin objects (I have not rebased to yched's follow-up patches in that issue), and the interdiff is relative to #12.

effulgentsia’s picture

StatusFileSize
new26.62 KB

This is rerolled to apply directly on plugins-next, so that it can proceed independently of #1540206: Allow self inspection on instantiated plugin objects, which requires more discussion.

neclimdul’s picture

Status: Needs review » Needs work
+++ b/core/lib/Drupal/Component/Plugin/Discovery/StaticDiscovery.phpundefined
@@ -14,17 +14,33 @@ class StaticDiscovery implements DiscoveryInterface {
+  public function __construct(DerivativeDiscoveryInterface $derivativeDiscovery = NULL) {
+    $this->derivativeDiscovery = isset($derivativeDiscovery) ? $derivativeDiscovery : new NullDerivativeDiscovery();
+  }

I'm a little uncertain of this derivative class for figuring out derivatives and then a different class and interface for each plugin instance. It seems harder to understand. The null fallback is fairly clever though.

+++ b/core/lib/Drupal/Component/Plugin/Discovery/DerivativeDiscovery.phpundefined
@@ -0,0 +1,88 @@
+  /**
+   * Returns the object that implements DerivativeInterface or class that implements StaticDerivativeInterface on behalf of the plugin.
+   *
+   * Returns NULL if the plugin doesn't define a value for this class, or if the
+   * class doesn't implement the required interface. Not throwing an error for
+   * either of these cases allows for flexibility in allowing but not requiring
+   * plugin classes to directly handle their own derivative fetching.
+   */
+  protected function getDerivativeClassOrObject($basePluginId, $baseDefinition) {
+    if (!isset($this->implementors[$basePluginId])) {
+      $this->implementors[$basePluginId] = FALSE;
+      if (isset($baseDefinition[$this->classKey])) {
+        $class = $baseDefinition[$this->classKey];
+        $reflector = new \ReflectionClass($class);
+        if ($reflector->implementsInterface('Drupal\Component\Plugin\Derivative\DerivativeInterface')) {
+          $this->implementors[$basePluginId] = new $class($basePluginId, $this->pluginType);
+        }
+        elseif ($reflector->implementsInterface('Drupal\Component\Plugin\Derivative\StaticDerivativeInterface')) {
+          $this->implementors[$basePluginId] = $class;
+        }
+      }
+    }
+    return $this->implementors[$basePluginId] ? $this->implementors[$basePluginId] : NULL;

I'm really not a fan of this, especially the "maybe we call a static method maybe we call a normal method" runtime on the calling side.

Kris and I are going to discuss this more tomorrow but just wanted to jot down some notes on looking over this patch.

neclimdul’s picture

merlin, eclipse and I had a call today and we decided supporting both static and instantiated methods is making things a bit of a mess. Eclipse conceded(perhaps a bit reluctantly) that using statics means we can not easily use any sort of constructor or injection if we go with static methods. It'd require a soft options array sort of argument. (side note, crell will also be happy)

So action items, look at at effulgentsia's last patch and see if we can't merge it into eclipse's earlier patch. Maybe using some of the branch I was working on if it makes sense.

Timeframe, we'll see how the weekend goes and if I'm able to review, if not eclipse and I will try and work on it during the week, worst case eclipse will probably work on it Friday during his SCOTCH time.

effulgentsia’s picture

Status: Needs review » Needs work
StatusFileSize
new20.52 KB

Ok, here's #16 with everything related to StaticDerivativeInterface removed.

I'm a little uncertain of this derivative class for figuring out derivatives and then a different class and interface for each plugin instance. It seems harder to understand.

I changed some terminology in this patch's DerivativeDiscovery class to make it even more confusing to draw attention to it: a really ugly name for its protected function: getPluginSpecificDerivativeDiscovery().

So basically, we have:
- Drupal\Component\Plugin\Derivative\DerivativeInterface: the interface that needs to be implemented by the $pluginDefinition['derivative'] class. This class deals with returning derivatives of a particular plugin. It doesn't trouble itself with splitting or joining on colon.
- Drupal\Component\Plugin\Discovery\DerivativeDiscoveryInterface: the interface that needs to be implemented by an object for merging base plugins with derivative plugins. I don't think it's technically a "mediator" design pattern, but I think of it as kind of mediating between the DiscoveryInterface object and the DerivativeInterface object, enabling those two to be fully decoupled.
- Drupal\Component\Plugin\Discovery\DerivativeDiscovery: the 1 class that provides an implementation of DerivativeDiscoveryInterface we expect to be suitable for 99% of plugin types that want to support derivative plugins.

While I think the names of all of the above could be improved to help clarify the relationships, I think the first question to settle is whether this architecture is what we want at all (no use brainstorming better names if we don't like the pattern).

The alternative is what neclimdul has in plugins-derivatives, where instead of a separate DerivativeDiscovery class, that logic is put inside DiscoveryAbstract and all Discovery classes inherit from that. That cuts down on the number of classes that need to be named, but I'm concerned that it makes some arbitrary logic (e.g., $derivativePluginId = $basePluginId . ':' . $derivativeId;) not easily swappable (for example, if for whatever weird reason, I want my plugin type to combine base plugin ids and derivative ids differently, I now need to subclass ConfigDiscovery or InfoHookDiscovery into SlightlyDifferentConfigDiscovery or SlightlyDifferentInfoHookDiscovery just to override the logic inherited from DiscoveryAbstract). If we're willing to accept this limitation due to this being an edge use-case, I won't object to us going back to the DiscoveryAbstract approach, but it stands out to me as unnecessary coupling.

effulgentsia’s picture

Status: Needs work » Needs review
sun’s picture

Status: Needs work » Needs review

Sorry, I totally wanted to post a more in-depth review here (some days ago), but only got this far. Got totally distracted with CMI and Testing system work. :-/ I'm not sure whether this helps in any way, but well...:

+++ b/core/lib/Drupal/Component/Plugin/Derivative/DerivativeInterface.php
@@ -1,36 +1,49 @@
+ * Interface implemented by plugins needing derivative support.

Is there any reason for why you guys chose to go with the name "derivatives" instead of "variants", which sounds much more human and pronounceable to me? At least for me, as a non-native English speaker, and as a non-native OO speaker, that would be much more clear.

+++ b/core/lib/Drupal/Component/Plugin/Derivative/DerivativeInterface.php
@@ -1,36 +1,49 @@
+   *   The derivative id. The id must uniquely identify the derivative within a
+   *   given base plugin, but derivative ids can be reused across base plugins.

It's no clear to me how a variant/derivative of one base plugin could ever be re-used for another base plugin, and why we actively try to support that.

+++ b/core/lib/Drupal/Component/Plugin/Derivative/DerivativeInterface.php
@@ -1,36 +1,49 @@
+   *   Implementations of
+   *   Drupal\Component\Plugin\Discovery\DerivativeDiscoveryInterface combine
+   *   the base plugin id with $derivativeId to construct the derivative's full
+   *   plugin id.

No idea what this means, needs more context.

effulgentsia’s picture

StatusFileSize
new17.57 KB

In IRC, neclimdul said that even though the DerivativeDiscovery mediator might be more architecturally pure, he felt it's not worth the added mental strain of yet another hard to name object/class in the whole system. In practice, the two assumptions it makes (combining base id and derivative id with a colon, and finding the plugin's derivative fetching class in $definition['derivative']) rarely need to be overridden, and in the rare cases where they do, subclassing the top-level Discovery class isn't that big a deal.

So, here's a patch that's closer to neclimdul's original architecture of using an abstract base class, which I named DerivativeAwareDiscovery.

It's no clear to me how a variant/derivative of one base plugin could ever be re-used for another base plugin, and why we actively try to support that.

The derivative ids can be reused (i.e., have local scope). Block plugin "foo" can have derivatives "x" and "y", and block plugin "bar" can have derivatives "x" and "z". The discovery system translates this into "foo:x", "foo:y", "bar:x", and "bar:z".

neclimdul’s picture

StatusFileSize
new21.77 KB
new14.46 KB

Ok, spent the day reviewing this and I like it. I'm not sure the fetcher helper buys us a lot but it probably doesn't hurt so I didn't touch it. Did some cleanups and a couple tweaks:

  1. Convert args and function variables to _ seperated.
  2. Convert some class variables to camelCase.
  3. Some doc cleanups.
  4. Split encode/decode of plugin/derivative id into functions so it /could/ be modified and is better documented.

If no one has any objections I'll commit it and we'll put this to rest at long last.

neclimdul’s picture

oh, the split encode/decode logic has a slightly cheaper set of string functions when dealing with derivative id's(from some goofy synthetic benchmarks). they where generally close though and this seems easier to read.

effulgentsia’s picture

Status: Needs review » Reviewed & tested by the community

Thanks!

neclimdul’s picture

Status: Reviewed & tested by the community » Fixed

I can't express how thankful I am for driving this home effulgentsia.

webchick’s picture

YAYYYY!! Great job, guys!!

eclipsegc’s picture

+1000000

Please link the core issue that will follow this patch (or whatever) here.

effulgentsia’s picture

#1497366: Introduce Plugin System to Core is the core issue where neclimdul will post an updated patch. Last one there is from too long ago. IMO, we should get these major/critical issues in first, though perhaps the documentation one can happen in stages so as not to delay getting the code itself in front of core reviewers.

Status: Fixed » Closed (fixed)

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