Problem/Motivation

Blocks make use of categorized plugins. Rules needs exactly the same functionality for actions and conditions and other plugins - so let's make it re-usable.

Proposed resolution

Provide a trait and a respective interface.

Remaining tasks

Do it and make use of it for actions and conditions.

User interface changes

-

API changes

None.

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Task: It refactors existing functionality to make it re-usable and applies it to more plugin types, e.g. #2373491: Categorize field type plugins
Issue priority Major, as it is important to contributed projects (=Rules) for being able to keep supporting d7 features based on d8 core APIs.
Disruption The change is not disruptive as it is totally backwards compatible, but
  • it allows Rules to keep supporting d7 features based on d8 core APIs (=categorized conditions, actions)
  • makes doing categorized plugins easy and consistent, what is a win for other contribs and possibly core plugin types.
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

fago’s picture

Status: Active » Needs review
FileSize
4.64 KB

first patch attached, does not yet implement it for conditions and actions.

Status: Needs review » Needs work

The last submitted patch, 1: d8_extraction.patch, failed testing.

fago’s picture

Title: Provide a trait for categorizing plugin managers » Provide a trait for categorizing plugin managers and use it for conditions and actions
Status: Needs work » Needs review
FileSize
11.01 KB

Improved the trait a bit and updated the patch to make use of the new trait for conditions and actions also.

fago’s picture

Added the category to annotations.

Status: Needs review » Needs work

The last submitted patch, 4: d8_category.patch, failed testing.

The last submitted patch, 3: d8_category.patch, failed testing.

fago’s picture

Status: Needs work » Needs review
FileSize
3.29 KB
12.58 KB

Completed the patch, this should be ready now.

Status: Needs review » Needs work

The last submitted patch, 7: d8_category.patch, failed testing.

fago’s picture

Status: Needs work » Needs review
FileSize
14.2 KB

ouch - BlockManager::getSortedDefinitions() was filtering for definitions without context. We've got test coverage for that, but it's not documented to behave that way. :-/

Let's fix this, such that we can make use of those helpers with passed in $definitions as well. That way you can categorize any set of already filtered definitions, while it just defaults to use all definitions.

fago’s picture

FileSize
5.13 KB

attaching interdiff also

Xano’s picture

FileSize
3.87 KB
14.47 KB
+++ b/core/lib/Drupal/Core/Plugin/CategorizingPluginManagerTrait.php
@@ -0,0 +1,113 @@
+  /**
+   * Processes a definition to ensure there is a category.
+   *
+   * If the definition lacks a category, it defaults to the providing module.
+   *
+   * @param array $definition
+   *   The plugin definition.
+   */
+  protected function processDefinitionCategory(&$definition) {
+    // Ensure that every block has a category.
+    if (empty($definition['category'])) {
+      $definition['category'] = $this->getModuleName($definition['provider']);
+    }
+  }

We are now introducing even more code that does not work with object plugin definitions.

fago’s picture

Yes. I hate the fact that plugin definitions are bound to arrays, but that's the current API so we have to work with that. Also, it's just a default implementation - you do not have to use it.

