In #1529162: Decouple plugin type discovery from plugin discovery, we decided to remove the concept of kernels, and have plugin type objects be the front objects for application code to interact with all plugin-related functionality for that type. We also decided (based on feedback from merlinofchaos) that the plugin type class does not in general need to be runtime swappable. For example, if we have a Drupal\Core\Cache\Cache class implementing the core cache plugin type, it should be okay for application code to call $cache = new Drupal\Core\Cache\Cache; and then interact with that object, without requiring a dependency injection system for making this class/object swappable. As yched stated on IRC one day: we need firm ground somewhere. Of course, nothing prevents application code from choosing to use the dependency injection container for access to plugin types when that's appropriate for the application code in question, just that we should structure plugin type classes with the assumption that they themselves are not presumed to be swappable.

For discovery and factory functionality, I think we're fine. It makes sense to me that each plugin type class specifies how plugins of that type are to be discovered and the factory to use for creating instances. According to merlinofchaos, neclimdul, and EclipseGc, who all have lots of CTools plugins experience, discovery and factory are intrinsic to the plugin type, and they have not encountered use-cases where it made sense to have any application-level override of the plugin type's intrinsic discovery and factory mechanisms.

For mappers, however, I think we have a problem. These are likely to need swappability. For example, consider a module that wants to implement some alternate way of mapping cache bins to backends. For example, maybe some code that wants to check if a memcache server is available, and if not, fallback to database. Drupal\Core\Cache\Cache shouldn't lock in a mapping mechanism that a module can't reconfigure.

So, how do we do this?

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

effulgentsia’s picture

FileSize
5.46 KB

