Spin-off from #1863816: Allow plugins to have services injected:

Problem/Motivation

In order to make the issue above work, plugin managers and / or factories need to reference the global container, making them non-serializable (the container is based on closures).

However, we currently stores a reference to the discovery within instantiated plugin objects, to support PluginBase::getDefinition() (returns the definition entry for the object's own plugin_id).
According to #1851706: Make all core plugin managers be consistent as to whether $this or $this->discovery is passed to the factory, this discovery object is either:
- the manager, it therefore contains a reference to the container
- the raw discovery, containing in most cases a reference to the manager because of ProcessDecorator($this, 'processDefinition'); (where $this is the manager), and thus containing a reference to the container.
Meaning, in both cases, plugin objects end up containing a reference to the container, and are therefore not serializable.

This is a problem, because tracking what gets serialized where and when can be tricky.
- $form['#entity'] = $node; is going to break if/when $entities become plugin instances.
- In the D8 cycle, support for (some) FAPI/render callbacks as methods : $form['#pre_render'][] = array($this, 'myMethod');.
This is something plugin implementations are going to be very tempted to do (some of them do this already). But serialize($form) will fatal.

Proposed resolution

Do not embed the discovery in plugin instances, but directly copy the definition array. This array is going to be read-only in at least 99% cases, so thanks to php's copy-on-write behavior, this should make no difference memory-wise.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

yched’s picture

Status: Active » Needs review
FileSize
12.1 KB

Attached is a preliminary patch to get feedback.
@todo : reflect the change in all plugin types (patch only does widgets, formatters, & view plugins)

Ran xhprrof on the following test code:

      $plugins = array();
      for ($i = 0; $i < 1000; $i++) {
        foreach (field_info_instances('node', 'article') as $instance) {
          $options = array(
            'instance' => $instance,
            'type' => $instance['widget']['type'],
            'settings' => $instance['widget']['settings'],
            'weight' => $instance['widget']['weight'],
          );
          $plugins[] = drupal_container()->get('plugin.manager.field.widget')->getInstance($options);
        }
      }

      $result = array();
      foreach ($plugins as $plugin) {
        $result[] = $plugin->getDefinition();
        $result[] = $plugin->getSettings();
      }

(that's 3000 plugins on my test setup)
and the results show no difference in memory or CPU use.

yched’s picture

I'll add that when a plugin does get serialized, it containing the discovery (including CacheDecorator and its static list of definitions) is actually a bit scary - potentially huge serialized strings...

effulgentsia’s picture

I'm in favor of this.

tim.plunkett’s picture

+++ b/core/lib/Drupal/Component/Plugin/PluginBase.phpundefined
@@ -14,18 +14,18 @@
+  protected $pluginId;
...
-  protected $plugin_id;

@@ -41,28 +41,27 @@
-    $this->plugin_id = $plugin_id;
...
+    $this->pluginId = $plugin_id;

+++ b/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.phpundefined
@@ -34,7 +34,7 @@ public function permissions() {
-        $permissions["restful $lowered_method $this->plugin_id"] = array(
+        $permissions["restful $lowered_method $this->pluginId"] = array(

@@ -53,8 +53,8 @@ public function permissions() {
-    $path_prefix = strtr($this->plugin_id, ':', '/');
-    $route_name = strtr($this->plugin_id, ':', '.');
+    $path_prefix = strtr($this->pluginId, ':', '/');
+    $route_name = strtr($this->pluginId, ':', '.');

@@ -64,11 +64,11 @@ public function routes() {
-          '_plugin' => $this->plugin_id,
+          '_plugin' => $this->pluginId,
...
-          '_permission' => "restful $lower_method $this->plugin_id",
+          '_permission' => "restful $lower_method $this->pluginId",

+++ b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.phpundefined
@@ -76,7 +76,7 @@ public function post($id, EntityInterface $entity) {
-      $url = url(strtr($this->plugin_id, ':', '/') . '/' . $entity->id(), array('absolute' => TRUE));
+      $url = url(strtr($this->pluginId, ':', '/') . '/' . $entity->id(), array('absolute' => TRUE));

Not in scope

However, we can likely remove all of the use Drupal\Component\Plugin\Discovery\DiscoveryInterface;

yched’s picture

- $this->plugin_id
+ $this->pluginId 

Not in scope

Yeah, arguably not.
This patch introduces a property in PluginBase for the definition array. Calling it $definition lacks some namespacing and steals the term from possible use by actual plugin classes (views plugins actually use $definition already). So, PluginBase::$pluginDefinition.
But then it kind of clashes with the existing PluginBase::$plugin_id, which violates coding standards, so I figured I'd rename it as well, since it's only a couple hunks. I can remove that from the patch if it hurts reviewers eyes :-).

But yeah, more importantly, I'd like agreement on the approach before converting the other plugin types.

tstoeckler’s picture

This totally makes sense. Another reason why I am in favor of this is if I do a debug($plugin) I totally don't want to see the innards of some discovery object (let alone the whole plugin manager) on my screen. This benefit is surely outweighed by the other factors mentioned above, but I thought I'd bring it up.

Regarding the patch: I'm not sure, but I think our coding standards would require $plugin->pluginID though, rather than $plugin->pluginId.

yched’s picture

Real patch, then.

yched’s picture

re @tstoeckler #6: the current PluginBase::$discovery member is protected, so I don't think it would show up in a debug($plugin) anyway ?

Status: Needs review » Needs work

The last submitted patch, pluginBase_copy_definition-1875182-7.patch, failed testing.

yched’s picture

Status: Needs work » Needs review
FileSize
597 bytes
21.6 KB

Missed a direct plugin instantiation in views' PluginBaseUnitTest.

yched’s picture

Updated after "Blocks as plugins" (just needed to update the new ViewsBlock::__construct())

tim.plunkett’s picture

Status: Needs review » Needs work

The last submitted patch, pluginBase_copy_definition-1875182-11.patch, failed testing.

yched’s picture

Status: Needs work » Needs review
FileSize
34.79 KB

Un-hibernate. We need this if we want plugin objects to be able to serialize and keep our sanity.

Reroll + update for the plugins that appeared meanwhile.

Status: Needs review » Needs work

The last submitted patch, pluginBase_copy_definition-1875182-14.patch, failed testing.

EclipseGc’s picture

Failures aside, this doesn't look insane to me, and the sooner we settle this particular issue the better as new plugin types are being created. I'd really like if neclimdul could weigh in on this as I think I proposed essentially this when we started and he wanted the discovery injected and I don't recall why. But in general I'm ++ to this approach.

Eclipse

yched’s picture

Status: Needs work » Needs review
FileSize
1.73 KB
34.96 KB

Should fix the fails.

- If $plugin_id doesn't exist, $plugin_definition is NULL and DefaultFactory::getPluginClass($plugin_id, $plugin_definition) fails because its $plugin_definition param is hinted as an array. Fixed that by setting NULL as default value.
I'm just a bit surprised that we have so many cases of createInstance($invalid_plugin_id) in our test suite.

- Missed a use of $this->discovery in TipPluginBase::__construct().
But actually the $this->definition and $this->module properties defined there are useless (duplicate $this->pluginDefinition already provided by Pluginbase), and actually not used anywhere, so removed the entire __construct() override.

While I was at it, opened #1925576: "Tip" plugins managed by TourManager ??

yched’s picture

tstoeckler’s picture

Status: Needs review » Reviewed & tested by the community

Still Looks good.

neclimdul’s picture

Quick pass, got some deeper thoughts I'm working on:

+++ b/core/lib/Drupal/Component/Plugin/Factory/DefaultFactory.phpundefined
@@ -41,23 +41,23 @@ public function __construct(DiscoveryInterface $discovery) {
-   *  @param array $plugin_id
+   *  @param string $plugin_id
    *    The id of a plugin.
-   * @param \Drupal\Component\Plugin\Discovery\DiscoveryInterface $discovery
-   *   The discovery object.
+   *  @param array $plugin_definition

Looks like the indenting is off on the @param lines?

+++ b/core/modules/tour/lib/Drupal/tour/TipPluginBase.phpundefined
@@ -41,16 +40,6 @@
-    $this->module = $this->definition['module'];

This change seems unrelated and possibly a regression.

+++ b/core/modules/views/lib/Drupal/views/Tests/PluginBaseUnitTest.phpundefined
@@ -250,8 +250,9 @@ public function testSetOptionDefault() {
     $discovery = new StaticDiscovery();
-    $discovery->setDefinition('default', array());
-    return new TestHelperPlugin(array(), 'default', $discovery);
+    $definition = array();
+    $discovery->setDefinition('default', $definition);
+    return new TestHelperPlugin(array(), 'default', $definition);

hugh? if we're not passing discovery why are we still bothering with it here?

neclimdul’s picture

Title: Copy the definition instead of referencing the discovery in plugin objects » Pass plugin definition to plugin instances instead of discovery object

Megh, I don't think its as big a deal. The lack of changes this caused to testing makes me very unhappy with the plugin tests but that's unrelated. I'm not sure we need to remove this from the reflection factory though.

+++ b/core/lib/Drupal/Component/Plugin/Factory/ReflectionFactory.phpundefined
@@ -46,28 +47,29 @@ public function createInstance($plugin_id, array $configuration) {
-      elseif ($param_class && $param_class->isInstance($this->discovery)) {
-        $arguments[] = $this->discovery;
-      }
yched’s picture

Regarding TipPluginBase :

@@ -41,16 +40,6 @@
-    $this->module = $this->definition['module'];

See 2nd point in #17. $this->module is not used at all in TipPluginBase or existing tip plugin implementations. This __construct() override is useless, and #1932048: Clean up tour ConfigEntity architecture also removes it. Even if i kept it here, either patch would need to be rerolled after the other one gets in, so I see no point in preserving it in this patch.

Attached patch fixes the other points mentioned in #20.

Regarding #21: Yeah, not sure either but :
- Plugins containing a reference to the factory are not something we want to encourage, for the reasons that are the motivations of this patch (serialization hell down the road)
- ReflectionFactory has no ability to pass the manager or the factory (itself) down to plugins, so why would discovery be special ? It only had the ability to pass the discovery because that was the regular way for a plugin to access its own definition. We're changing that, so why would we allow passing the discovery any longer ?

For now I left those lines removed.

yched’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
34.19 KB

Reroll.

Actually #1932048: Clean up tour ConfigEntity architecture didn't remove the needless __construct() override in TipPluginBase, so I left it here as well, and will post a followup over there.

yched’s picture

Status: Needs review » Reviewed & tested by the community

Green, back to RTBC.

EclipseGc’s picture

EclipseGc’s picture

After discussing this further, neclimdul and I are both ++ on the RTBC for this if 23 comes back green again.

Eclipse

EclipseGc’s picture

Also, effulgentsia indicated support of the approach in general in #3, so I think we have all 3 of the plugin devs on board here.

Eclipse

webchick’s picture

Status: Reviewed & tested by the community » Fixed

This makes a lot more sense to me, and seems to simplify things quite a bit too. Yay!

Committed and pushed to 8.x. Thanks!

Status: Fixed » Closed (fixed)

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