From 120a1da34c131282960dd993fc5bd2eb1036d924 Mon Sep 17 00:00:00 2001
From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org>
Date: Wed, 14 May 2014 21:18:01 +0100
Subject: [PATCH] Issue #2257835 by tim.plunkett, sun, Jalandhar: Move form
 submission logic out of FormBuilder into a new class.

---
 core/core.services.yml                        |   5 +-
 core/includes/form.inc                        |  12 +-
 core/lib/Drupal/Core/Form/FormBuilder.php     | 197 ++----------
 .../Drupal/Core/Form/FormBuilderInterface.php |  77 -----
 core/lib/Drupal/Core/Form/FormSubmitter.php   | 232 ++++++++++++++
 .../Core/Form/FormSubmitterInterface.php      | 106 +++++++
 .../Tests/Core/Form/FormBuilderTest.php       | 151 ---------
 .../Tests/Core/Form/FormSubmitterTest.php     | 291 ++++++++++++++++++
 .../Drupal/Tests/Core/Form/FormTestBase.php   |  55 ++--
 9 files changed, 686 insertions(+), 440 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Form/FormSubmitter.php
 create mode 100644 core/lib/Drupal/Core/Form/FormSubmitterInterface.php
 create mode 100644 core/tests/Drupal/Tests/Core/Form/FormSubmitterTest.php

diff --git a/core/core.services.yml b/core/core.services.yml
index bb65f34ba147..dc55af1cffe2 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -119,10 +119,13 @@ services:
     arguments: [default]
   form_builder:
     class: Drupal\Core\Form\FormBuilder
-    arguments: ['@form_validator', '@module_handler', '@keyvalue.expirable', '@event_dispatcher', '@url_generator', '@request_stack', '@?csrf_token', '@?http_kernel']
+    arguments: ['@form_validator', '@form_submitter', '@module_handler', '@keyvalue.expirable', '@event_dispatcher', '@request_stack', '@?csrf_token', '@?http_kernel']
   form_validator:
     class: Drupal\Core\Form\FormValidator
     arguments: ['@request_stack', '@string_translation', '@csrf_token']
+  form_submitter:
+    class: Drupal\Core\Form\FormSubmitter
+    arguments: ['@request_stack', '@url_generator']
   keyvalue:
     class: Drupal\Core\KeyValueStore\KeyValueFactory
     arguments: ['@service_container', '@settings']
