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