fago’s picture

  1. +++ b/core/lib/Drupal/Component/Plugin/CategorizingPluginManagerInterface.php
    @@ -34,13 +34,12 @@ public function getSortedDefinitions(array $definitions = NULL);
    +   * @param array[]|object[]|null $definitions
    

    array[]|null - yes.

  2. +++ b/core/lib/Drupal/Component/Plugin/CategorizingPluginManagerInterface.php
    @@ -34,13 +34,12 @@ public function getSortedDefinitions(array $definitions = NULL);
    +   * @return array[]|object
    

    nope, as to HEAD this is an array.

  3. +++ b/core/lib/Drupal/Core/Plugin/CategorizingPluginManagerTrait.php
    @@ -35,7 +35,7 @@
    -   * @param array $definition
    +   * @param array[] $definition
    

    No, it's single valued.

  4. +++ b/core/lib/Drupal/Core/Plugin/CategorizingPluginManagerTrait.php
    @@ -99,9 +97,10 @@ public function getSortedDefinitions(array $definitions = NULL, $label_key = 'la
    -   * See \Drupal\Component\Plugin\CategorizingPluginManagerInterface::getGroupedDefinitions().
    +   * Implements \Drupal\Component\Plugin\CategorizingPluginManagerInterface::getGroupedDefinitions().
    

    Is that the standard way to do it? I copied the See from an existing trait.

Xano’s picture

FileSize
1.84 KB
3.62 KB

Status: Needs review » Needs work

The last submitted patch, 14: drupal_2349991_14.patch, failed testing.

Xano’s picture

Status: Needs work » Needs review
FileSize
14.5 KB
fago’s picture

Thanks, changes look good.

fago queued 16: drupal_2349991_16.patch for re-testing.

klausi’s picture

This looks almost ready!

  1. +++ b/core/lib/Drupal/Core/Plugin/CategorizingPluginManagerTrait.php
    @@ -0,0 +1,112 @@
    +trait CategorizingPluginManagerTrait {
    

    Looks like there is no test coverage for this trait? Or is that already covered with block plugin test cases? Could you add a comment where this is covered?

  2. +++ b/core/lib/Drupal/Core/Plugin/CategorizingPluginManagerTrait.php
    @@ -0,0 +1,112 @@
    +  /**
    +   * Processes a definition to ensure there is a category.
    +   *
    +   * If the definition lacks a category, it defaults to the providing module.
    +   *
    +   * @param array $definition
    +   *   The plugin definition.
    +   */
    +  protected function processDefinitionCategory(&$definition) {
    +    // Ensure that every block has a category.
    +    if (empty($definition['category'])) {
    +      $definition['category'] = $this->getModuleName($definition['provider']);
    +    }
    +  }
    

    So the assumption is that only modules can provide plugins? The plugin definition uses "provider" to name the extension providing the plugin - I'm not sure if themes or install profiles even can provide plugins.

Berdir’s picture

2. More importantly, the comments still talk about block, should be plugin.

A install profile should be in the module list too, and the worst thing that would happen to a module is that it would get the machine name in category if it doesn't specify it manually.

fago’s picture

Looks like there is no test coverage for this trait? Or is that already covered with block plugin test cases? Could you add a comment where this is covered?

Yes, it's mostly covered indirectly with BlockUITest, but this misses coverage for some edge cases. So I've added a unit test case for it.

ad 2, I fixed the block comment to talk about a plugin now. Also, I improved code a bit such that categorized definitions are sorted as well.

klausi’s picture

Status: Needs review » Needs work

The new test case looks good, thanks! Only minor nitpicks left:

  1. +++ b/core/tests/Drupal/Tests/Core/Plugin/CategorizingPluginManagerTraitTest.php
    @@ -0,0 +1,151 @@
    +     * @var \Drupal\Component\Plugin\CategorizingPluginManagerInterface | \PHPUnit_Framework_MockObject_MockObject
    

    there should not be a a space before and after the | separator.

  2. +++ b/core/tests/Drupal/Tests/Core/Plugin/CategorizingPluginManagerTraitTest.php
    @@ -0,0 +1,151 @@
    +          'vegetables'
    

    missing trailing comma.

  3. +++ b/core/tests/Drupal/Tests/Core/Plugin/CategorizingPluginManagerTraitTest.php
    @@ -0,0 +1,151 @@
    +      $this->assertSame(array_keys($sorted), ['apple', 'mango', 'cucumber']);
    

    Inconsistent usage of [] and array() in this test file. Please use one or the other everywhere.

  4. +++ b/core/tests/Drupal/Tests/Core/Plugin/CategorizingPluginManagerTraitTest.php
    @@ -0,0 +1,151 @@
    +    /**
    +     * Implements getDefinitions() for the trait to provide some test definitions.
    +     *
    +     * @return array
    +     */
    +    public function getDefinitions() {
    

    no needs to document @return here. Just use {@inheritdoc} and add the additional comment under it.

  5. +++ b/core/tests/Drupal/Tests/Core/Plugin/CategorizingPluginManagerTraitTest.php
    @@ -0,0 +1,151 @@
    +     * Implements getDefinitions() for the trait to provide some test definitions.
    

    Exceeds 80 chars.

  6. +++ b/core/tests/Drupal/Tests/Core/Plugin/CategorizingPluginManagerTraitTest.php
    @@ -0,0 +1,151 @@
    +    /**
    +     * {@inheritdoc}
    +     */
    +    public function processDefinition(&$definition, $plugin_id) {
    +      parent::processDefinition($definition, $plugin_id);
    +      $this->processDefinitionCategory($definition);
    +    }
    

    why does the test class have to override this method? Please add a comment.

fago’s picture

Status: Needs work » Needs review
FileSize
3.1 KB
19.22 KB

Thanks, updated patch to account for all remarks exception for the following:

why does the test class have to override this method? Please add a comment.

If you read the code, it should tell you. I'd have added the comment that it processes definition categories, but that the method call already says - so I just left it as it was.

klausi’s picture

Status: Needs review » Reviewed & tested by the community

Ah, I see.

So this looks good to me now.

amateescu’s picture

FileSize
19.23 KB
823 bytes
+++ b/core/lib/Drupal/Core/Plugin/CategorizingPluginManagerTrait.php
@@ -0,0 +1,114 @@
+      $grouped_definitions[$definition['category']][$id] = $definition;

An object is not casted to a string when used as an array key.

Simple fix attached since I already wrote it when implementing this new trait for field type plugins.

yched’s picture

An object is not casted to a string when used as an array key.

$definition['category'] can be an object ? Sounds weird - when does this happen ?

yched’s picture

Also, not convinced by "use module name as fallback when no description is provided".
That's taken from the exiisting code for Blocks, and is possibly fine for the Blocks UI use case, but doesn't feel like the generic behavior we want for all cases ?

amateescu’s picture

$definition['category'] can be an object ? Sounds weird - when does this happen ?

It's always an object, just like 'label' and 'description' and all the other translatable keys from a plugin annotation, they're instances of \Drupal\Core\StringTranslation\TranslationWrapper.

Also, not convinced by "use module name as fallback when no description is provided".

That's just a protected helper method, it's not the default, as can be seen in the patch from #2373491: Categorize field type plugins.

Wim Leers’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/lib/Drupal/Component/Plugin/CategorizingPluginManagerInterface.php
    @@ -0,0 +1,50 @@
    +   * Gets sorted definitions.
    ...
    +   *   (optional) The definitions to sort. If omitted, all definitions are used.
    ...
    +   * Gets sorted definitions grouped by category.
    ...
    +   * whereas definitions are sorted by label.
    ...
    +   *   (optional) The definitions to group. If omitted, all definitions are
    

    s/definitions/plugin definitions/

  2. +++ b/core/lib/Drupal/Core/Block/BlockManager.php
    @@ -23,17 +23,13 @@
    +  use CategorizingPluginManagerTrait {
    +    getSortedDefinitions as traitGetSortedDefinitions;
    +    getGroupedDefinitions as traitGetGroupedDefinitions;
    +  }
    

    Wow, never seen this before.

    At first: WTF.

    Having read the other hunks in this file: makes sense.

  3. +++ b/core/lib/Drupal/Core/Plugin/CategorizingPluginManagerTrait.php
    @@ -0,0 +1,114 @@
    +  }
    

    Still only supports modules?

  4. +++ b/core/lib/Drupal/Core/Plugin/CategorizingPluginManagerTrait.php
    @@ -0,0 +1,114 @@
    +   * Implements \Drupal\Component\Plugin\CategorizingPluginManagerInterface::getCategories().
    ...
    +   * Implements \Drupal\Component\Plugin\CategorizingPluginManagerInterface::getSortedDefinitions().
    ...
    +   * Implements \Drupal\Component\Plugin\CategorizingPluginManagerInterface::getGroupedDefinitions().
    

    {@inheritdoc}

  5. +++ b/core/tests/Drupal/Tests/Core/Plugin/CategorizingPluginManagerTraitTest.php
    @@ -0,0 +1,151 @@
    +    }
    +  }
    ...
    +  }
    +}
    ...
    +  }
    +}
    

    Nitpicky nitpick: missing trailing newline.

