Commit 120a1da3 authored by catch's avatar catch

Issue #2257835 by tim.plunkett, sun, Jalandhar: Move form submission logic out...

Issue #2257835 by tim.plunkett, sun, Jalandhar: Move form submission logic out of FormBuilder into a new class.
parent 5f79a75c
......@@ -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']
......
......@@ -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);
......
This diff is collapsed.
......@@ -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.
*
......
<?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();
}
}
<?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);
}
......@@ -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');
......
<?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);
}