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);
+  }
+
+}