Commit a470bc0c authored by webchick's avatar webchick

Issue #2773197 by jalpesh, tstoeckler:...

Issue #2773197 by jalpesh, tstoeckler: DefaultHtmlRouteProviderTest::testGetCollectionRoute() has wrong @covers
parent c4cb804d
...@@ -577,6 +577,9 @@ services: ...@@ -577,6 +577,9 @@ services:
entity.autocomplete_matcher: entity.autocomplete_matcher:
class: Drupal\Core\Entity\EntityAutocompleteMatcher class: Drupal\Core\Entity\EntityAutocompleteMatcher
arguments: ['@plugin.manager.entity_reference_selection'] arguments: ['@plugin.manager.entity_reference_selection']
plugin_form.factory:
class: Drupal\Core\Plugin\PluginFormFactory
arguments: ['@class_resolver']
plugin.manager.entity_reference_selection: plugin.manager.entity_reference_selection:
class: Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManager class: Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManager
parent: default_plugin_manager parent: default_plugin_manager
......
<?php
namespace Drupal\Component\Plugin;
/**
* Provides an interface for objects that depend on a plugin.
*/
interface PluginAwareInterface {
/**
* Sets the plugin for this object.
*
* @param \Drupal\Component\Plugin\PluginInspectionInterface $plugin
* The plugin.
*/
public function setPlugin(PluginInspectionInterface $plugin);
}
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\NestedArray; use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Plugin\PluginWithFormsInterface;
use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AccountInterface;
use Drupal\Component\Transliteration\TransliterationInterface; use Drupal\Component\Transliteration\TransliterationInterface;
...@@ -22,7 +23,7 @@ ...@@ -22,7 +23,7 @@
* *
* @ingroup block_api * @ingroup block_api
*/ */
abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginInterface { abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginInterface, PluginWithFormsInterface {
use ContextAwarePluginAssignmentTrait; use ContextAwarePluginAssignmentTrait;
...@@ -271,4 +272,20 @@ public function setTransliteration(TransliterationInterface $transliteration) { ...@@ -271,4 +272,20 @@ public function setTransliteration(TransliterationInterface $transliteration) {
$this->transliteration = $transliteration; $this->transliteration = $transliteration;
} }
/**
* {@inheritdoc}
*/
public function getFormClass($operation) {
if ($this->hasFormClass($operation)) {
return $this->getPluginDefinition()['forms'][$operation];
}
}
/**
* {@inheritdoc}
*/
public function hasFormClass($operation) {
return isset($this->getPluginDefinition()['forms'][$operation]);
}
} }
...@@ -239,9 +239,20 @@ public function useCaches($use_caches = FALSE) { ...@@ -239,9 +239,20 @@ public function useCaches($use_caches = FALSE) {
* method. * method.
*/ */
public function processDefinition(&$definition, $plugin_id) { public function processDefinition(&$definition, $plugin_id) {
// Only arrays can be operated on.
if (!is_array($definition)) {
return;
}
if (!empty($this->defaults) && is_array($this->defaults)) { if (!empty($this->defaults) && is_array($this->defaults)) {
$definition = NestedArray::mergeDeep($this->defaults, $definition); $definition = NestedArray::mergeDeep($this->defaults, $definition);
} }
// If no default form is defined and this plugin implements
// \Drupal\Core\Plugin\PluginFormInterface, use that for the default form.
if (!isset($definition['forms']['configure']) && isset($definition['class']) && is_subclass_of($definition['class'], PluginFormInterface::class)) {
$definition['forms']['configure'] = $definition['class'];
}
} }
/** /**
......
<?php
namespace Drupal\Core\Plugin;
use Drupal\Component\Plugin\PluginAwareInterface;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a base class for plugin forms.
*
* Classes extending this can be in any namespace, but are commonly placed in
* the 'PluginForm' namespace, such as \Drupal\module_name\PluginForm\ClassName.
*/
abstract class PluginFormBase implements PluginFormInterface, PluginAwareInterface {
/**
* The plugin this form is for.
*
* @var \Drupal\Component\Plugin\PluginInspectionInterface
*/
protected $plugin;
/**
* {@inheritdoc}
*/
public function setPlugin(PluginInspectionInterface $plugin) {
$this->plugin = $plugin;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
// Validation is optional.
}
}
<?php
namespace Drupal\Core\Plugin;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Plugin\PluginAwareInterface;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
/**
* Provides form discovery capabilities for plugins.
*/
class PluginFormFactory implements PluginFormFactoryInterface {
/**
* The class resolver.
*
* @var \Drupal\Core\DependencyInjection\ClassResolverInterface
*/
protected $classResolver;
/**
* PluginFormFactory constructor.
*
* @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
* The class resolver.
*/
public function __construct(ClassResolverInterface $class_resolver) {
$this->classResolver = $class_resolver;
}
/**
* {@inheritdoc}
*/
public function createInstance(PluginWithFormsInterface $plugin, $operation, $fallback_operation = NULL) {
if (!$plugin->hasFormClass($operation)) {
// Use the default form class if no form is specified for this operation.
if ($fallback_operation && $plugin->hasFormClass($fallback_operation)) {
$operation = $fallback_operation;
}
else {
throw new InvalidPluginDefinitionException($plugin->getPluginId(), sprintf('The "%s" plugin did not specify a "%s" form class', $plugin->getPluginId(), $operation));
}
}
$form_class = $plugin->getFormClass($operation);
// If the form specified is the plugin itself, use it directly.
if (ltrim(get_class($plugin), '\\') === ltrim($form_class, '\\')) {
$form_object = $plugin;
}
else {
$form_object = $this->classResolver->getInstanceFromDefinition($form_class);
}
// Ensure the resulting object is a plugin form.
if (!$form_object instanceof PluginFormInterface) {
throw new InvalidPluginDefinitionException($plugin->getPluginId(), sprintf('The "%s" plugin did not specify a valid "%s" form class, must implement \Drupal\Core\Plugin\PluginFormInterface', $plugin->getPluginId(), $operation));
}
if ($form_object instanceof PluginAwareInterface) {
$form_object->setPlugin($plugin);
}
return $form_object;
}
}
<?php
namespace Drupal\Core\Plugin;
/**
* Provides an interface for retrieving form objects for plugins.
*
* This allows a plugin to define multiple forms, in addition to the plugin
* itself providing a form. All forms, decoupled or self-contained, must
* implement \Drupal\Core\Plugin\PluginFormInterface. Decoupled forms can
* implement \Drupal\Component\Plugin\PluginAwareInterface in order to gain
* access to the plugin.
*/
interface PluginFormFactoryInterface {
/**
* Creates a new form instance.
*
* @param \Drupal\Core\Plugin\PluginWithFormsInterface $plugin
* The plugin the form is for.
* @param string $operation
* The name of the operation to use, e.g., 'add' or 'edit'.
* @param string $fallback_operation
* (optional) The name of the fallback operation to use.
*
* @return \Drupal\Core\Plugin\PluginFormInterface
* A plugin form instance.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
*/
public function createInstance(PluginWithFormsInterface $plugin, $operation, $fallback_operation = NULL);
}
...@@ -7,6 +7,10 @@ ...@@ -7,6 +7,10 @@
/** /**
* Provides an interface for an embeddable plugin form. * Provides an interface for an embeddable plugin form.
* *
* Plugins can implement this form directly, or a standalone class can be used.
* Decoupled forms can implement \Drupal\Component\Plugin\PluginAwareInterface
* in order to gain access to the plugin.
*
* @ingroup plugin_api * @ingroup plugin_api
*/ */
interface PluginFormInterface { interface PluginFormInterface {
......
<?php
namespace Drupal\Core\Plugin;
use Drupal\Component\Plugin\PluginInspectionInterface;
/**
* Provides an interface for plugins which have forms.
*
* Plugin forms are embeddable forms referenced by the plugin annotation.
* Used by plugin types which have a larger number of plugin-specific forms.
*/
interface PluginWithFormsInterface extends PluginInspectionInterface {
/**
* Gets the form class for the given operation.
*
* @param string $operation
* The name of the operation.
*
* @return string|null
* The form class if defined, NULL otherwise.
*/
public function getFormClass($operation);
/**
* Gets whether the plugin has a form class for the given operation.
*
* @param string $operation
* The name of the operation.
*
* @return bool
* TRUE if the plugin has a form class for the given operation.
*/
public function hasFormClass($operation);
}
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
namespace Drupal\block; namespace Drupal\block;
use Drupal\Component\Utility\Html; use Drupal\Component\Utility\Html;
use Drupal\Core\Plugin\PluginFormFactoryInterface;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Entity\EntityForm; use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Executable\ExecutableManagerInterface; use Drupal\Core\Executable\ExecutableManagerInterface;
...@@ -68,6 +70,13 @@ class BlockForm extends EntityForm { ...@@ -68,6 +70,13 @@ class BlockForm extends EntityForm {
*/ */
protected $contextRepository; protected $contextRepository;
/**
* The plugin form manager.
*
* @var \Drupal\Core\Plugin\PluginFormFactoryInterface
*/
protected $pluginFormFactory;
/** /**
* Constructs a BlockForm object. * Constructs a BlockForm object.
* *
...@@ -81,13 +90,16 @@ class BlockForm extends EntityForm { ...@@ -81,13 +90,16 @@ class BlockForm extends EntityForm {
* The language manager. * The language manager.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler. * The theme handler.
* @param \Drupal\Core\Plugin\PluginFormFactoryInterface $plugin_form_manager
* The plugin form manager.
*/ */
public function __construct(EntityManagerInterface $entity_manager, ExecutableManagerInterface $manager, ContextRepositoryInterface $context_repository, LanguageManagerInterface $language, ThemeHandlerInterface $theme_handler) { public function __construct(EntityManagerInterface $entity_manager, ExecutableManagerInterface $manager, ContextRepositoryInterface $context_repository, LanguageManagerInterface $language, ThemeHandlerInterface $theme_handler, PluginFormFactoryInterface $plugin_form_manager) {
$this->storage = $entity_manager->getStorage('block'); $this->storage = $entity_manager->getStorage('block');
$this->manager = $manager; $this->manager = $manager;
$this->contextRepository = $context_repository; $this->contextRepository = $context_repository;
$this->language = $language; $this->language = $language;
$this->themeHandler = $theme_handler; $this->themeHandler = $theme_handler;
$this->pluginFormFactory = $plugin_form_manager;
} }
/** /**
...@@ -99,7 +111,8 @@ public static function create(ContainerInterface $container) { ...@@ -99,7 +111,8 @@ public static function create(ContainerInterface $container) {
$container->get('plugin.manager.condition'), $container->get('plugin.manager.condition'),
$container->get('context.repository'), $container->get('context.repository'),
$container->get('language_manager'), $container->get('language_manager'),
$container->get('theme_handler') $container->get('theme_handler'),
$container->get('plugin_form.factory')
); );
} }
...@@ -120,7 +133,7 @@ public function form(array $form, FormStateInterface $form_state) { ...@@ -120,7 +133,7 @@ public function form(array $form, FormStateInterface $form_state) {
$form_state->setTemporaryValue('gathered_contexts', $this->contextRepository->getAvailableContexts()); $form_state->setTemporaryValue('gathered_contexts', $this->contextRepository->getAvailableContexts());
$form['#tree'] = TRUE; $form['#tree'] = TRUE;
$form['settings'] = $entity->getPlugin()->buildConfigurationForm(array(), $form_state); $form['settings'] = $this->getPluginForm($entity->getPlugin())->buildConfigurationForm(array(), $form_state);
$form['visibility'] = $this->buildVisibilityInterface([], $form_state); $form['visibility'] = $this->buildVisibilityInterface([], $form_state);
// If creating a new block, calculate a safe default machine name. // If creating a new block, calculate a safe default machine name.
...@@ -282,7 +295,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) { ...@@ -282,7 +295,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
// settings form element, so just pass that to the block for validation. // settings form element, so just pass that to the block for validation.
$settings = (new FormState())->setValues($form_state->getValue('settings')); $settings = (new FormState())->setValues($form_state->getValue('settings'));
// Call the plugin validate handler. // Call the plugin validate handler.
$this->entity->getPlugin()->validateConfigurationForm($form, $settings); $this->getPluginForm($this->entity->getPlugin())->validateConfigurationForm($form, $settings);
// Update the original form values. // Update the original form values.
$form_state->setValue('settings', $settings->getValues()); $form_state->setValue('settings', $settings->getValues());
$this->validateVisibility($form, $form_state); $this->validateVisibility($form, $form_state);
...@@ -329,8 +342,8 @@ public function submitForm(array &$form, FormStateInterface $form_state) { ...@@ -329,8 +342,8 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
$settings = (new FormState())->setValues($form_state->getValue('settings')); $settings = (new FormState())->setValues($form_state->getValue('settings'));
// Call the plugin submit handler. // Call the plugin submit handler.
$entity->getPlugin()->submitConfigurationForm($form, $settings);
$block = $entity->getPlugin(); $block = $entity->getPlugin();
$this->getPluginForm($block)->submitConfigurationForm($form, $settings);
// If this block is context-aware, set the context mapping. // If this block is context-aware, set the context mapping.
if ($block instanceof ContextAwarePluginInterface && $block->getContextDefinitions()) { if ($block instanceof ContextAwarePluginInterface && $block->getContextDefinitions()) {
$context_mapping = $settings->getValue('context_mapping', []); $context_mapping = $settings->getValue('context_mapping', []);
...@@ -402,4 +415,17 @@ public function getUniqueMachineName(BlockInterface $block) { ...@@ -402,4 +415,17 @@ public function getUniqueMachineName(BlockInterface $block) {
return $machine_default; return $machine_default;
} }
/**
* Retrieves the plugin form for a given block and operation.
*
* @param \Drupal\Core\Block\BlockPluginInterface $block
* The block plugin.
*
* @return \Drupal\Core\Plugin\PluginFormInterface
* The plugin form for the block.
*/
protected function getPluginForm(BlockPluginInterface $block) {
return $this->pluginFormFactory->createInstance($block, 'configure');
}
} }
<?php
namespace Drupal\block_test\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* Provides a block with multiple forms.
*
* @Block(
* id = "test_multiple_forms_block",
* forms = {
* "secondary" = "\Drupal\block_test\PluginForm\EmptyBlockForm"
* },
* admin_label = @Translation("Multiple forms test block")
* )
*/
class TestMultipleFormsBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
return [];
}
}
<?php
namespace Drupal\block_test\PluginForm;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginFormBase;
/**
* Provides a form for a block that is empty.
*/
class EmptyBlockForm extends PluginFormBase {
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
// Intentionally empty.
}
}
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
namespace Drupal\Tests\block\Unit; namespace Drupal\Tests\block\Unit;
use Drupal\block\BlockForm; use Drupal\block\BlockForm;
use Drupal\Core\Plugin\PluginFormFactoryInterface;
use Drupal\Tests\UnitTestCase; use Drupal\Tests\UnitTestCase;
/** /**
...@@ -54,6 +55,13 @@ class BlockFormTest extends UnitTestCase { ...@@ -54,6 +55,13 @@ class BlockFormTest extends UnitTestCase {
*/ */
protected $contextRepository; protected $contextRepository;
/**
* The plugin form manager.
*
* @var \Drupal\Core\Plugin\PluginFormFactoryInterface|\Prophecy\Prophecy\ProphecyInterface
*/
protected $pluginFormFactory;
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
...@@ -71,6 +79,7 @@ protected function setUp() { ...@@ -71,6 +79,7 @@ protected function setUp() {
->method('getStorage') ->method('getStorage')
->will($this->returnValue($this->storage)); ->will($this->returnValue($this->storage));
$this->pluginFormFactory = $this->prophesize(PluginFormFactoryInterface::class);
} }
/** /**
...@@ -99,7 +108,7 @@ public function testGetUniqueMachineName() { ...@@ -99,7 +108,7 @@ public function testGetUniqueMachineName() {
->method('getQuery') ->method('getQuery')
->will($this->returnValue($query)); ->will($this->returnValue($query));
$block_form_controller = new BlockForm($this->entityManager, $this->conditionManager, $this->contextRepository, $this->language, $this->themeHandler); $block_form_controller = new BlockForm($this->entityManager, $this->conditionManager, $this->contextRepository, $this->language, $this->themeHandler, $this->pluginFormFactory->reveal());
// Ensure that the block with just one other instance gets the next available // Ensure that the block with just one other instance gets the next available
// name suggestion. // name suggestion.
......
<?php
namespace Drupal\KernelTests\Core\Block;
use Drupal\block_test\PluginForm\EmptyBlockForm;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests that blocks can have multiple forms.
*
* @group block
*/
class MultipleBlockFormTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['system', 'block', 'block_test'];
/**
* Tests that blocks can have multiple forms.
*/
public function testMultipleForms() {
$configuration = ['label' => 'A very cool block'];
$block = \Drupal::service('plugin.manager.block')->createInstance('test_multiple_forms_block', $configuration);
$form_object1 = \Drupal::service('plugin_form.factory')->createInstance($block, 'configure');
$form_object2 = \Drupal::service('plugin_form.factory')->createInstance($block, 'secondary');
// Assert that the block itself is used for the default form.
$this->assertSame($block, $form_object1);
// Ensure that EmptyBlockForm is used and the plugin is set.
$this->assertInstanceOf(EmptyBlockForm::class, $form_object2);
$this->assertAttributeEquals($block, 'plugin', $form_object2);
}
}
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
namespace Drupal\Tests\Core\Plugin; namespace Drupal\Tests\Core\Plugin;
use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Tests\UnitTestCase; use Drupal\Tests\UnitTestCase;
/** /**
...@@ -338,4 +340,98 @@ public function testGetCacheMaxAge() { ...@@ -338,4 +340,98 @@ public function testGetCacheMaxAge() {
$this->assertInternalType('int', $cache_max_age); $this->assertInternalType('int', $cache_max_age);
} }
/**
* @covers ::processDefinition
* @dataProvider providerTestProcessDefinition
*/
public function testProcessDefinition($definition, $expected) {
$module_handler = $this->prophesize(ModuleHandlerInterface::class);
$plugin_manager = new TestPluginManagerWithDefaults($this->namespaces, $this->expectedDefinitions, $module_handler->reveal(), NULL);
$plugin_manager->processDefinition($definition, 'the_plugin_id');
$this->assertEquals($expected, $definition);