diff --git a/core/includes/form.inc b/core/includes/form.inc
index c037ce1eda58..42206a444369 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -334,27 +334,27 @@ function drupal_validate_form($form_id, &$form, &$form_state) {
  * Redirects the user to a URL after a form has been processed.
  *
  * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
- *   Use \Drupal::formBuilder()->redirectForm().
+ *   Use \Drupal::service('form_submitter')->redirectForm().
  *
- * @see \Drupal\Core\Form\FormBuilderInterface::redirectForm().
+ * @see \Drupal\Core\Form\FormSubmitterInterface::redirectForm().
  */
 function drupal_redirect_form($form_state) {
-  return \Drupal::formBuilder()->redirectForm($form_state);
+  return \Drupal::service('form_submitter')->redirectForm($form_state);
 }
 
 /**
  * Executes custom validation and submission handlers for a given form.
  *
  * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
- *   Use either \Drupal::formBuilder()->executeSubmitHandlers() or
+ *   Use either \Drupal::service('form_submitter')->executeSubmitHandlers() or
  *   \Drupal::service('form_validator')->executeValidateHandlers().
  *
- * @see \Drupal\Core\Form\FormBuilderInterface::executeSubmitHandlers()
+ * @see \Drupal\Core\Form\FormSubmitterInterface::executeSubmitHandlers()
  * @see \Drupal\Core\Form\FormValidatorInterface::executeValidateHandlers()
  */
 function form_execute_handlers($type, &$form, &$form_state) {
   if ($type == 'submit') {
-    \Drupal::formBuilder()->executeSubmitHandlers($form, $form_state);
+    \Drupal::service('form_submitter')->executeSubmitHandlers($form, $form_state);
   }
   elseif ($type == 'validate') {
     \Drupal::service('form_validator')->executeValidateHandlers($form, $form_state);
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index 3efa1f2d3099..ebaf19b14261 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -15,11 +15,8 @@
 use Drupal\Core\HttpKernel;
 use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface;
 use Drupal\Core\Render\Element;
-use Drupal\Core\Routing\UrlGeneratorInterface;
 use Drupal\Core\Site\Settings;
-use Drupal\Core\Url;
 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
-use Symfony\Component\HttpFoundation\RedirectResponse;
 use Symfony\Component\HttpFoundation\RequestStack;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
@@ -29,7 +26,7 @@
 /**
  * Provides form building and processing.
  */
-class FormBuilder implements FormBuilderInterface, FormValidatorInterface {
+class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormSubmitterInterface {
 
   /**
    * The module handler.
@@ -52,13 +49,6 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface {
    */
   protected $eventDispatcher;
 
-  /**
-   * The URL generator.
-   *
-   * @var \Drupal\Core\Routing\UrlGeneratorInterface
-   */
-  protected $urlGenerator;
-
   /**
    * The request stack.
    *
@@ -92,19 +82,24 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface {
    */
   protected $formValidator;
 
+  /**
+   * @var \Drupal\Core\Form\FormSubmitterInterface
+   */
+  protected $formSubmitter;
+
   /**
    * Constructs a new FormBuilder.
    *
    * @param \Drupal\Core\Form\FormValidatorInterface $form_validator
    *   The form validator.
+   * @param \Drupal\Core\Form\FormSubmitterInterface $form_submitter
+   *   The form submission processor.
    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
    *   The module handler.
    * @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface $key_value_expirable_factory
    *   The keyvalue expirable factory.
    * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
    *   The event dispatcher.
-   * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
-   *   The URL generator.
    * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
    *   The request stack.
    * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
@@ -112,12 +107,12 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface {
    * @param \Drupal\Core\HttpKernel $http_kernel
    *   The HTTP kernel.
    */
-  public function __construct(FormValidatorInterface $form_validator, ModuleHandlerInterface $module_handler, KeyValueExpirableFactoryInterface $key_value_expirable_factory, EventDispatcherInterface $event_dispatcher, UrlGeneratorInterface $url_generator, RequestStack $request_stack, CsrfTokenGenerator $csrf_token = NULL, HttpKernel $http_kernel = NULL) {
+  public function __construct(FormValidatorInterface $form_validator, FormSubmitterInterface $form_submitter, ModuleHandlerInterface $module_handler, KeyValueExpirableFactoryInterface $key_value_expirable_factory, EventDispatcherInterface $event_dispatcher, RequestStack $request_stack, CsrfTokenGenerator $csrf_token = NULL, HttpKernel $http_kernel = NULL) {
     $this->formValidator = $form_validator;
+    $this->formSubmitter = $form_submitter;
     $this->moduleHandler = $module_handler;
     $this->keyValueExpirableFactory = $key_value_expirable_factory;
     $this->eventDispatcher = $event_dispatcher;
-    $this->urlGenerator = $url_generator;
     $this->requestStack = $request_stack;
     $this->csrfToken = $csrf_token;
     $this->httpKernel = $http_kernel;
@@ -555,53 +550,9 @@ public function processForm($form_id, &$form, &$form_state) {
         $this->drupalStaticReset('drupal_html_id');
       }
 
-      // @todo Move into a dedicated class in https://drupal.org/node/2257835.
-      if ($form_state['submitted'] && !$this->getAnyErrors() && !$form_state['rebuild']) {
-        // Execute form submit handlers.
-        $this->executeSubmitHandlers($form, $form_state);
-
-        // If batches were set in the submit handlers, we process them now,
-        // possibly ending execution. We make sure we do not react to the batch
-        // that is already being processed (if a batch operation performs a
-        // self::submitForm).
-        if ($batch = &$this->batchGet() && !isset($batch['current_set'])) {
-          // Store $form_state information in the batch definition.
-          // We need the full $form_state when either:
-          // - Some submit handlers were saved to be called during batch
-          //   processing. See self::executeSubmitHandlers().
-          // - The form is multistep.
-          // In other cases, we only need the information expected by
-          // self::redirectForm().
-          if ($batch['has_form_submits'] || !empty($form_state['rebuild'])) {
-            $batch['form_state'] = $form_state;
-          }
-          else {
-            $batch['form_state'] = array_intersect_key($form_state, array_flip(array('programmed', 'rebuild', 'storage', 'no_redirect', 'redirect', 'redirect_route')));
-          }
-
-          $batch['progressive'] = !$form_state['programmed'];
-          $response = batch_process();
-          if ($batch['progressive']) {
-            return $response;
-          }
-
-          // Execution continues only for programmatic forms.
-          // For 'regular' forms, we get redirected to the batch processing
-          // page. Form redirection will be handled in _batch_finished(),
-          // after the batch is processed.
-        }
-
-        // Set a flag to indicate the the form has been processed and executed.
-        $form_state['executed'] = TRUE;
-
-        // If no response has been set, process the form redirect.
-        if (!isset($form_state['response']) && $redirect = $this->redirectForm($form_state)) {
-          $form_state['response'] = $redirect;
-        }
-
-        // If there is a response was set, return it instead of continuing.
-        if (isset($form_state['response']) && $form_state['response'] instanceof Response) {
-          return $form_state['response'];
+      if (!$form_state['rebuild'] && !$this->formValidator->getAnyErrors()) {
+        if ($submit_response = $this->formSubmitter->doSubmitForm($form, $form_state)) {
+          return $submit_response;
         }
       }
 
@@ -793,78 +744,7 @@ public function validateForm($form_id, &$form, &$form_state) {
    * {@inheritdoc}
    */
   public function redirectForm($form_state) {
-    // Skip redirection for form submissions invoked via self::submitForm().
-    if (!empty($form_state['programmed'])) {
-      return;
-    }
-    // Skip redirection if rebuild is activated.
-    if (!empty($form_state['rebuild'])) {
-      return;
-    }
-    // Skip redirection if it was explicitly disallowed.
-    if (!empty($form_state['no_redirect'])) {
-      return;
-    }
-
-    // Allow using redirect responses directly if needed.
-    if (isset($form_state['redirect']) && $form_state['redirect'] instanceof RedirectResponse) {
-      return $form_state['redirect'];
-    }
-
-    // Check for a route-based redirection.
-    if (isset($form_state['redirect_route'])) {
-      // @todo Remove once all redirects are converted to Url.
-      if (!($form_state['redirect_route'] instanceof Url)) {
-        $form_state['redirect_route'] += array(
-          'route_parameters' => array(),
-          'options' => array(),
-        );
-        $form_state['redirect_route'] = new Url($form_state['redirect_route']['route_name'], $form_state['redirect_route']['route_parameters'], $form_state['redirect_route']['options']);
-      }
-
-      $form_state['redirect_route']->setAbsolute();
-      return new RedirectResponse($form_state['redirect_route']->toString());
-    }
-
-    // Only invoke a redirection if redirect value was not set to FALSE.
-    if (!isset($form_state['redirect']) || $form_state['redirect'] !== FALSE) {
-      if (isset($form_state['redirect'])) {
-        if (is_array($form_state['redirect'])) {
-          if (isset($form_state['redirect'][1])) {
-            $options = $form_state['redirect'][1];
-          }
-          else {
-            $options = array();
-          }
-          // Redirections should always use absolute URLs.
-          $options['absolute'] = TRUE;
-          if (isset($form_state['redirect'][2])) {
-            $status_code = $form_state['redirect'][2];
-          }
-          else {
-            $status_code = 302;
-          }
-          return new RedirectResponse($this->urlGenerator->generateFromPath($form_state['redirect'][0], $options), $status_code);
-        }
-        else {
-          // This function can be called from the installer, which guarantees
-          // that $redirect will always be a string, so catch that case here
-          // and use the appropriate redirect function.
-          if ($this->drupalInstallationAttempted()) {
-            install_goto($form_state['redirect']);
-          }
-          else {
-            return new RedirectResponse($this->urlGenerator->generateFromPath($form_state['redirect'], array('absolute' => TRUE)));
-          }
-        }
-      }
-      $request = $this->requestStack->getCurrentRequest();
-      $url = $this->urlGenerator->generateFromPath($request->attributes->get('_system_path'), array(
-        'query' => $request->query->all(),
-        'absolute' => TRUE,
-      ));
-      return new RedirectResponse($url);
-    }
+    return $this->formSubmitter->redirectForm($form_state);
   }
 
   /**
@@ -878,33 +758,14 @@ public function executeValidateHandlers(&$form, &$form_state) {
    * {@inheritdoc}
    */
   public function executeSubmitHandlers(&$form, &$form_state) {
-    // If there was a button pressed, use its handlers.
-    if (isset($form_state['submit_handlers'])) {
-      $handlers = $form_state['submit_handlers'];
-    }
-    // Otherwise, check for a form-level handler.
-    elseif (isset($form['#submit'])) {
-      $handlers = $form['#submit'];
-    }
-    else {
-      $handlers = array();
-    }
+    $this->formSubmitter->executeSubmitHandlers($form, $form_state);
+  }
 
-    foreach ($handlers as $function) {
-      // Check if a previous _submit handler has set a batch, but make sure we
-      // do not react to a batch that is already being processed (for instance
-      // if a batch operation performs a self::submitForm()).
-      if (($batch = &$this->batchGet()) && !isset($batch['id'])) {
-        // Some previous submit handler has set a batch. To ensure correct
-        // execution order, store the call in a special 'control' batch set.
-        // See _batch_next_set().
-        $batch['sets'][] = array('form_submit' => $function);
-        $batch['has_form_submits'] = TRUE;
-      }
-      else {
-        call_user_func_array($function, array(&$form, &$form_state));
-      }
-    }
+  /**
+   * {@inheritdoc}
+   */
+  public function doSubmitForm(&$form, &$form_state) {
+    throw new \LogicException('Use FormBuilderInterface::processForm() instead.');
   }
 
   /**
@@ -1378,15 +1239,6 @@ protected function getElementInfo($type) {
     return element_info($type);
   }
 
-  /**
-   * Wraps drupal_installation_attempted().
-   *
-   * @return bool
-   */
-  protected function drupalInstallationAttempted() {
-    return drupal_installation_attempted();
-  }
-
   /**
    * Wraps drupal_html_class().
    *
@@ -1430,11 +1282,4 @@ protected function currentUser() {
     return $this->currentUser;
   }
 
-  /**
-   * Wraps batch_get().
-   */
-  protected function &batchGet() {
-    return batch_get();
-  }
-
 }
diff --git a/core/lib/Drupal/Core/Form/FormBuilderInterface.php b/core/lib/Drupal/Core/Form/FormBuilderInterface.php
index 1e43984cb913..bf00f50ad186 100644
--- a/core/lib/Drupal/Core/Form/FormBuilderInterface.php
+++ b/core/lib/Drupal/Core/Form/FormBuilderInterface.php
@@ -385,83 +385,6 @@ public function processForm($form_id, &$form, &$form_state);
    */
   public function prepareForm($form_id, &$form, &$form_state);
 
-  /**
-   * Redirects the user to a URL after a form has been processed.
-   *
-   * After a form is submitted and processed, normally the user should be
-   * redirected to a new destination page. This function figures out what that
-   * destination should be, based on the $form_state array and the 'destination'
-   * query string in the request URL, and redirects the user there.
-   *
-   * Usually (for exceptions, see below) $form_state['redirect'] determines
-   * where to redirect the user. This can be set either to a string (the path to
-   * redirect to), or an array of arguments for url(). If
-   * $form_state['redirect'] is missing, the user is usually (again, see below
-   * for exceptions) redirected back to the page they came from, where they
-   * should see a fresh, unpopulated copy of the form.
-   *
-   * Here is an example of how to set up a form to redirect to the path 'node':
-   * @code
-   * $form_state['redirect'] = 'node';
-   * @endcode
-   * And here is an example of how to redirect to 'node/123?foo=bar#baz':
-   * @code
-   * $form_state['redirect'] = array(
-   *   'node/123',
-   *   array(
-   *     'query' => array(
-   *       'foo' => 'bar',
-   *     ),
-   *     'fragment' => 'baz',
-   *   ),
-   * );
-   * @endcode
-   *
-   * There are several exceptions to the "usual" behavior described above:
-   * - If $form_state['programmed'] is TRUE, the form submission was usually
-   *   invoked via self::submitForm(), so any redirection would break the script
-   *   that invoked self::submitForm() and no redirection is done.
-   * - If $form_state['rebuild'] is TRUE, the form is being rebuilt, and no
-   *   redirection is done.
-   * - If $form_state['no_redirect'] is TRUE, redirection is disabled. This is
-   *   set, for instance, by \Drupal\system\FormAjaxController::getForm() to
-   *   prevent redirection in Ajax callbacks. $form_state['no_redirect'] should
-   *   never be set or altered by form builder functions or form validation
-   *   or submit handlers.
-   * - If $form_state['redirect'] is set to FALSE, redirection is disabled.
-   * - If none of the above conditions has prevented redirection, then the
-   *   redirect is accomplished by returning a RedirectResponse, passing in the
-   *   value of $form_state['redirect'] if it is set, or the current path if it
-   *   is not. RedirectResponse preferentially uses the value of
-   *   \Drupal::request->query->get('destination') (the 'destination' URL query
-   *   string) if it is present, so this will override any values set by
-   *   $form_state['redirect'].
-   *
-   * @param $form_state
-   *   An associative array containing the current state of the form.
-   *
-   * @return \Symfony\Component\HttpFoundation\RedirectResponse|null
-   *
-   * @see self::processForm()
-   * @see self::buildForm()
-   */
-  public function redirectForm($form_state);
-
-  /**
-   * Executes custom submission handlers for a given form.
-   *
-   * Button-specific handlers are checked first. If none exist, the function
-   * falls back to form-level handlers.
-   *
-   * @param $form
-   *   An associative array containing the structure of the form.
-   * @param $form_state
-   *   A keyed array containing the current state of the form. If the user
-   *   submitted the form by clicking a button with custom handler functions
-   *   defined, those handlers will be stored here.
-   */
-  public function executeSubmitHandlers(&$form, &$form_state);
-
   /**
    * Builds and processes all elements in the structured form array.
    *
diff --git a/core/lib/Drupal/Core/Form/FormSubmitter.php b/core/lib/Drupal/Core/Form/FormSubmitter.php
new file mode 100644
index 000000000000..fea8f540b9a5
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/FormSubmitter.php
@@ -0,0 +1,232 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Form\FormSubmitter.
+ */
+
+namespace Drupal\Core\Form;
+
+use Drupal\Core\Url;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\HttpFoundation\Response;
+use Drupal\Core\Routing\UrlGeneratorInterface;
+
+/**
+ * Provides submission processing for forms.
+ */
+class FormSubmitter implements FormSubmitterInterface {
+
+  /**
+   * The URL generator.
+   *
+   * @var \Drupal\Core\Routing\UrlGeneratorInterface
+   */
+  protected $urlGenerator;
+
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * Constructs a new FormValidator.
+   *
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   * @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
+   */
+  public function __construct(RequestStack $request_stack, UrlGeneratorInterface $url_generator) {
+    $this->requestStack = $request_stack;
+    $this->urlGenerator = $url_generator;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function doSubmitForm(&$form, &$form_state) {
+    if (!$form_state['submitted']) {
+      return;
+    }
+
+    // Execute form submit handlers.
+    $this->executeSubmitHandlers($form, $form_state);
+
+    // If batches were set in the submit handlers, we process them now,
+    // possibly ending execution. We make sure we do not react to the batch
+    // that is already being processed (if a batch operation performs a
+    // \Drupal\Core\Form\FormBuilderInterface::submitForm).
+    if ($batch = &$this->batchGet() && !isset($batch['current_set'])) {
+      // Store $form_state information in the batch definition.
+      // We need the full $form_state when either:
+      // - Some submit handlers were saved to be called during batch
+      //   processing. See self::executeSubmitHandlers().
+      // - The form is multistep.
+      // In other cases, we only need the information expected by
+      // self::redirectForm().
+      if ($batch['has_form_submits'] || !empty($form_state['rebuild'])) {
+        $batch['form_state'] = $form_state;
+      }
+      else {
+        $batch['form_state'] = array_intersect_key($form_state, array_flip(array('programmed', 'rebuild', 'storage', 'no_redirect', 'redirect', 'redirect_route')));
+      }
+
+      $batch['progressive'] = !$form_state['programmed'];
+      $response = batch_process();
+      if ($batch['progressive']) {
+        return $response;
+      }
+
+      // Execution continues only for programmatic forms.
+      // For 'regular' forms, we get redirected to the batch processing
+      // page. Form redirection will be handled in _batch_finished(),
+      // after the batch is processed.
+    }
+
+    // Set a flag to indicate the the form has been processed and executed.
+    $form_state['executed'] = TRUE;
+
+    // If no response has been set, process the form redirect.
+    if (!isset($form_state['response']) && $redirect = $this->redirectForm($form_state)) {
+      $form_state['response'] = $redirect;
+    }
+
+    // If there is a response was set, return it instead of continuing.
+    if (isset($form_state['response']) && $form_state['response'] instanceof Response) {
+      return $form_state['response'];
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function executeSubmitHandlers(&$form, &$form_state) {
+    // If there was a button pressed, use its handlers.
+    if (isset($form_state['submit_handlers'])) {
+      $handlers = $form_state['submit_handlers'];
+    }
+    // Otherwise, check for a form-level handler.
+    elseif (isset($form['#submit'])) {
+      $handlers = $form['#submit'];
+    }
+    else {
+      $handlers = array();
+    }
+
+    foreach ($handlers as $function) {
+      // Check if a previous _submit handler has set a batch, but make sure we
+      // do not react to a batch that is already being processed (for instance
+      // if a batch operation performs a
+      //  \Drupal\Core\Form\FormBuilderInterface::submitForm()).
+      if (($batch = &$this->batchGet()) && !isset($batch['id'])) {
+        // Some previous submit handler has set a batch. To ensure correct
+        // execution order, store the call in a special 'control' batch set.
+        // See _batch_next_set().
+        $batch['sets'][] = array('form_submit' => $function);
+        $batch['has_form_submits'] = TRUE;
+      }
+      else {
+        call_user_func_array($function, array(&$form, &$form_state));
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function redirectForm($form_state) {
+    // Skip redirection for form submissions invoked via
+    // \Drupal\Core\Form\FormBuilderInterface::submitForm().
+    if (!empty($form_state['programmed'])) {
+      return;
+    }
+    // Skip redirection if rebuild is activated.
+    if (!empty($form_state['rebuild'])) {
+      return;
+    }
+    // Skip redirection if it was explicitly disallowed.
+    if (!empty($form_state['no_redirect'])) {
+      return;
+    }
+
+    // Allow using redirect responses directly if needed.
+    if (isset($form_state['redirect']) && $form_state['redirect'] instanceof RedirectResponse) {
+      return $form_state['redirect'];
+    }
+
+    // Check for a route-based redirection.
+    if (isset($form_state['redirect_route'])) {
+      // @todo Remove once all redirects are converted to Url.
+      if (!($form_state['redirect_route'] instanceof Url)) {
+        $form_state['redirect_route'] += array(
+          'route_parameters' => array(),
+          'options' => array(),
+        );
+        $form_state['redirect_route'] = new Url($form_state['redirect_route']['route_name'], $form_state['redirect_route']['route_parameters'], $form_state['redirect_route']['options']);
+      }
+
+      $form_state['redirect_route']->setAbsolute();
+      return new RedirectResponse($form_state['redirect_route']->toString());
+    }
+
+    // Only invoke a redirection if redirect value was not set to FALSE.
+    if (!isset($form_state['redirect']) || $form_state['redirect'] !== FALSE) {
+      if (isset($form_state['redirect'])) {
+        if (is_array($form_state['redirect'])) {
+          if (isset($form_state['redirect'][1])) {
+            $options = $form_state['redirect'][1];
+          }
+          else {
+            $options = array();
+          }
+          // Redirections should always use absolute URLs.
+          $options['absolute'] = TRUE;
+          if (isset($form_state['redirect'][2])) {
+            $status_code = $form_state['redirect'][2];
+          }
+          else {
+            $status_code = 302;
+          }
+          return new RedirectResponse($this->urlGenerator->generateFromPath($form_state['redirect'][0], $options), $status_code);
+        }
+        else {
+          // This function can be called from the installer, which guarantees
+          // that $redirect will always be a string, so catch that case here
+          // and use the appropriate redirect function.
+          if ($this->drupalInstallationAttempted()) {
+            install_goto($form_state['redirect']);
+          }
+          else {
+            return new RedirectResponse($this->urlGenerator->generateFromPath($form_state['redirect'], array('absolute' => TRUE)));
+          }
+        }
+      }
+      $request = $this->requestStack->getCurrentRequest();
+      $url = $this->urlGenerator->generateFromPath($request->attributes->get('_system_path'), array(
+        'query' => $request->query->all(),
+        'absolute' => TRUE,
+      ));
+      return new RedirectResponse($url);
+    }
+  }
+
+  /**
+   * Wraps drupal_installation_attempted().
+   *
+   * @return bool
+   */
+  protected function drupalInstallationAttempted() {
+    return drupal_installation_attempted();
+  }
+
+  /**
+   * Wraps batch_get().
+   */
+  protected function &batchGet() {
+    return batch_get();
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Form/FormSubmitterInterface.php b/core/lib/Drupal/Core/Form/FormSubmitterInterface.php
new file mode 100644
index 000000000000..8ae8b0832e7a
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/FormSubmitterInterface.php
@@ -0,0 +1,106 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Form\FormSubmitterInterface.
+ */
+
+namespace Drupal\Core\Form;
+
+/**
+ * Provides an interface for processing form submissions.
+ */
+interface FormSubmitterInterface {
+
+  /**
+   * Handles the submitted form, executing callbacks and processing responses.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param array $form_state
+   *   An associative array containing the current state of the form.
+   *
+   * @return null|\Symfony\Component\HttpFoundation\Response
+   *   If a response was set by a submit handler, or if the form needs to
+   *   redirect, a Response object will be returned.
+   */
+  public function doSubmitForm(&$form, &$form_state);
+
+  /**
+   * Executes custom submission handlers for a given form.
+   *
+   * Button-specific handlers are checked first. If none exist, the function
+   * falls back to form-level handlers.
+   *
+   * @param $form
+   *   An associative array containing the structure of the form.
+   * @param $form_state
+   *   A keyed array containing the current state of the form. If the user
+   *   submitted the form by clicking a button with custom handler functions
+   *   defined, those handlers will be stored here.
+   */
+  public function executeSubmitHandlers(&$form, &$form_state);
+
+  /**
+   * Redirects the user to a URL after a form has been processed.
+   *
+   * After a form is submitted and processed, normally the user should be
+   * redirected to a new destination page. This function figures out what that
+   * destination should be, based on the $form_state array and the 'destination'
+   * query string in the request URL, and redirects the user there.
+   *
+   * Usually (for exceptions, see below) $form_state['redirect'] determines
+   * where to redirect the user. This can be set either to a string (the path to
+   * redirect to), or an array of arguments for url(). If
+   * $form_state['redirect'] is missing, the user is usually (again, see below
+   * for exceptions) redirected back to the page they came from, where they
+   * should see a fresh, unpopulated copy of the form.
+   *
+   * Here is an example of how to set up a form to redirect to the path 'node':
+   * @code
+   * $form_state['redirect'] = 'node';
+   * @endcode
+   * And here is an example of how to redirect to 'node/123?foo=bar#baz':
+   * @code
+   * $form_state['redirect'] = array(
+   *   'node/123',
+   *   array(
+   *     'query' => array(
+   *       'foo' => 'bar',
+   *     ),
+   *     'fragment' => 'baz',
+   *   ),
+   * );
+   * @endcode
+   *
+   * There are several exceptions to the "usual" behavior described above:
+   * - If $form_state['programmed'] is TRUE, the form submission was usually
+   *   invoked via self::submitForm(), so any redirection would break the script
+   *   that invoked self::submitForm() and no redirection is done.
+   * - If $form_state['rebuild'] is TRUE, the form is being rebuilt, and no
+   *   redirection is done.
+   * - If $form_state['no_redirect'] is TRUE, redirection is disabled. This is
+   *   set, for instance, by \Drupal\system\FormAjaxController::getForm() to
+   *   prevent redirection in Ajax callbacks. $form_state['no_redirect'] should
+   *   never be set or altered by form builder functions or form validation
+   *   or submit handlers.
+   * - If $form_state['redirect'] is set to FALSE, redirection is disabled.
+   * - If none of the above conditions has prevented redirection, then the
+   *   redirect is accomplished by returning a RedirectResponse, passing in the
+   *   value of $form_state['redirect'] if it is set, or the current path if it
+   *   is not. RedirectResponse preferentially uses the value of
+   *   \Drupal::request->query->get('destination') (the 'destination' URL query
+   *   string) if it is present, so this will override any values set by
+   *   $form_state['redirect'].
+   *
+   * @param $form_state
+   *   An associative array containing the current state of the form.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse|null
+   *
+   * @see \Drupal\Core\Form\FormBuilderInterface::processForm()
+   * @see \Drupal\Core\Form\FormBuilderInterface::buildForm()
+   */
+  public function redirectForm($form_state);
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
index f7c94ed44bce..8b09681f4043 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
@@ -7,12 +7,9 @@
 
 namespace Drupal\Tests\Core\Form {
 
-use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
 use Drupal\Core\Form\FormInterface;
-use Drupal\Core\Url;
 use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\HttpFoundation\RedirectResponse;
 
 /**
  * Tests the form builder.
@@ -35,17 +32,6 @@ public static function getInfo() {
     );
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    parent::setUp();
-
-    $container = new ContainerBuilder();
-    $container->set('url_generator', $this->urlGenerator);
-    \Drupal::setContainer($container);
-  }
-
   /**
    * Tests the getFormId() method with a string based form ID.
    */
@@ -219,140 +205,6 @@ public function testHandleRedirectWithResponse() {
     }
     $this->assertSame($response, $form_state['response']);
   }
-
-  /**
-   * Tests the redirectForm() method when a redirect is expected.
-   *
-   * @param array $form_state
-   *   An array of form state data to use for the redirect.
-   * @param string $result
-   *   The URL the redirect is targeting.
-   * @param int $status
-   *   (optional) The HTTP status code for the redirect.
-   *
-   * @dataProvider providerTestRedirectWithResult
-   */
-  public function testRedirectWithResult($form_state, $result, $status = 302) {
-    $this->urlGenerator->expects($this->once())
-      ->method('generateFromPath')
-      ->will($this->returnValueMap(array(
-        array(NULL, array('query' => array(), 'absolute' => TRUE), '<front>'),
-        array('foo', array('absolute' => TRUE), 'foo'),
-        array('bar', array('query' => array('foo' => 'baz'), 'absolute' => TRUE), 'bar'),
-        array('baz', array('absolute' => TRUE), 'baz'),
-      ))
-    );
-
-    $form_state += $this->formBuilder->getFormStateDefaults();
-    $redirect = $this->formBuilder->redirectForm($form_state);
-    $this->assertSame($result, $redirect->getTargetUrl());
-    $this->assertSame($status, $redirect->getStatusCode());
-  }
-
-  /**
-   * Tests the redirectForm() with redirect_route when a redirect is expected.
-   *
-   * @param array $form_state
-   *   An array of form state data to use for the redirect.
-   * @param string $result
-   *   The URL the redirect is targeting.
-   * @param int $status
-   *   (optional) The HTTP status code for the redirect.
-   *
-   * @dataProvider providerTestRedirectWithRouteWithResult
-   */
-  public function testRedirectWithRouteWithResult($form_state, $result, $status = 302) {
-    $this->urlGenerator->expects($this->once())
-      ->method('generateFromRoute')
-      ->will($this->returnValueMap(array(
-          array('test_route_a', array(), array('absolute' => TRUE), 'test-route'),
-          array('test_route_b', array('key' => 'value'), array('absolute' => TRUE), 'test-route/value'),
-        ))
-      );
-
-    $form_state += $this->formBuilder->getFormStateDefaults();
-    $redirect = $this->formBuilder->redirectForm($form_state);
-    $this->assertSame($result, $redirect->getTargetUrl());
-    $this->assertSame($status, $redirect->getStatusCode());
-  }
-
-  /**
-   * Tests the redirectForm() method with a response object.
-   */
-  public function testRedirectWithResponseObject() {
-    $redirect = new RedirectResponse('/example');
-    $form_state['redirect'] = $redirect;
-
-    $form_state += $this->formBuilder->getFormStateDefaults();
-    $result_redirect = $this->formBuilder->redirectForm($form_state);
-
-    $this->assertSame($redirect, $result_redirect);
-  }
-
-  /**
-   * Tests the redirectForm() method when no redirect is expected.
-   *
-   * @param array $form_state
-   *   An array of form state data to use for the redirect.
-   *
-   * @dataProvider providerTestRedirectWithoutResult
-   */
-  public function testRedirectWithoutResult($form_state) {
-    $this->urlGenerator->expects($this->never())
-      ->method('generateFromPath');
-    $this->urlGenerator->expects($this->never())
-      ->method('generateFromRoute');
-    $form_state += $this->formBuilder->getFormStateDefaults();
-    $redirect = $this->formBuilder->redirectForm($form_state);
-    $this->assertNull($redirect);
-  }
-
-  /**
-   * Provides test data for testing the redirectForm() method with a redirect.
-   *
-   * @return array
-   *   Returns some test data.
-   */
-  public function providerTestRedirectWithResult() {
-    return array(
-      array(array(), '<front>'),
-      array(array('redirect' => 'foo'), 'foo'),
-      array(array('redirect' => array('foo')), 'foo'),
-      array(array('redirect' => array('foo')), 'foo'),
-      array(array('redirect' => array('bar', array('query' => array('foo' => 'baz')))), 'bar'),
-      array(array('redirect' => array('baz', array(), 301)), 'baz', 301),
-    );
-  }
-
-  /**
-   * Provides test data for testing the redirectForm() method with a route name.
-   *
-   * @return array
-   *   Returns some test data.
-   */
-  public function providerTestRedirectWithRouteWithResult() {
-    return array(
-      array(array('redirect_route' => array('route_name' => 'test_route_a')), 'test-route'),
-      array(array('redirect_route' => array('route_name' => 'test_route_b', 'route_parameters' => array('key' => 'value'))), 'test-route/value'),
-      array(array('redirect_route' => new Url('test_route_b', array('key' => 'value'))), 'test-route/value'),
-    );
-  }
-
-  /**
-   * Provides test data for testing the redirectForm() method with no redirect.
-   *
-   * @return array
-   *   Returns some test data.
-   */
-  public function providerTestRedirectWithoutResult() {
-    return array(
-      array(array('programmed' => TRUE)),
-      array(array('rebuild' => TRUE)),
-      array(array('no_redirect' => TRUE)),
-      array(array('redirect' => FALSE)),
-    );
-  }
-
   /**
    * Tests the getForm() method with a string based form ID.
    */
@@ -554,9 +406,6 @@ public function testUniqueHtmlId() {
     $form_id = 'test_form_id';
     $expected_form = $form_id();
     $expected_form['test']['#required'] = TRUE;
-    $this->formValidator->expects($this->exactly(4))
-      ->method('getAnyErrors')
-      ->will($this->returnValue(TRUE));
 
     // Mock a form object that will be built two times.
     $form_arg = $this->getMock('Drupal\Core\Form\FormInterface');
diff --git a/core/tests/Drupal/Tests/Core/Form/FormSubmitterTest.php b/core/tests/Drupal/Tests/Core/Form/FormSubmitterTest.php
new file mode 100644
index 000000000000..5dcc66816b60
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Form/FormSubmitterTest.php
@@ -0,0 +1,291 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Form\FormSubmitterTest.
+ */
+
+namespace Drupal\Tests\Core\Form;
+
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Url;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Tests the form submission handler.
+ *
+ * @coversDefaultClass \Drupal\Core\Form\FormSubmitter
+ *
+ * @group Drupal
+ * @group Form
+ */
+class FormSubmitterTest extends UnitTestCase {
+
+  /**
+   * The mocked URL generator.
+   *
+   * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\Routing\UrlGeneratorInterface
+   */
+  protected $urlGenerator;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Form submission test',
+      'description' => 'Tests the form submission handler.',
+      'group' => 'Form API',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->urlGenerator = $this->getMock('Drupal\Core\Routing\UrlGeneratorInterface');
+  }
+
+  /**
+   * @covers ::doSubmitForm
+   */
+  public function testHandleFormSubmissionNotSubmitted() {
+    $form_submitter = $this->getFormSubmitter();
+    $form = array();
+    $form_state = $this->getFormStateDefaults();
+
+    $return = $form_submitter->doSubmitForm($form, $form_state);
+    $this->assertFalse($form_state['executed']);
+    $this->assertNull($return);
+  }
+
+  /**
+   * @covers ::doSubmitForm
+   */
+  public function testHandleFormSubmissionNoRedirect() {
+    $form_submitter = $this->getFormSubmitter();
+    $form = array();
+    $form_state = $this->getFormStateDefaults();
+    $form_state['submitted'] = TRUE;
+    $form_state['no_redirect'] = TRUE;
+
+    $return = $form_submitter->doSubmitForm($form, $form_state);
+    $this->assertTrue($form_state['executed']);
+    $this->assertNull($return);
+  }
+
+  /**
+   * @covers ::doSubmitForm
+   *
+   * @dataProvider providerTestHandleFormSubmissionWithResponses
+   */
+  public function testHandleFormSubmissionWithResponses($class, $form_state_key) {
+    $response = $this->getMockBuilder($class)
+      ->disableOriginalConstructor()
+      ->getMock();
+    $response->expects($this->any())
+      ->method('prepare')
+      ->will($this->returnValue($response));
+
+    $form_state = $this->getFormStateDefaults();
+    $form_state['submitted'] = TRUE;
+    $form_state[$form_state_key] = $response;
+
+    $form_submitter = $this->getFormSubmitter();
+    $form = array();
+    $return = $form_submitter->doSubmitForm($form, $form_state);
+
+    $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $return);
+  }
+
+  public function providerTestHandleFormSubmissionWithResponses() {
+    return array(
+      array('Symfony\Component\HttpFoundation\Response', 'response'),
+      array('Symfony\Component\HttpFoundation\RedirectResponse', 'redirect'),
+    );
+  }
+
+  /**
+   * Tests the redirectForm() method when a redirect is expected.
+   *
+   * @covers ::redirectForm
+   *
+   * @dataProvider providerTestRedirectWithResult
+   */
+  public function testRedirectWithResult($form_state, $result, $status = 302) {
+    $form_submitter = $this->getFormSubmitter();
+    $this->urlGenerator->expects($this->once())
+      ->method('generateFromPath')
+      ->will($this->returnValueMap(array(
+          array(NULL, array('query' => array(), 'absolute' => TRUE), '<front>'),
+          array('foo', array('absolute' => TRUE), 'foo'),
+          array('bar', array('query' => array('foo' => 'baz'), 'absolute' => TRUE), 'bar'),
+          array('baz', array('absolute' => TRUE), 'baz'),
+        ))
+      );
+
+    $form_state += $this->getFormStateDefaults();
+    $redirect = $form_submitter->redirectForm($form_state);
+    $this->assertSame($result, $redirect->getTargetUrl());
+    $this->assertSame($status, $redirect->getStatusCode());
+  }
+
+  /**
+   * Tests the redirectForm() with redirect_route when a redirect is expected.
+   *
+   * @covers ::redirectForm
+   *
+   * @dataProvider providerTestRedirectWithRouteWithResult
+   */
+  public function testRedirectWithRouteWithResult($form_state, $result, $status = 302) {
+    $container = new ContainerBuilder();
+    $container->set('url_generator', $this->urlGenerator);
+    \Drupal::setContainer($container);
+    $form_submitter = $this->getFormSubmitter();
+    $this->urlGenerator->expects($this->once())
+      ->method('generateFromRoute')
+      ->will($this->returnValueMap(array(
+          array('test_route_a', array(), array('absolute' => TRUE), 'test-route'),
+          array('test_route_b', array('key' => 'value'), array('absolute' => TRUE), 'test-route/value'),
+        ))
+      );
+
+    $form_state += $this->getFormStateDefaults();
+    $redirect = $form_submitter->redirectForm($form_state);
+    $this->assertSame($result, $redirect->getTargetUrl());
+    $this->assertSame($status, $redirect->getStatusCode());
+  }
+
+  /**
+   * Tests the redirectForm() method with a response object.
+   *
+   * @covers ::redirectForm
+   */
+  public function testRedirectWithResponseObject() {
+    $form_submitter = $this->getFormSubmitter();
+    $redirect = new RedirectResponse('/example');
+    $form_state['redirect'] = $redirect;
+
+    $form_state += $this->getFormStateDefaults();
+    $result_redirect = $form_submitter->redirectForm($form_state);
+
+    $this->assertSame($redirect, $result_redirect);
+  }
+
+  /**
+   * Tests the redirectForm() method when no redirect is expected.
+   *
+   * @covers ::redirectForm
+   *
+   * @dataProvider providerTestRedirectWithoutResult
+   */
+  public function testRedirectWithoutResult($form_state) {
+    $form_submitter = $this->getFormSubmitter();
+    $this->urlGenerator->expects($this->never())
+      ->method('generateFromPath');
+    $this->urlGenerator->expects($this->never())
+      ->method('generateFromRoute');
+    $form_state += $this->getFormStateDefaults();
+    $redirect = $form_submitter->redirectForm($form_state);
+    $this->assertNull($redirect);
+  }
+
+  /**
+   * Provides test data for testing the redirectForm() method with a redirect.
+   *
+   * @return array
+   *   Returns some test data.
+   */
+  public function providerTestRedirectWithResult() {
+    return array(
+      array(array(), '<front>'),
+      array(array('redirect' => 'foo'), 'foo'),
+      array(array('redirect' => array('foo')), 'foo'),
+      array(array('redirect' => array('foo')), 'foo'),
+      array(array('redirect' => array('bar', array('query' => array('foo' => 'baz')))), 'bar'),
+      array(array('redirect' => array('baz', array(), 301)), 'baz', 301),
+    );
+  }
+
+  /**
+   * Provides test data for testing the redirectForm() method with a route name.
+   *
+   * @return array
+   *   Returns some test data.
+   */
+  public function providerTestRedirectWithRouteWithResult() {
+    return array(
+      array(array('redirect_route' => array('route_name' => 'test_route_a')), 'test-route'),
+      array(array('redirect_route' => array('route_name' => 'test_route_b', 'route_parameters' => array('key' => 'value'))), 'test-route/value'),
+      array(array('redirect_route' => new Url('test_route_b', array('key' => 'value'))), 'test-route/value'),
+    );
+  }
+
+  /**
+   * Provides test data for testing the redirectForm() method with no redirect.
+   *
+   * @return array
+   *   Returns some test data.
+   */
+  public function providerTestRedirectWithoutResult() {
+    return array(
+      array(array('programmed' => TRUE)),
+      array(array('rebuild' => TRUE)),
+      array(array('no_redirect' => TRUE)),
+      array(array('redirect' => FALSE)),
+    );
+  }
+
+  /**
+   * @covers ::executeSubmitHandlers
+   */
+  public function testExecuteSubmitHandlers() {
+    $form_submitter = $this->getFormSubmitter();
+    $mock = $this->getMock('stdClass', array('submit_handler', 'hash_submit'));
+    $mock->expects($this->once())
+      ->method('submit_handler')
+      ->with($this->isType('array'), $this->isType('array'));
+    $mock->expects($this->once())
+      ->method('hash_submit')
+      ->with($this->isType('array'), $this->isType('array'));
+
+    $form = array();
+    $form_state = $this->getFormStateDefaults();
+    $form_submitter->executeSubmitHandlers($form, $form_state);
+
+    $form['#submit'][] = array($mock, 'hash_submit');
+    $form_submitter->executeSubmitHandlers($form, $form_state);
+
+    // $form_state submit handlers will supersede $form handlers.
+    $form_state['submit_handlers'][] = array($mock, 'submit_handler');
+    $form_submitter->executeSubmitHandlers($form, $form_state);
+  }
+
+  /**
+   * @return array()
+   */
+  protected function getFormStateDefaults() {
+    $form_builder = $this->getMockBuilder('Drupal\Core\Form\FormBuilder')
+      ->disableOriginalConstructor()
+      ->setMethods(NULL)
+      ->getMock();
+    return $form_builder->getFormStateDefaults();
+  }
+
+  /**
+   * @return \Drupal\Core\Form\FormSubmitterInterface
+   */
+  protected function getFormSubmitter() {
+    $request_stack = new RequestStack();
+    $request_stack->push(new Request());
+    return $this->getMockBuilder('Drupal\Core\Form\FormSubmitter')
+      ->setConstructorArgs(array($request_stack, $this->urlGenerator))
+      ->setMethods(array('batchGet', 'drupalInstallationAttempted'))
+      ->getMock();
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php b/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
index 1a8b61fa6bf0..280e54ece6ca 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
@@ -34,6 +34,11 @@ abstract class FormTestBase extends UnitTestCase {
    */
   protected $formValidator;
 
+  /**
+   * @var \Drupal\Core\Form\FormSubmitterInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $formSubmitter;
+
   /**
    * The mocked URL generator.
    *
@@ -83,6 +88,13 @@ abstract class FormTestBase extends UnitTestCase {
    */
   protected $request;
 
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
   /**
    * The event dispatcher.
    *
@@ -117,8 +129,6 @@ public function setUp() {
         array('form_state', $this->formStateCache),
       )));
 
-    $this->eventDispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
-    $this->formValidator = $this->getMock('Drupal\Core\Form\FormValidatorInterface');
     $this->urlGenerator = $this->getMock('Drupal\Core\Routing\UrlGeneratorInterface');
     $this->csrfToken = $this->getMockBuilder('Drupal\Core\Access\CsrfTokenGenerator')
       ->disableOriginalConstructor()
@@ -126,10 +136,22 @@ public function setUp() {
     $this->httpKernel = $this->getMockBuilder('Drupal\Core\HttpKernel')
       ->disableOriginalConstructor()
       ->getMock();
-    $this->request = new Request();
     $this->account = $this->getMock('Drupal\Core\Session\AccountInterface');
+    $this->request = new Request();
+    $this->eventDispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
+    $this->requestStack = new RequestStack();
+    $this->requestStack->push($this->request);
+    $this->formValidator = $this->getMockBuilder('Drupal\Core\Form\FormValidator')
+      ->setConstructorArgs(array($this->requestStack, $this->getStringTranslationStub(), $this->csrfToken))
+      ->setMethods(array('drupalSetMessage'))
+      ->getMock();
+    $this->formSubmitter = $this->getMockBuilder('Drupal\Core\Form\FormSubmitter')
+      ->setConstructorArgs(array($this->requestStack, $this->urlGenerator))
+      ->setMethods(array('batchGet', 'drupalInstallationAttempted'))
+      ->getMock();
 
-    $this->setupFormBuilder();
+    $this->formBuilder = new TestFormBuilder($this->formValidator, $this->formSubmitter, $this->moduleHandler, $this->keyValueExpirableFactory, $this->eventDispatcher, $this->requestStack, $this->csrfToken, $this->httpKernel);
+    $this->formBuilder->setCurrentUser($this->account);
   }
 
   /**
@@ -139,16 +161,6 @@ protected function tearDown() {
     $this->formBuilder->drupalStaticReset();
   }
 
-  /**
-   * Sets up a new form builder object to test.
-   */
-  protected function setupFormBuilder() {
-    $request_stack = new RequestStack();
-    $request_stack->push($this->request);
-    $this->formBuilder = new TestFormBuilder($this->formValidator, $this->moduleHandler, $this->keyValueExpirableFactory, $this->eventDispatcher, $this->urlGenerator, $request_stack, $this->csrfToken, $this->httpKernel);
-    $this->formBuilder->setCurrentUser($this->account);
-  }
-
   /**
    * Provides a mocked form object.
    *
@@ -276,13 +288,6 @@ protected function getElementInfo($type) {
     return $types[$type];
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  protected function drupalInstallationAttempted() {
-    return FALSE;
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -310,14 +315,6 @@ public function drupalStaticReset($name = NULL) {
     static::$seenIds = array();
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  protected function &batchGet() {
-    $batch = array();
-    return $batch;
-  }
-
 }
 
 }
-- 
GitLab