Here's a patch that demonstrates the cache use-case. There's a @todo in there pointing out that one approach is to make it the responsibility of the plugin type class to get the mapper from a dependency injection container. (Doing so requires #1538712: Merge latest 8.x HEAD into plugins-kernel).

In IRC, pounard suggested that maybe mapping should be decoupled from plugin type entirely. Anyone care to submit a patch for what that could look like?

effulgentsia’s picture

Issue summary: View changes

Updated issue summary.

effulgentsia’s picture

FileSize
5.31 KB

Here's a reroll for plugins-next, thanks to #1538712: Merge latest 8.x HEAD into plugins-kernel.

effulgentsia’s picture

FileSize
5.34 KB

This fixes some copy/paste errors.

effulgentsia’s picture

+++ b/core/lib/Drupal/Core/Cache/Cache.php
@@ -0,0 +1,21 @@
+class Cache extends PluginType {
+
+  public function __construct() {
+    $this->discovery = NULL;
+    $this->factory = NULL;
+    $this->mapper = drupal_container()->has('cache.mapper') ? drupal_container()->get('cache.mapper') : new DefaultCacheMapper();
+  }
+}

Just to clarify, this is the important part of the patch. I'm okay with this being our answer to making mappers runtime swappable: i.e., place the onus on the plugin type class to do so when that's what's appropriate. But if someone wants to propose a better idea, please do so.

effulgentsia’s picture

This might be a non-issue given the latest patch on #1529162: Decouple plugin type discovery from plugin discovery where we settled on each plugin type provider adding its own wrapper function and/or using drupal_container() for instantiating its plugin type objects if appropriate to that type. However, I'm leaving this open for comment from pounard, given that the plugin type / mapper relationship is something he's expressed interest in. Others with opinions are encouraged to comment too.

yched’s picture

I don't think I get how "each plugin type provider adding its own wrapper function" makes it possible to swap the mapper that gets instanciated withinPluginType::__construct() ?

neclimdul’s picture

ATM, no this probably isn't doable even with the per-type factories and that's definitely an oversight I've been mulling over. It was possibly made worse by the type object because that logic is coded not meta now. I'm pretty sure it wasn't possible before though either. Its kinda a blocker too because without it we end up just proxying around the plugin system like cache router and that sort of defeats some of the purpose of having a core plugin system.

effulgentsia’s picture

I don't think I get how "each plugin type provider adding its own wrapper function" makes it possible to swap the mapper that gets instanciated within PluginType::__construct() ?

Because per #1529162-28: Decouple plugin type discovery from plugin discovery, the wrapper function can use the DI container as appropriate for the type. For example, if a plugin type wants a swappable mapper (or a swappable anything else), it can take those objects as constructor arguments, and configure the plugin type in the DI container accordingly. merlinofchaos has been against forcing all plugin types to use the DI container, and he's basing that on his experience of there being many plugin types that don't require dependency injected factories, discovery, or mapping. For those that do require it (like cache routing), I think the DI container is the right tool for achieving it, but we can see if that bears out when we start converting various systems to the plugin architecture, and adjust if necessary.

Its kinda a blocker too because without it we end up just proxying around the plugin system like cache router and that sort of defeats some of the purpose of having a core plugin system.

I don't quite follow. Can you elaborate?

neclimdul’s picture

With hard coded mappers like we currently have then to implement custom mapping you have to use the hard coded mapper to always map to a single plugin that then implements its own mapping layer that then instantiates another plugin that handles the actual logic. Less then ideal.

yched’s picture

@effulgentsia : Ok, in the sense "the mapper can be swapped if the PluginType was coded in order to support it (receives a mapper through injection, and gets instanciated by the DI)". Which is probably good enough as far as I'm concerned.

neclimdul’s picture

After talking this through with effulgentsia last night I agree. Is there anything actionable or do we just say "you can do it" for now?

effulgentsia’s picture

Status: Needs review » Needs work

Is there anything actionable or do we just say "you can do it" for now?

I think we're all good on the plugin system code with respect to this. It might be nice to get the cache system converted to using the technique we settled on, so that when we post the next patch to #1497366: Introduce Plugin System to Core, we have a demonstration use-case. I can reroll #3 to do that, but can you merge 8.x HEAD to plugins-next first? I'm particularly wanting to use the DI changes that just landed an hour ago (note, that link is the 2nd of 2 commits on that today).

effulgentsia’s picture

Status: Needs work » Needs review
FileSize
5.28 KB

This could use some docs, but perhaps we can defer that to #1512588: [meta] Fill in plugin documentation and commit this if you think the code itself is good and demonstrates the decision we reached..

effulgentsia’s picture

+++ b/core/includes/cache.inc
@@ -27,16 +27,7 @@ function cache($bin = 'cache') {
+  return drupal_container('cache')->getPluginInstance(array('bin' => $bin));

Fixed in this patch to drupal_container()->get('cache')->...

cosmicdreams’s picture

Status: Needs review » Reviewed & tested by the community

Great Patch! love the use of ?: . I forgot about that operator

effulgentsia’s picture

Priority: Normal » Major

Upping to major, because this patch contains our first use-case for mappers, and ideally the initial patch for getting plugins into core should demonstrate use-cases for all major components.

effulgentsia’s picture

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

On second thought, maybe this is a better way to plugin'ify the cache system? With this approach, the mapper isn't runtime swappable directly, but the top-level cache plugin type is (by virtue of being in drupal_container()), so the goal of this issue is still preserved. As a bonus, this demonstrates a plugin type that uses all 3 components (discovery, factory, and mapper), solves #1512594: Figure out what a default mapper implementation would look like., and gives us a reusable PersistentVariableDiscovery class that EclipseGc asked for in IRC.

cosmicdreams’s picture

Status: Needs review » Needs work
+++ b/core/lib/Drupal/Component/Plugin/Mapper/DefaultMapper.phpundefined
@@ -29,18 +29,29 @@ class DefaultMapper implements MapperInterface {
+    $this->pluginIdKey = $plugin_id_key;

Would it be better if plugin_id_key and default_plugin_id were grouped together in a property or options array?

+++ b/core/lib/Drupal/Core/Cache/Cache.phpundefined
@@ -0,0 +1,24 @@
+  public function __construct() {
+    $this->discovery = new PersistentVariableDiscovery('cache_plugins', array('cache' => array('class' => 'Drupal\Core\Cache\DatabaseBackend')));
+    $this->factory = new DefaultFactory($this->discovery, 'class');
+    $this->mapper = new DefaultMapper($this->discovery, $this->factory, 'bin', 'cache');

It seems like we want the tight coupling that this constructor encodes for the new PersistentVariableDiscovery(), but are we sure we want to tightly couple this constructor to DefaultFactory and DefaultMapper. If not then those should be arguments.

http://martinfowler.com/articles/injection.html#ConstructorInjectionWith...

neclimdul’s picture

Now that #1538692: Change PluginTypeInterface to extend Discovery, Factory, and Mapper interfaces is in I'd been playing with the idea of keeping the DiscoveryInterface for the DefaultFactory DefaultMapper constructor but passing the type in instead since it does the default population. Yched had talked about ways to fix that but we haven't done it yet.

neclimdul’s picture

I think a DI Container could be a good approach to passing around the components of plugins internally but lets be careful about creeping this issue or put anything in the way of the core patch. Things are working.

effulgentsia’s picture

Status: Needs work » Needs review
FileSize
11.06 KB

Implemented #18.1, #19, and some other small improvements.

Regarding #18.2 (DI for Drupal/Core/Cache/Cache::__construct()), it's a chicken and egg problem. DefaultFactory and DefaultMapper require either $this or $this->discovery, so they can't be passed as constructor arguments unless the discovery object is also created by the caller. If we make all 3 constructor arguments, then there's no logic at all left in Drupal/Core/Cache/Cache. Since Drupal/Core/Cache/Cache itself is in the DI, I think it's okay for its constructor to hard-code all 3 components, as it seems easy enough for someone to just override the entire class.

neclimdul’s picture

Status: Needs review » Needs work
+++ b/core/includes/cache.incundefined
@@ -53,22 +44,12 @@ function cache($bin = 'cache') {
+  foreach (drupal_container()->get('cache')->getPluginDefinitions() as $bin => $definition) {

The keys here are plugin_id's but those aren't going to be bin's are they? They'll be something like memcache, db and null. Seem's like the cache type needs a method that talks to the mapper or something and retrieves implementations or something... though one.

effulgentsia’s picture

Status: Needs work » Needs review

I agree that it's a bit weird for bin to equal plugin_id, but that's how the cache system currently works. As per:

function cache_invalidate(array $tags) {
-  foreach (cache_get_backends() as $bin => $class) {
     cache($bin)->invalidateTags($tags);
   }
 }
 
 /**
- * Returns a list of cache backends for this site.
- *
- * @return
- *   An associative array with cache bins as keys, and backend classes as value.
- */
-function cache_get_backends() {
-  return variable_get('cache_classes', array('cache' => 'Drupal\Core\Cache\DatabaseBackend'));
-}
-

So this patch keeps that. Perhaps we need a follow-up to organize cache plugins differently, but is that a short term blocker?

neclimdul’s picture

Status: Needs review » Fixed

marking this fixed because i committed it but i think pounard is about to post comments with concerns...

Status: Fixed » Closed (fixed)

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

Anonymous’s picture

Issue summary: View changes

Updated issue summary.