yched’s picture

@a 'fliptable' mateescu : oh, right - thanks, don't mind me :-)

fago’s picture

Status: Needs work » Needs review
FileSize
19.3 KB
3.09 KB

addressed #29.

ad, 29.3: >Still only supports modules?

Not sure how you mean that, but nope - it works with any kind of categories. The default-default is the module/provider of the plugin, but any plugin manager can use it's own default category.

klausi’s picture

Status: Needs review » Reviewed & tested by the community

#29.4: {@inheritfoc} does not fit here because a trait cannot implement an interface, so it does not inherit anything. So the direct reference is fine.

So looks RTBC again.

fago’s picture

#29.4: {@inheritfoc} does not fit here because a trait cannot implement an interface, so it does not inherit anything. So the direct reference is fine.

Ops, sry I forgot to mention that - thanks. Yes, inheritdoc is not applicable, so we use those "Implements .." docs instead. The patch just follows what we are already doing for traits elsewhere - no new invention :)

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs issue summary update

This issue is a normal task so we need to outline how it fits within the allowable Drupal 8 beta criteria. Can someone add Drupal 8 beta phase evaluation template to the issue summary.

fago’s picture

Priority: Normal » Major
Issue summary: View changes
Issue tags: -Needs issue summary update +Contributed project blocker

