Issue #2949177 by tim.plunkett, samuel.mortenson, mark_fullmer, tedbow, EclipseGc, andypost: Introduce a service that returns filtered plugin definitions

merge-requests/1654/head
Lee Rowlands 2018-04-30 19:18:43 +10:00
parent 223216cb94
commit e91d969537
No known key found for this signature in database
GPG Key ID: 2B829A3DF9204DC4
19 changed files with 425 additions and 17 deletions

View File

@ -33,6 +33,8 @@ interface DiscoveryInterface {
* @return mixed[]
* An array of plugin definitions (empty array if no definitions were
* found). Keys are plugin IDs.
*
* @see \Drupal\Core\Plugin\FilteredPluginManagerInterface::getFilteredDefinitions()
*/
public function getDefinitions();

View File

@ -6,8 +6,8 @@ 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\Plugin\FilteredPluginManagerTrait;
/**
* Manages discovery and instantiation of block plugins.
@ -21,7 +21,7 @@ class BlockManager extends DefaultPluginManager implements BlockManagerInterface
use CategorizingPluginManagerTrait {
getSortedDefinitions as traitGetSortedDefinitions;
}
use ContextAwarePluginManagerTrait;
use FilteredPluginManagerTrait;
/**
* Constructs a new \Drupal\Core\Block\BlockManager object.
@ -37,10 +37,17 @@ class BlockManager extends DefaultPluginManager implements BlockManagerInterface
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/Block', $namespaces, $module_handler, 'Drupal\Core\Block\BlockPluginInterface', 'Drupal\Core\Block\Annotation\Block');
$this->alterInfo('block');
$this->alterInfo($this->getType());
$this->setCacheBackend($cache_backend, 'block_plugins');
}
/**
* {@inheritdoc}
*/
protected function getType() {
return 'block';
}
/**
* {@inheritdoc}
*/

View File

@ -4,10 +4,11 @@ namespace Drupal\Core\Block;
use Drupal\Component\Plugin\CategorizingPluginManagerInterface;
use Drupal\Core\Plugin\Context\ContextAwarePluginManagerInterface;
use Drupal\Core\Plugin\FilteredPluginManagerInterface;
/**
* Provides an interface for the discovery and instantiation of block plugins.
*/
interface BlockManagerInterface extends ContextAwarePluginManagerInterface, CategorizingPluginManagerInterface {
interface BlockManagerInterface extends ContextAwarePluginManagerInterface, CategorizingPluginManagerInterface, FilteredPluginManagerInterface {
}

View File

@ -8,8 +8,9 @@ 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;
use Drupal\Core\Plugin\FilteredPluginManagerInterface;
use Drupal\Core\Plugin\FilteredPluginManagerTrait;
/**
* A plugin manager for condition plugins.
@ -20,10 +21,10 @@ use Drupal\Core\Plugin\DefaultPluginManager;
*
* @ingroup plugin_api
*/
class ConditionManager extends DefaultPluginManager implements ExecutableManagerInterface, CategorizingPluginManagerInterface {
class ConditionManager extends DefaultPluginManager implements ExecutableManagerInterface, CategorizingPluginManagerInterface, FilteredPluginManagerInterface {
use CategorizingPluginManagerTrait;
use ContextAwarePluginManagerTrait;
use FilteredPluginManagerTrait;
/**
* Constructs a ConditionManager object.
@ -43,6 +44,13 @@ class ConditionManager extends DefaultPluginManager implements ExecutableManager
parent::__construct('Plugin/Condition', $namespaces, $module_handler, 'Drupal\Core\Condition\ConditionInterface', 'Drupal\Core\Condition\Annotation\Condition');
}
/**
* {@inheritdoc}
*/
protected function getType() {
return 'condition';
}
/**
* {@inheritdoc}
*/

View File

@ -12,12 +12,15 @@ use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
use Drupal\Core\Plugin\Discovery\YamlDiscoveryDecorator;
use Drupal\Core\Layout\Annotation\Layout;
use Drupal\Core\Plugin\FilteredPluginManagerTrait;
/**
* Provides a plugin manager for layouts.
*/
class LayoutPluginManager extends DefaultPluginManager implements LayoutPluginManagerInterface {
use FilteredPluginManagerTrait;
/**
* The theme handler.
*
@ -42,8 +45,16 @@ class LayoutPluginManager extends DefaultPluginManager implements LayoutPluginMa
parent::__construct('Plugin/Layout', $namespaces, $module_handler, LayoutInterface::class, Layout::class);
$this->themeHandler = $theme_handler;
$this->setCacheBackend($cache_backend, 'layout');
$this->alterInfo('layout');
$type = $this->getType();
$this->setCacheBackend($cache_backend, $type);
$this->alterInfo($type);
}
/**
* {@inheritdoc}
*/
protected function getType() {
return 'layout';
}
/**

View File

@ -3,11 +3,12 @@
namespace Drupal\Core\Layout;
use Drupal\Component\Plugin\CategorizingPluginManagerInterface;
use Drupal\Core\Plugin\FilteredPluginManagerInterface;
/**
* Provides the interface for a plugin manager of layouts.
*/
interface LayoutPluginManagerInterface extends CategorizingPluginManagerInterface {
interface LayoutPluginManagerInterface extends CategorizingPluginManagerInterface, FilteredPluginManagerInterface {
/**
* Gets theme implementations for layouts.

View File

@ -20,6 +20,8 @@ interface ContextAwarePluginManagerInterface extends PluginManagerInterface {
*
* @return array
* An array of plugin definitions.
*
* @see \Drupal\Core\Plugin\FilteredPluginManagerInterface::getFilteredDefinitions()
*/
public function getDefinitionsForContexts(array $contexts = []);

View File

@ -17,7 +17,9 @@ class ContextHandler implements ContextHandlerInterface {
public function filterPluginDefinitionsByContexts(array $contexts, array $definitions) {
return array_filter($definitions, function ($plugin_definition) use ($contexts) {
// If this plugin doesn't need any context, it is available to use.
if (!isset($plugin_definition['context'])) {
// @todo Support object-based plugin definitions in
// https://www.drupal.org/project/drupal/issues/2961822.
if (!is_array($plugin_definition) || !isset($plugin_definition['context'])) {
return TRUE;
}

View File

@ -0,0 +1,36 @@
<?php
namespace Drupal\Core\Plugin;
use Drupal\Component\Plugin\PluginManagerInterface;
/**
* Provides an interface for plugin managers that allow filtering definitions.
*/
interface FilteredPluginManagerInterface extends PluginManagerInterface {
/**
* Gets the plugin definitions for a given type and consumer and filters them.
*
* This allows modules and themes to alter plugin definitions at runtime,
* which is useful for tasks like hiding specific plugins from a particular
* user interface.
*
* @param string $consumer
* A string identifying the consumer of these plugin definitions.
* @param \Drupal\Component\Plugin\Context\ContextInterface[]|null $contexts
* (optional) Either an array of contexts to use for filtering, or NULL to
* not filter by contexts.
* @param mixed[] $extra
* (optional) An associative array containing additional information
* provided by the code requesting the filtered definitions.
*
* @return \Drupal\Component\Plugin\Definition\PluginDefinitionInterface[]|array[]
* An array of plugin definitions that are filtered.
*
* @see hook_plugin_filter_TYPE_alter()
* @see hook_plugin_filter_TYPE__CONSUMER_alter()
*/
public function getFilteredDefinitions($consumer, $contexts = NULL, array $extra = []);
}

View File

@ -0,0 +1,75 @@
<?php
namespace Drupal\Core\Plugin;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\Context\ContextAwarePluginManagerTrait;
use Drupal\Core\Theme\ThemeManagerInterface;
/**
* Provides a trait for plugin managers that allow filtering plugin definitions.
*/
trait FilteredPluginManagerTrait {
use ContextAwarePluginManagerTrait;
/**
* Implements \Drupal\Core\Plugin\FilteredPluginManagerInterface::getFilteredDefinitions().
*/
public function getFilteredDefinitions($consumer, $contexts = NULL, array $extra = []) {
if (!is_null($contexts)) {
$definitions = $this->getDefinitionsForContexts($contexts);
}
else {
$definitions = $this->getDefinitions();
}
$type = $this->getType();
$hooks = [];
$hooks[] = "plugin_filter_{$type}";
$hooks[] = "plugin_filter_{$type}__{$consumer}";
$this->moduleHandler()->alter($hooks, $definitions, $extra, $consumer);
$this->themeManager()->alter($hooks, $definitions, $extra, $consumer);
return $definitions;
}
/**
* A string identifying the plugin type.
*
* This string should be unique and generally will correspond to the string
* used by the discovery, e.g. the annotation class or the YAML file name.
*
* @return string
* A string identifying the plugin type.
*/
abstract protected function getType();
/**
* Wraps the module handler.
*
* @return \Drupal\Core\Extension\ModuleHandlerInterface
* The module handler.
*/
protected function moduleHandler() {
if (property_exists($this, 'moduleHandler') && $this->moduleHandler instanceof ModuleHandlerInterface) {
return $this->moduleHandler;
}
return \Drupal::service('module_handler');
}
/**
* Wraps the theme manager.
*
* @return \Drupal\Core\Theme\ThemeManagerInterface
* The theme manager.
*/
protected function themeManager() {
if (property_exists($this, 'themeManager') && $this->themeManager instanceof ThemeManagerInterface) {
return $this->themeManager;
}
return \Drupal::service('theme.manager');
}
}

View File

@ -0,0 +1,68 @@
<?php
/**
* @file
* Hooks provided by the Plugin system.
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Alter the filtering of plugin definitions for a specific plugin type.
*
* TYPE (e.g. "block", "layout") limits hook scope to a plugin type.
* For example, HOOK_plugin_filter_block_alter() would be invoked
* by a hook listener which specifies the 'block' plugin list,
* e.g., BlockLibraryController or ChooseBlockController.
*
* @param \Drupal\Component\Plugin\Definition\PluginDefinitionInterface[]|array[] $definitions
* The array of plugin definitions.
* @param mixed[] $extra
* An associative array containing additional information provided by the code
* requesting the filtered definitions.
* @param string $consumer
* A string identifying the consumer of these plugin definitions.
*/
function hook_plugin_filter_TYPE_alter(array &$definitions, array $extra, $consumer) {
// Remove the "Help" block from the Block UI list.
if ($consumer == 'block_ui') {
unset($definitions['help_block']);
}
// If the theme is specified, remove the branding block from the Bartik theme.
if (isset($extra['theme']) && $extra['theme'] === 'bartik') {
unset($definitions['system_branding_block']);
}
// Remove the "Main page content" block from everywhere.
unset($definitions['system_main_block']);
}
/**
* Alter the filtering of plugin definitions for a specific type and consumer.
*
* TYPE (e.g. "block", "layout") limits hook scope to a plugin type.
* CONSUMER (e.g., "block_ui", "layout_builder") limits hook scope to one or
* more listeners, typically provided the same module. For example,
* HOOK_plugin_filter_layout__layout_builder_alter() would affect
* Layout Builder's listeners for the 'layout' plugin type (see
* ChooseSectionController), while HOOK_plugin_filter_block__block_ui_alter()
* would affect the Block UI's listeners for the 'block' plugin type.
*
* @param \Drupal\Component\Plugin\Definition\PluginDefinitionInterface[]|array[] $definitions
* The array of plugin definitions.
* @param mixed[] $extra
* An associative array containing additional information provided by the code
* requesting the filtered definitions.
*/
function hook_plugin_filter_TYPE__CONSUMER_alter(array &$definitions, array $extra) {
// Explicitly remove the "Help" block for this consumer.
unset($definitions['help_block']);
}
/**
* @} End of "addtogroup hooks".
*/

View File

@ -239,7 +239,8 @@ class BlockForm extends EntityForm {
// @todo Allow list of conditions to be configured in
// https://www.drupal.org/node/2284687.
$visibility = $this->entity->getVisibility();
foreach ($this->manager->getDefinitionsForContexts($form_state->getTemporaryValue('gathered_contexts')) as $condition_id => $definition) {
$definitions = $this->manager->getFilteredDefinitions('block_ui', $form_state->getTemporaryValue('gathered_contexts'), ['block' => $this->entity]);
foreach ($definitions as $condition_id => $definition) {
// Don't display the current theme condition.
if ($condition_id == 'current_theme') {
continue;

View File

@ -101,8 +101,14 @@ class BlockLibraryController extends ControllerBase {
['data' => $this->t('Operations')],
];
$region = $request->query->get('region');
$weight = $request->query->get('weight');
// Only add blocks which work without any available context.
$definitions = $this->blockManager->getDefinitionsForContexts($this->contextRepository->getAvailableContexts());
$definitions = $this->blockManager->getFilteredDefinitions('block_ui', $this->contextRepository->getAvailableContexts(), [
'theme' => $theme,
'region' => $region,
]);
// Order by category, and then by admin label.
$definitions = $this->blockManager->getSortedDefinitions($definitions);
// Filter out definitions that are not intended to be placed by the UI.
@ -110,8 +116,6 @@ class BlockLibraryController extends ControllerBase {
return empty($definition['_block_ui_hidden']);
});
$region = $request->query->get('region');
$weight = $request->query->get('weight');
$rows = [];
foreach ($definitions as $plugin_id => $plugin_definition) {
$row = [];

View File

@ -63,7 +63,10 @@ class ChooseBlockController implements ContainerInjectionInterface {
$build['#type'] = 'container';
$build['#attributes']['class'][] = 'block-categories';
$definitions = $this->blockManager->getDefinitionsForContexts($this->getAvailableContexts($section_storage));
$definitions = $this->blockManager->getFilteredDefinitions('layout_builder', $this->getAvailableContexts($section_storage), [
'section_storage' => $section_storage,
'region' => $region,
]);
foreach ($this->blockManager->getGroupedDefinitions($definitions) as $category => $blocks) {
$build[$category]['#type'] = 'details';
$build[$category]['#open'] = TRUE;

View File

@ -62,7 +62,8 @@ class ChooseSectionController implements ContainerInjectionInterface {
$output['#title'] = $this->t('Choose a layout');
$items = [];
foreach ($this->layoutManager->getDefinitions() as $plugin_id => $definition) {
$definitions = $this->layoutManager->getFilteredDefinitions('layout_builder', [], ['section_storage' => $section_storage]);
foreach ($definitions as $plugin_id => $definition) {
$layout = $this->layoutManager->createInstance($plugin_id);
$item = [
'#type' => 'link',

View File

@ -0,0 +1,6 @@
name: 'Layout Builder test'
type: module
description: 'Support module for testing layout building.'
package: Testing
version: VERSION
core: 8.x

View File

@ -0,0 +1,29 @@
<?php
/**
* @file
* Provides hook implementations for Layout Builder tests.
*/
/**
* Implements hook_plugin_filter_TYPE__CONSUMER_alter().
*/
function layout_builder_test_plugin_filter_block__layout_builder_alter(array &$definitions) {
// Explicitly remove the "Help" blocks from the list.
unset($definitions['help_block']);
// Explicitly remove the "Sticky at top of lists field_block".
$disallowed_fields = [
'sticky',
];
foreach ($definitions as $plugin_id => $definition) {
// Field block IDs are in the form 'field_block:{entity}:{bundle}:{name}',
// for example 'field_block:node:article:revision_timestamp'.
preg_match('/field_block:.*:.*:(.*)/', $plugin_id, $parts);
if (isset($parts[1]) && in_array($parts[1], $disallowed_fields, TRUE)) {
// Unset any field blocks that match our predefined list.
unset($definitions[$plugin_id]);
}
}
}

View File

@ -19,6 +19,7 @@ class LayoutBuilderTest extends BrowserTestBase {
'layout_test',
'block',
'node',
'layout_builder_test',
];
/**
@ -319,4 +320,33 @@ class LayoutBuilderTest extends BrowserTestBase {
$this->clickLink('Cancel Layout');
}
/**
* {@inheritdoc}
*/
public function testLayoutBuilderChooseBlocksAlter() {
// See layout_builder_test_plugin_filter_block__layout_builder_alter().
$assert_session = $this->assertSession();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
'administer node fields',
]));
// From the manage display page, go to manage the layout.
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default');
$this->clickLink('Manage layout');
// Add a new block.
$this->clickLink('Add Block');
// Verify that blocks not modified are present.
$assert_session->linkExists('Powered by Drupal');
$assert_session->linkExists('Default revision');
// Verify that blocks explicitly removed are not present.
$assert_session->linkNotExists('Help');
$assert_session->linkNotExists('Sticky at top of lists');
}
}

View File

@ -0,0 +1,121 @@
<?php
namespace Drupal\Tests\Core\Plugin;
use Drupal\Component\Plugin\PluginManagerBase;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\Context\ContextHandlerInterface;
use Drupal\Core\Plugin\FilteredPluginManagerInterface;
use Drupal\Core\Plugin\FilteredPluginManagerTrait;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\Core\Plugin\FilteredPluginManagerTrait
* @group Plugin
*/
class FilteredPluginManagerTraitTest extends UnitTestCase {
/**
* @covers ::getFilteredDefinitions
* @dataProvider providerTestGetFilteredDefinitions
*/
public function testGetFilteredDefinitions($contexts, $expected) {
// Start with two plugins.
$definitions = [];
$definitions['plugin1'] = ['id' => 'plugin1'];
$definitions['plugin2'] = ['id' => 'plugin2'];
$type = 'the_type';
$consumer = 'the_consumer';
$extra = ['foo' => 'bar'];
$context_handler = $this->prophesize(ContextHandlerInterface::class);
// Remove the second plugin when context1 is provided.
$context_handler->filterPluginDefinitionsByContexts(['context1' => 'fake context'], $definitions)
->willReturn(['plugin1' => $definitions['plugin1']]);
// Remove the first plugin when no contexts are provided.
$context_handler->filterPluginDefinitionsByContexts([], $definitions)
->willReturn(['plugin2' => $definitions['plugin2']]);
// After context filtering, the alter hook will be invoked.
$module_handler = $this->prophesize(ModuleHandlerInterface::class);
$hooks = ["plugin_filter_{$type}", "plugin_filter_{$type}__{$consumer}"];
$module_handler->alter($hooks, $expected, $extra, $consumer)->shouldBeCalled();
$theme_manager = $this->prophesize(ThemeManagerInterface::class);
$theme_manager->alter($hooks, $expected, $extra, $consumer)->shouldBeCalled();
$plugin_manager = new TestFilteredPluginManager($definitions, $module_handler->reveal(), $theme_manager->reveal(), $context_handler->reveal());
$result = $plugin_manager->getFilteredDefinitions($consumer, $contexts, $extra);
$this->assertSame($expected, $result);
}
/**
* Provides test data for ::testGetFilteredDefinitions().
*/
public function providerTestGetFilteredDefinitions() {
$data = [];
$data['populated context'] = [
['context1' => 'fake context'],
['plugin1' => ['id' => 'plugin1']],
];
$data['empty context'] = [
[],
['plugin2' => ['id' => 'plugin2']],
];
$data['null context'] = [
NULL,
[
'plugin1' => ['id' => 'plugin1'],
'plugin2' => ['id' => 'plugin2'],
],
];
return $data;
}
}
/**
* Class that allows testing the trait.
*/
class TestFilteredPluginManager extends PluginManagerBase implements FilteredPluginManagerInterface {
use FilteredPluginManagerTrait;
protected $definitions = [];
protected $moduleHandler;
protected $themeManager;
protected $contextHandler;
public function __construct(array $definitions, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager, ContextHandlerInterface $context_handler) {
$this->definitions = $definitions;
$this->moduleHandler = $module_handler;
$this->themeManager = $theme_manager;
$this->contextHandler = $context_handler;
}
protected function contextHandler() {
return $this->contextHandler;
}
protected function moduleHandler() {
return $this->moduleHandler;
}
protected function themeManager() {
return $this->themeManager;
}
protected function getType() {
return 'the_type';
}
public function getDefinitions() {
return $this->definitions;
}
}