Commit d3e3d701 authored by alexpott's avatar alexpott

Issue #2349991 by fago, Xano, amateescu: Provide a trait for categorizing...

Issue #2349991 by fago, Xano, amateescu: Provide a trait for categorizing plugin managers and use it for conditions and actions
parent a5f0e6af
<?php
/**
* @file
* Contains \Drupal\Component\Plugin\CategorizingPluginManagerInterface.
*/
namespace Drupal\Component\Plugin;
/**
* Defines an interface for plugin managers that categorize plugin definitions.
*/
interface CategorizingPluginManagerInterface extends PluginManagerInterface {
/**
* Gets the names of all categories.
*
* @return string[]
* An array of translated categories, sorted alphabetically.
*/
public function getCategories();
/**
* Gets sorted plugin definitions.
*
* @param array[]|null $definitions
* (optional) The plugin definitions to sort. If omitted, all plugin
* definitions are used.
*
* @return array[]
* An array of plugin definitions, sorted by category and label.
*/
public function getSortedDefinitions(array $definitions = NULL);
/**
* Gets sorted plugin definitions grouped by category.
*
* In addition to grouping, both categories and its entries are sorted,
* whereas plugin definitions are sorted by label.
*
* @param array[]|null $definitions
* (optional) The plugin definitions to group. If omitted, all plugin
* definitions are used.
*
* @return array[]
* Keys are category names, and values are arrays of which the keys are
* plugin IDs and the values are plugin definitions.
*/
public function getGroupedDefinitions(array $definitions = NULL);
}
......@@ -7,8 +7,10 @@
namespace Drupal\Core\Action;
use Drupal\Component\Plugin\CategorizingPluginManagerInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\CategorizingPluginManagerTrait;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
......@@ -19,7 +21,9 @@
* @see \Drupal\Core\Action\ActionBase
* @see plugin_api
*/
class ActionManager extends DefaultPluginManager {
class ActionManager extends DefaultPluginManager implements CategorizingPluginManagerInterface {
use CategorizingPluginManagerTrait;
/**
* Constructs a new class instance.
......
......@@ -59,4 +59,13 @@ class Action extends Plugin {
*/
public $type = '';
/**
* The category under which the action should be listed in the UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $category;
}
......@@ -10,9 +10,9 @@
use Drupal\Component\Plugin\FallbackPluginManagerInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\CategorizingPluginManagerTrait;
use Drupal\Core\Plugin\Context\ContextAwarePluginManagerTrait;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Manages discovery and instantiation of block plugins.
......@@ -23,16 +23,12 @@
*/
class BlockManager extends DefaultPluginManager implements BlockManagerInterface, FallbackPluginManagerInterface {
use StringTranslationTrait;
use CategorizingPluginManagerTrait {
getSortedDefinitions as traitGetSortedDefinitions;
getGroupedDefinitions as traitGetGroupedDefinitions;
}
use ContextAwarePluginManagerTrait;
/**
* An array of all available modules and their data.
*
* @var array
*/
protected $moduleData;
/**
* Constructs a new \Drupal\Core\Block\BlockManager object.
*
......@@ -56,61 +52,27 @@ public function __construct(\Traversable $namespaces, CacheBackendInterface $cac
*/
public function processDefinition(&$definition, $plugin_id) {
parent::processDefinition($definition, $plugin_id);
// Ensure that every block has a category.
if (empty($definition['category'])) {
$definition['category'] = $this->getModuleName($definition['provider']);
}
}
/**
* 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;
$this->processDefinitionCategory($definition);
}
/**
* {@inheritdoc}
*/
public function getCategories() {
$categories = array_unique(array_values(array_map(function ($definition) {
return $definition['category'];
}, $this->getDefinitions())));
natcasesort($categories);
return $categories;
public function getSortedDefinitions(array $definitions = NULL) {
// Sort the plugins first by category, then by label.
$definitions = $this->traitGetSortedDefinitions($definitions, 'admin_label');
// Do not display the 'broken' plugin in the UI.
unset($definitions['broken']);
return $definitions;
}
/**
* {@inheritdoc}
*/
public function getSortedDefinitions() {
// Sort the plugins first by category, then by label.
$definitions = $this->getDefinitionsForContexts();
uasort($definitions, function ($a, $b) {
if ($a['category'] != $b['category']) {
return strnatcasecmp($a['category'], $b['category']);
}
return strnatcasecmp($a['admin_label'], $b['admin_label']);
});
public function getGroupedDefinitions(array $definitions = NULL) {
$definitions = $this->traitGetGroupedDefinitions($definitions, 'admin_label');
// Do not display the 'broken' plugin in the UI.
unset($definitions['broken']);
unset($definitions[$this->t('Block')]['broken']);
return $definitions;
}
......
......@@ -7,27 +7,12 @@
namespace Drupal\Core\Block;
use Drupal\Component\Plugin\CategorizingPluginManagerInterface;
use Drupal\Core\Plugin\Context\ContextAwarePluginManagerInterface;
/**
* Provides an interface for the discovery and instantiation of block plugins.
*/
interface BlockManagerInterface extends ContextAwarePluginManagerInterface {
/**
* Gets the names of all block categories.
*
* @return array
* An array of translated categories, sorted alphabetically.
*/
public function getCategories();
/**
* Gets the sorted definitions.
*
* @return array
* An array of plugin definitions, sorted by category and admin label.
*/
public function getSortedDefinitions();
interface BlockManagerInterface extends ContextAwarePluginManagerInterface, CategorizingPluginManagerInterface {
}
......@@ -59,4 +59,13 @@ class Condition extends Plugin {
*/
public $condition = array();
/**
* The category under which the condition should listed in the UI.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $category;
}
......@@ -7,10 +7,12 @@
namespace Drupal\Core\Condition;
use Drupal\Component\Plugin\CategorizingPluginManagerInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Executable\ExecutableManagerInterface;
use Drupal\Core\Executable\ExecutableInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\CategorizingPluginManagerTrait;
use Drupal\Core\Plugin\Context\ContextAwarePluginManagerTrait;
use Drupal\Core\Plugin\DefaultPluginManager;
......@@ -23,8 +25,9 @@
*
* @ingroup plugin_api
*/
class ConditionManager extends DefaultPluginManager implements ExecutableManagerInterface {
class ConditionManager extends DefaultPluginManager implements ExecutableManagerInterface, CategorizingPluginManagerInterface {
use CategorizingPluginManagerTrait;
use ContextAwarePluginManagerTrait;
/**
......
<?php
/**
* @file
* Contains \Drupal\Core\Plugin\CategorizingPluginManagerTrait.
*/
namespace Drupal\Core\Plugin;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Provides a trait for the CategorizingPluginManagerInterface.
*
* The trait provides methods for categorizing plugin definitions based on a
* 'category' key. The plugin manager should make sure there is a default
* category. For that the trait's processDefinitionCategory() method can be
* invoked from the processDefinition() method.
*
* @see \Drupal\Component\Plugin\CategorizingPluginManagerInterface
*/
trait CategorizingPluginManagerTrait {
use StringTranslationTrait;
/**
* Processes a plugin 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 plugin has a category.
if (empty($definition['category'])) {
// Default to the human readable module name if the provider is a module;
// otherwise the provider machine name is used.
$definition['category'] = $this->getProviderName($definition['provider']);
}
}
/**
* Gets the name of a provider.
*
* @param string $provider
* The machine name of a plugin provider.
*
* @return string
* The human-readable module name if it exists, otherwise the
* machine-readable name passed.
*/
protected function getProviderName($provider) {
$list = $this->getModuleHandler()->getModuleList();
// If the module exists, return its human-readable name.
if (isset($list[$provider])) {
return $this->getModuleHandler()->getName($provider);
}
// Otherwise, return the machine name.
return $provider;
}
/**
* Returns the module handler used.
*
* @return \Drupal\Core\Extension\ModuleHandlerInterface
* The module handler.
*/
public function getModuleHandler() {
// 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();
}
/**
* Implements \Drupal\Component\Plugin\CategorizingPluginManagerInterface::getCategories().
*/
public function getCategories() {
/** @var \Drupal\Core\Plugin\CategorizingPluginManagerTrait|\Drupal\Component\Plugin\PluginManagerInterface $this */
// Fetch all categories from definitions and remove duplicates.
$categories = array_unique(array_values(array_map(function ($definition) {
return $definition['category'];
}, $this->getDefinitions())));
natcasesort($categories);
return $categories;
}
/**
* Implements \Drupal\Component\Plugin\CategorizingPluginManagerInterface::getSortedDefinitions().
*/
public function getSortedDefinitions(array $definitions = NULL, $label_key = 'label') {
// Sort the plugins first by category, then by label.
/** @var \Drupal\Core\Plugin\CategorizingPluginManagerTrait|\Drupal\Component\Plugin\PluginManagerInterface $this */
$definitions = isset($definitions) ? $definitions : $this->getDefinitions();
uasort($definitions, function ($a, $b) use ($label_key) {
if ($a['category'] != $b['category']) {
return strnatcasecmp($a['category'], $b['category']);
}
return strnatcasecmp($a[$label_key], $b[$label_key]);
});
return $definitions;
}
/**
* Implements \Drupal\Component\Plugin\CategorizingPluginManagerInterface::getGroupedDefinitions().
*/
public function getGroupedDefinitions(array $definitions = NULL, $label_key = 'label') {
/** @var \Drupal\Core\Plugin\CategorizingPluginManagerTrait|\Drupal\Component\Plugin\PluginManagerInterface $this */
$definitions = $this->getSortedDefinitions(isset($definitions) ? $definitions : $this->getDefinitions(), $label_key);
$grouped_definitions = array();
foreach ($definitions as $id => $definition) {
$grouped_definitions[(string) $definition['category']][$id] = $definition;
}
return $grouped_definitions;
}
}
......@@ -332,9 +332,10 @@ public function buildForm(array $form, FormStateInterface $form_state) {
$form['place_blocks']['list']['#type'] = 'container';
$form['place_blocks']['list']['#attributes']['class'][] = 'entity-meta';
// Sort the plugins first by category, then by label.
$plugins = $this->blockManager->getSortedDefinitions();
foreach ($plugins as $plugin_id => $plugin_definition) {
// Only add blocks which work without any available context.
$definitions = $this->blockManager->getDefinitionsForContexts();
$sorted_definitions = $this->blockManager->getSortedDefinitions($definitions);
foreach ($sorted_definitions as $plugin_id => $plugin_definition) {
$category = String::checkPlain($plugin_definition['category']);
$category_key = 'category-' . $category;
if (!isset($form['place_blocks']['list'][$category_key])) {
......
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Plugin\CategorizingPluginManagerTraitTest.
*/
namespace Drupal\Tests\Core\Plugin;
use Drupal\Component\Plugin\CategorizingPluginManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\CategorizingPluginManagerTrait;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Core\Plugin\CategorizingPluginManagerTrait
* @group Plugin
*/
class CategorizingPluginManagerTraitTest extends UnitTestCase {
/**
* The plugin manager to test.
*
* @var \Drupal\Component\Plugin\CategorizingPluginManagerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $pluginManager;
/**
* {@inheritdoc}
*/
protected function setUp() {
$module_handler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
$module_handler->expects($this->any())
->method('getModuleList')
->willReturn(['node' => []]);
$module_handler->expects($this->any())
->method('getName')
->with('node')
->willReturn('Node');
$this->pluginManager = new CategorizingPluginManager($module_handler);
$this->pluginManager->setStringTranslation($this->getStringTranslationStub());
}
/**
* @covers ::getCategories
*/
public function testGetCategories() {
$this->assertSame(array_values($this->pluginManager->getCategories()), [
'fruits',
'vegetables',
]);
}
/**
* @covers ::getSortedDefinitions
*/
public function testGetSortedDefinitions() {
$sorted = $this->pluginManager->getSortedDefinitions();
$this->assertSame(array_keys($sorted), ['apple', 'mango', 'cucumber']);
}
/**
* @covers ::getGroupedDefinitions
*/
public function testGetGroupedDefinitions() {
$grouped = $this->pluginManager->getGroupedDefinitions();
$this->assertSame(array_keys($grouped), ['fruits', 'vegetables']);
$this->assertSame(array_keys($grouped['fruits']), ['apple', 'mango']);
$this->assertSame(array_keys($grouped['vegetables']), ['cucumber']);
}
/**
* @covers ::processDefinitionCategory
*/
public function testProcessDefinitionCategory() {
// Existing category.
$definition = [
'label' => 'some',
'provider' => 'core',
'category' => 'bag',
];
$this->pluginManager->processDefinition($definition, 'some');
$this->assertSame($definition['category'], 'bag');
// No category, provider without label.
$definition = [
'label' => 'some',
'provider' => 'core',
];
$this->pluginManager->processDefinition($definition, 'some');
$this->assertSame($definition['category'], 'core');
// No category, provider is module with label.
$definition = [
'label' => 'some',
'provider' => 'node',
];
$this->pluginManager->processDefinition($definition, 'some');
$this->assertSame($definition['category'], 'Node');
}
}
/**
* Class that allows testing the trait.
*/
class CategorizingPluginManager extends DefaultPluginManager implements CategorizingPluginManagerInterface {
use CategorizingPluginManagerTrait;
/**
* Replace the constructor so we can instantiate a stub.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject $module_handler
* The module handler.
*/
public function __construct(ModuleHandlerInterface $module_handler) {
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*
* Provides some test definitions to the trait.
*/
public function getDefinitions() {
return [
'cucumber' => [
'label' => 'cucumber',
'category' => 'vegetables',
],
'apple' => [
'label' => 'apple',
'category' => 'fruits',
],
'mango' => [
'label' => 'mango',
'category' => 'fruits',
],
];
}
/**
* {@inheritdoc}
*/
public function processDefinition(&$definition, $plugin_id) {
parent::processDefinition($definition, $plugin_id);
$this->processDefinitionCategory($definition);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment