diff --git a/core/core.services.yml b/core/core.services.yml index 0e7eb586e931c3a28364a5b31329943a944455e6..70c6441f91e0d8a7a749a6c01c716aaa698aefbc 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -577,6 +577,9 @@ services: entity.autocomplete_matcher: class: Drupal\Core\Entity\EntityAutocompleteMatcher arguments: ['@plugin.manager.entity_reference_selection'] + plugin_form.factory: + class: Drupal\Core\Plugin\PluginFormFactory + arguments: ['@class_resolver'] plugin.manager.entity_reference_selection: class: Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManager parent: default_plugin_manager diff --git a/core/lib/Drupal/Component/Plugin/PluginAwareInterface.php b/core/lib/Drupal/Component/Plugin/PluginAwareInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..5506c9f7ab916e730e0010fe1ea12763c56f9a42 --- /dev/null +++ b/core/lib/Drupal/Component/Plugin/PluginAwareInterface.php @@ -0,0 +1,18 @@ +<?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); + +} diff --git a/core/lib/Drupal/Core/Block/BlockBase.php b/core/lib/Drupal/Core/Block/BlockBase.php index 03d76acb6635e580db9cb30f9d69d6901d8ea424..bcc3954ae69baa19fd36221ef048ca80512ec128 100644 --- a/core/lib/Drupal/Core/Block/BlockBase.php +++ b/core/lib/Drupal/Core/Block/BlockBase.php @@ -10,6 +10,7 @@ use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Plugin\PluginWithFormsInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Component\Transliteration\TransliterationInterface; @@ -22,7 +23,7 @@ * * @ingroup block_api */ -abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginInterface { +abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginInterface, PluginWithFormsInterface { use ContextAwarePluginAssignmentTrait; @@ -271,4 +272,20 @@ public function setTransliteration(TransliterationInterface $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]); + } + } diff --git a/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php b/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php index 06365fa0da0b96c2773fcb9a33d267a8579ca530..fa706a47adc5b22efc2d868b7e16f69c7bf382e5 100644 --- a/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php +++ b/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php @@ -239,9 +239,20 @@ public function useCaches($use_caches = FALSE) { * method. */ 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)) { $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']; + } } /** diff --git a/core/lib/Drupal/Core/Plugin/PluginFormBase.php b/core/lib/Drupal/Core/Plugin/PluginFormBase.php new file mode 100644 index 0000000000000000000000000000000000000000..d0fe1b4026eb6dfb3e8f2f96126f139cc500ab76 --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/PluginFormBase.php @@ -0,0 +1,38 @@ +<?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. + } + +} diff --git a/core/lib/Drupal/Core/Plugin/PluginFormFactory.php b/core/lib/Drupal/Core/Plugin/PluginFormFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..6cbbc22c763e48a118fed3f7035924ee8928290f --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/PluginFormFactory.php @@ -0,0 +1,67 @@ +<?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; + } + +} diff --git a/core/lib/Drupal/Core/Plugin/PluginFormFactoryInterface.php b/core/lib/Drupal/Core/Plugin/PluginFormFactoryInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..f4dd2882d3f75ba4f7f7e88ce72416bb97ddc76d --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/PluginFormFactoryInterface.php @@ -0,0 +1,33 @@ +<?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); + +} diff --git a/core/lib/Drupal/Core/Plugin/PluginFormInterface.php b/core/lib/Drupal/Core/Plugin/PluginFormInterface.php index 577735f13719de32f950f2422c0afe4ade770ff8..397c98f0863d4f768954db954cd37ad5bc0d9377 100644 --- a/core/lib/Drupal/Core/Plugin/PluginFormInterface.php +++ b/core/lib/Drupal/Core/Plugin/PluginFormInterface.php @@ -7,6 +7,10 @@ /** * 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 */ interface PluginFormInterface { diff --git a/core/lib/Drupal/Core/Plugin/PluginWithFormsInterface.php b/core/lib/Drupal/Core/Plugin/PluginWithFormsInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..aed5adf4d2e5bec9829f1b1be581e8bfe2045d0b --- /dev/null +++ b/core/lib/Drupal/Core/Plugin/PluginWithFormsInterface.php @@ -0,0 +1,37 @@ +<?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); + +} diff --git a/core/modules/block/src/BlockForm.php b/core/modules/block/src/BlockForm.php index eeec4d3338a8ef1e7afd6925b75c00e1fe925c0d..03448b9abc968ade292b0480b8dec03a8456d3f8 100644 --- a/core/modules/block/src/BlockForm.php +++ b/core/modules/block/src/BlockForm.php @@ -3,6 +3,8 @@ namespace Drupal\block; use Drupal\Component\Utility\Html; +use Drupal\Core\Plugin\PluginFormFactoryInterface; +use Drupal\Core\Block\BlockPluginInterface; use Drupal\Core\Entity\EntityForm; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Executable\ExecutableManagerInterface; @@ -68,6 +70,13 @@ class BlockForm extends EntityForm { */ protected $contextRepository; + /** + * The plugin form manager. + * + * @var \Drupal\Core\Plugin\PluginFormFactoryInterface + */ + protected $pluginFormFactory; + /** * Constructs a BlockForm object. * @@ -81,13 +90,16 @@ class BlockForm extends EntityForm { * The language manager. * @param \Drupal\Core\Extension\ThemeHandlerInterface $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->manager = $manager; $this->contextRepository = $context_repository; $this->language = $language; $this->themeHandler = $theme_handler; + $this->pluginFormFactory = $plugin_form_manager; } /** @@ -99,7 +111,8 @@ public static function create(ContainerInterface $container) { $container->get('plugin.manager.condition'), $container->get('context.repository'), $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) { $form_state->setTemporaryValue('gathered_contexts', $this->contextRepository->getAvailableContexts()); $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); // If creating a new block, calculate a safe default machine name. @@ -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 = (new FormState())->setValues($form_state->getValue('settings')); // Call the plugin validate handler. - $this->entity->getPlugin()->validateConfigurationForm($form, $settings); + $this->getPluginForm($this->entity->getPlugin())->validateConfigurationForm($form, $settings); // Update the original form values. $form_state->setValue('settings', $settings->getValues()); $this->validateVisibility($form, $form_state); @@ -329,8 +342,8 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $settings = (new FormState())->setValues($form_state->getValue('settings')); // Call the plugin submit handler. - $entity->getPlugin()->submitConfigurationForm($form, $settings); $block = $entity->getPlugin(); + $this->getPluginForm($block)->submitConfigurationForm($form, $settings); // If this block is context-aware, set the context mapping. if ($block instanceof ContextAwarePluginInterface && $block->getContextDefinitions()) { $context_mapping = $settings->getValue('context_mapping', []); @@ -402,4 +415,17 @@ public function getUniqueMachineName(BlockInterface $block) { 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'); + } + } diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestMultipleFormsBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestMultipleFormsBlock.php new file mode 100644 index 0000000000000000000000000000000000000000..64b70b22c04ef34b8d520495c9e7719d07e9eb8a --- /dev/null +++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestMultipleFormsBlock.php @@ -0,0 +1,27 @@ +<?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 []; + } + +} diff --git a/core/modules/block/tests/modules/block_test/src/PluginForm/EmptyBlockForm.php b/core/modules/block/tests/modules/block_test/src/PluginForm/EmptyBlockForm.php new file mode 100644 index 0000000000000000000000000000000000000000..6a654cbdef1515d965562ab7ce6cb6a914c1a86e --- /dev/null +++ b/core/modules/block/tests/modules/block_test/src/PluginForm/EmptyBlockForm.php @@ -0,0 +1,27 @@ +<?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. + } + +} diff --git a/core/modules/block/tests/src/Unit/BlockFormTest.php b/core/modules/block/tests/src/Unit/BlockFormTest.php index 09f450b853ac002049c1e4c6acef52857d280744..d8efe2b79109dd2a19d69c6969889a66a19a939c 100644 --- a/core/modules/block/tests/src/Unit/BlockFormTest.php +++ b/core/modules/block/tests/src/Unit/BlockFormTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\block\Unit; use Drupal\block\BlockForm; +use Drupal\Core\Plugin\PluginFormFactoryInterface; use Drupal\Tests\UnitTestCase; /** @@ -54,6 +55,13 @@ class BlockFormTest extends UnitTestCase { */ protected $contextRepository; + /** + * The plugin form manager. + * + * @var \Drupal\Core\Plugin\PluginFormFactoryInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $pluginFormFactory; + /** * {@inheritdoc} */ @@ -71,6 +79,7 @@ protected function setUp() { ->method('getStorage') ->will($this->returnValue($this->storage)); + $this->pluginFormFactory = $this->prophesize(PluginFormFactoryInterface::class); } /** @@ -99,7 +108,7 @@ public function testGetUniqueMachineName() { ->method('getQuery') ->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 // name suggestion. diff --git a/core/tests/Drupal/KernelTests/Core/Block/MultipleBlockFormTest.php b/core/tests/Drupal/KernelTests/Core/Block/MultipleBlockFormTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2fbe025acfe9472a366f4cfdabf9eac36d56ff88 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Block/MultipleBlockFormTest.php @@ -0,0 +1,38 @@ +<?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); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Plugin/DefaultPluginManagerTest.php b/core/tests/Drupal/Tests/Core/Plugin/DefaultPluginManagerTest.php index 8cc0f81c1aaf7e937ef740ec8c2a3f08ddb496d1..feac00467c2e3275f8b4f6a0979edb73f64a186a 100644 --- a/core/tests/Drupal/Tests/Core/Plugin/DefaultPluginManagerTest.php +++ b/core/tests/Drupal/Tests/Core/Plugin/DefaultPluginManagerTest.php @@ -3,6 +3,8 @@ namespace Drupal\Tests\Core\Plugin; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Plugin\PluginFormInterface; use Drupal\Tests\UnitTestCase; /** @@ -338,4 +340,98 @@ public function testGetCacheMaxAge() { $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); + } + + public function providerTestProcessDefinition() { + $data = []; + + $data['merge'][] = [ + 'foo' => [ + 'bar' => [ + 'asdf', + ], + ], + ]; + $data['merge'][] = [ + 'foo' => [ + 'bar' => [ + 'baz', + 'asdf', + ], + ], + ]; + + $object_definition = (object) [ + 'foo' => [ + 'bar' => [ + 'asdf', + ], + ], + ]; + $data['object_definition'] = [$object_definition, clone $object_definition]; + + $data['no_form'][] = ['class' => TestPluginForm::class]; + $data['no_form'][] = [ + 'class' => TestPluginForm::class, + 'forms' => ['configure' => TestPluginForm::class], + 'foo' => ['bar' => ['baz']], + ]; + + $data['default_form'][] = ['class' => TestPluginForm::class, 'forms' => ['configure' => 'stdClass']]; + $data['default_form'][] = [ + 'class' => TestPluginForm::class, + 'forms' => ['configure' => 'stdClass'], + 'foo' => ['bar' => ['baz']], + ]; + return $data; + } + +} + +class TestPluginManagerWithDefaults extends TestPluginManager { + + /** + * {@inheritdoc} + */ + protected $defaults = [ + 'foo' => [ + 'bar' => [ + 'baz', + ], + ], + ]; + +} + +class TestPluginForm implements PluginFormInterface { + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + return []; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + } + } diff --git a/core/tests/Drupal/Tests/Core/Plugin/PluginFormFactoryTest.php b/core/tests/Drupal/Tests/Core/Plugin/PluginFormFactoryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..fc4b77aebe171b7c8c2c58e4918ae4cbb6670058 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Plugin/PluginFormFactoryTest.php @@ -0,0 +1,156 @@ +<?php + +namespace Drupal\Tests\Core\Plugin; + +use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException; +use Drupal\Component\Plugin\PluginAwareInterface; +use Drupal\Core\DependencyInjection\ClassResolverInterface; +use Drupal\Core\Plugin\PluginFormInterface; +use Drupal\Core\Plugin\PluginFormFactory; +use Drupal\Core\Plugin\PluginWithFormsInterface; +use Drupal\Tests\UnitTestCase; +use Prophecy\Argument; + +/** + * @coversDefaultClass \Drupal\Core\Plugin\PluginFormFactory + * @group Plugin + */ +class PluginFormFactoryTest extends UnitTestCase { + + /** + * The class resolver. + * + * @var \Drupal\Core\DependencyInjection\ClassResolverInterface|\Prophecy\Prophecy\ProphecyInterface + */ + protected $classResolver; + + /** + * The manager being tested. + * + * @var \Drupal\Core\Plugin\PluginFormFactory + */ + protected $manager; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->classResolver = $this->prophesize(ClassResolverInterface::class); + $this->manager = new PluginFormFactory($this->classResolver->reveal()); + } + + /** + * @covers ::createInstance + */ + public function testCreateInstance() { + $plugin_form = $this->prophesize(PluginFormInterface::class); + $expected = $plugin_form->reveal(); + + $this->classResolver->getInstanceFromDefinition(get_class($expected))->willReturn($expected); + + $plugin = $this->prophesize(PluginWithFormsInterface::class); + $plugin->hasFormClass('standard_class')->willReturn(TRUE); + $plugin->getFormClass('standard_class')->willReturn(get_class($expected)); + + $form_object = $this->manager->createInstance($plugin->reveal(), 'standard_class'); + $this->assertSame($expected, $form_object); + } + + /** + * @covers ::createInstance + */ + public function testCreateInstanceUsingPlugin() { + $this->classResolver->getInstanceFromDefinition(Argument::cetera())->shouldNotBeCalled(); + + $plugin = $this->prophesize(PluginWithFormsInterface::class)->willImplement(PluginFormInterface::class); + $plugin->hasFormClass('configure')->willReturn(TRUE); + $plugin->getFormClass('configure')->willReturn(get_class($plugin->reveal())); + + $form_object = $this->manager->createInstance($plugin->reveal(), 'configure'); + $this->assertSame($plugin->reveal(), $form_object); + } + + /** + * @covers ::createInstance + */ + public function testCreateInstanceUsingPluginWithSlashes() { + $this->classResolver->getInstanceFromDefinition(Argument::cetera())->shouldNotBeCalled(); + + $plugin = $this->prophesize(PluginWithFormsInterface::class)->willImplement(PluginFormInterface::class); + $plugin->hasFormClass('configure')->willReturn(TRUE); + $plugin->getFormClass('configure')->willReturn('\\' . get_class($plugin->reveal())); + + $form_object = $this->manager->createInstance($plugin->reveal(), 'configure'); + $this->assertSame($plugin->reveal(), $form_object); + } + + /** + * @covers ::createInstance + */ + public function testCreateInstanceDefaultFallback() { + $this->classResolver->getInstanceFromDefinition(Argument::cetera())->shouldNotBeCalled(); + + $plugin = $this->prophesize(PluginWithFormsInterface::class)->willImplement(PluginFormInterface::class); + $plugin->hasFormClass('missing')->willReturn(FALSE); + $plugin->hasFormClass('fallback')->willReturn(TRUE); + $plugin->getFormClass('fallback')->willReturn(get_class($plugin->reveal())); + + $form_object = $this->manager->createInstance($plugin->reveal(), 'missing', 'fallback'); + $this->assertSame($plugin->reveal(), $form_object); + } + + /** + * @covers ::createInstance + */ + public function testCreateInstancePluginAware() { + $plugin_form = $this->prophesize(PluginFormInterface::class)->willImplement(PluginAwareInterface::class); + + $expected = $plugin_form->reveal(); + + $this->classResolver->getInstanceFromDefinition(get_class($expected))->willReturn($expected); + + $plugin = $this->prophesize(PluginWithFormsInterface::class); + $plugin->hasFormClass('operation_aware')->willReturn(TRUE); + $plugin->getFormClass('operation_aware')->willReturn(get_class($expected)); + + $plugin_form->setPlugin($plugin->reveal())->shouldBeCalled(); + + $form_object = $this->manager->createInstance($plugin->reveal(), 'operation_aware'); + $this->assertSame($expected, $form_object); + } + + /** + * @covers ::createInstance + */ + public function testCreateInstanceDefinitionException() { + $this->setExpectedException(InvalidPluginDefinitionException::class, 'The "the_plugin_id" plugin did not specify a "anything" form class'); + + $plugin = $this->prophesize(PluginWithFormsInterface::class); + $plugin->getPluginId()->willReturn('the_plugin_id'); + $plugin->hasFormClass('anything')->willReturn(FALSE); + + $form_object = $this->manager->createInstance($plugin->reveal(), 'anything'); + $this->assertSame(NULL, $form_object); + } + + /** + * @covers ::createInstance + */ + public function testCreateInstanceInvalidException() { + $this->setExpectedException(InvalidPluginDefinitionException::class, 'The "the_plugin_id" plugin did not specify a valid "invalid" form class, must implement \Drupal\Core\Plugin\PluginFormInterface'); + + $expected = new \stdClass(); + $this->classResolver->getInstanceFromDefinition(get_class($expected))->willReturn($expected); + + $plugin = $this->prophesize(PluginWithFormsInterface::class); + $plugin->getPluginId()->willReturn('the_plugin_id'); + $plugin->hasFormClass('invalid')->willReturn(TRUE); + $plugin->getFormClass('invalid')->willReturn(get_class($expected)); + + $form_object = $this->manager->createInstance($plugin->reveal(), 'invalid'); + $this->assertSame(NULL, $form_object); + } + +}