I've updated the summary with the template. Also, this is important for Rules to be able to keep supporting d7 features based on d8 core APIs, what I think qualifies this to be major.

fago’s picture

Issue summary: View changes
Status: Needs work » Reviewed & tested by the community
fago’s picture

Issue summary: View changes
fago’s picture

Apparently there can be troubles if system.module is included as well, thus we need to get #2392281: system.module is included in our PHPUnit tests in first.

dawehner’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/lib/Drupal/Core/Block/BlockManager.php
    @@ -56,61 +52,27 @@ public function __construct(\Traversable $namespaces, CacheBackendInterface $cac
    +    // Do not display the 'broken' plugin in the UI.
    +    unset($definitions['broken']);
    +    return $definitions;
    

    It is odd that the BlockManager has to take care of that. A concept of a broken plugin is probably a problem in a lot of places. Do we have an issue to solve just that? (I know its out of scope for this issue)

  2. +++ b/core/lib/Drupal/Core/Plugin/CategorizingPluginManagerTrait.php
    @@ -0,0 +1,114 @@
    +  /**
    +   * Gets the name of the module.
    +   *
    +   * @param string $module
    +   *   The machine name of a module.
    +   *
    +   * @return string
    +   *   The human-readable module name if it exists, otherwise the
    +   *   machine-readable module name.
    +   */
    +  protected function getModuleName($module) {
    +    // Gather module data.
    +    if (!isset($this->moduleData)) {
    +      $this->moduleData = system_get_info('module');
    +    }
    +    // If the module exists, return its human-readable name.
    +    if (isset($this->moduleData[$module])) {
    +      return $this->t($this->moduleData[$module]['name']);
    +    }
    +    // Otherwise, return the machine name.
    +    return $module;
    +  }
    

    Isn't that exactly what ModuleHandlerInterface::getName() is supposed to do? ... In case there are modules without a label, we should fix it in there.

  3. +++ b/core/lib/Drupal/Core/Plugin/CategorizingPluginManagerTrait.php
    @@ -0,0 +1,114 @@
    +  /**
    +   * Implements \Drupal\Component\Plugin\CategorizingPluginManagerInterface::getCategories().
    +   */
    ...
    +  /**
    +   * Implements \Drupal\Component\Plugin\CategorizingPluginManagerInterface::getSortedDefinitions().
    +   */
    ...
    +  /**
    +   * Implements \Drupal\Component\Plugin\CategorizingPluginManagerInterface::getGroupedDefinitions().
    +   */
    

    Let's not criple in these old documentation lines and use {@inheritdoc} instead.

  4. +++ b/core/lib/Drupal/Core/Plugin/CategorizingPluginManagerTrait.php
    @@ -0,0 +1,114 @@
    +    $categories = array_unique(array_values(array_map(function ($definition) {
    

    Maybe this line is worth to have a quick one line documentation.

  5. +++ b/core/tests/Drupal/Tests/Core/Plugin/CategorizingPluginManagerTraitTest.php
    @@ -0,0 +1,156 @@
    +namespace Drupal\Tests\Core\Plugin {
    

    ... Afaik always did not used the intentation, but gladly we don't need it.

  6. +++ b/core/tests/Drupal/Tests/Core/Plugin/CategorizingPluginManagerTraitTest.php
    @@ -0,0 +1,156 @@
    +  class CategorizingPluginManagerImpl extends DefaultPluginManager implements CategorizingPluginManagerInterface {
    

    Is Impl. as a name a thing?

fago’s picture

Status: Needs work » Needs review
FileSize
19.75 KB
11.39 KB

It is odd that the BlockManager has to take care of that. A concept of a broken plugin is probably a problem in a lot of places. Do we have an issue to solve just that? (I know its out of scope for this issue)

Agreed, not that I'd know.

Isn't that exactly what ModuleHandlerInterface::getName() is supposed to do? ... In case there are modules without a label, we should fix it in there.

Right, except that in our case $module can be any provider. I improved things by renaming this to getProviderName() and to use the module handler for fetching the module name anyway.

Let's not criple in these old documentation lines and use {@inheritdoc} instead.

See comments #32,#33.

4.,5, 6.: fixed

fago’s picture

tim.plunkett’s picture

  1. +++ b/core/lib/Drupal/Core/Plugin/CategorizingPluginManagerTrait.php
    @@ -43,31 +36,43 @@ protected function processDefinitionCategory(&$definition) {
    +    // If the class has an injected module handler, use it. Otherwise fall back
    +    // to fetch it from the service container.
    +    if (isset($this->moduleHandler)) {
    +      return $this->moduleHandler;
    +    }
    +    return \Drupal::moduleHandler();
    

    Why not follow our normal pattern?

    if (!$this->moduleHandler) {
      $this->moduleHandler = \Drupal::moduleHandler();
    }
    return $this->moduleHandler;
    

    Then no need for a comment.

  2. +++ b/core/lib/Drupal/Core/Block/BlockManager.php
    @@ -56,61 +52,27 @@ public function __construct(\Traversable $namespaces, CacheBackendInterface $cac
    +    // Do not display the 'broken' plugin in the UI.
    +    unset($definitions['broken']);
    ...
         // Do not display the 'broken' plugin in the UI.
    -    unset($definitions['broken']);
    +    unset($definitions[$this->t('Block')]['broken']);
    

    Now we have this twice, once with using a t() call as the array key? :( :( :(

fago’s picture

Why not follow our normal pattern?

Because there is no easy way to inject into the trait and make users of the trait aware of that - but the modulehandler property is already set on plugin managers so we can access it that way as documented. That way, the trait will respect the dependency injected module handler of default plugin managers.
-> Imo it makes sense to comment that, plus we need to use isset() or it will notice if the trait-ed objected does not have the property.

Now we have this twice, once with using a t() call as the array key? :( :( :(

>Now we have this twice, once with using a t() call as the array key? :( :( :(
Well, the translated key is just how optgroups work - they have the label as key - nothing we can do about that. Having the unset twice is unfortunate - yes, but the whole broken thing of filtering it out for some UI-used methods but not in some UI-unused methods is a pre-existing ugly thing of blockmanager - that's not changed here.

dawehner’s picture

+++ b/core/lib/Drupal/Core/Plugin/CategorizingPluginManagerTrait.php
@@ -0,0 +1,120 @@
+  /**
+   * Implements \Drupal\Component\Plugin\CategorizingPluginManagerInterface::getCategories().
+   */
...
+  /**
+   * Implements \Drupal\Component\Plugin\CategorizingPluginManagerInterface::getSortedDefinitions().
+   */
...
+  /**
+   * Implements \Drupal\Component\Plugin\CategorizingPluginManagerInterface::getGroupedDefinitions().
+   */

Can we just fix that quickly?

dawehner’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: +Ghent DA sprint

I'm stupid.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Unblocking contrib is prioritised for beta and having less duplicate code reduces fragility. Committed d3e3d70 and pushed to 8.0.x. Thanks!

Thanks for adding the beta evaluation for to the issue summary.

  • alexpott committed d3e3d70 on 8.0.x
    Issue #2349991 by fago, Xano, amateescu: Provide a trait for...

Status: Fixed » Closed (fixed)

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