Commit f710a6c9 authored by alexpott's avatar alexpott

Issue #2209977 by tim.plunkett: Move form validation logic out of FormBuilder into a new class.

parent 02eb3d3f
......@@ -126,7 +126,10 @@ services:
arguments: [default]
form_builder:
class: Drupal\Core\Form\FormBuilder
arguments: ['@module_handler', '@keyvalue.expirable', '@event_dispatcher', '@url_generator', '@string_translation', '@request_stack', '@?csrf_token', '@?http_kernel']
arguments: ['@form_validator', '@module_handler', '@keyvalue.expirable', '@event_dispatcher', '@url_generator', '@request_stack', '@?csrf_token', '@?http_kernel']
form_validator:
class: Drupal\Core\Form\FormValidator
arguments: ['@request_stack', '@string_translation', '@csrf_token']
keyvalue:
class: Drupal\Core\KeyValueStore\KeyValueFactory
arguments: ['@service_container', '@settings']
......
......@@ -11,6 +11,7 @@
use Drupal\Component\Utility\UrlHelper;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Database\Database;
use Drupal\Core\Form\OptGroup;
use Drupal\Core\Language\Language;
use Drupal\Core\Render\Element;
use Drupal\Core\Template\Attribute;
......@@ -323,7 +324,7 @@ function drupal_prepare_form($form_id, &$form, &$form_state) {
* @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
* Use \Drupal::formBuilder()->validateForm().
*
* @see \Drupal\Core\Form\FormBuilderInterface::validateForm().
* @see \Drupal\Core\Form\FormValidatorInterface::validateForm().
*/
function drupal_validate_form($form_id, &$form, &$form_state) {
\Drupal::formBuilder()->validateForm($form_id, $form, $form_state);
......@@ -345,12 +346,19 @@ function drupal_redirect_form($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 \Drupal::formBuilder()->executeHandlers().
* Use either \Drupal::formBuilder()->executeSubmitHandlers() or
* \Drupal::service('form_validator')->executeValidateHandlers().
*
* @see \Drupal\Core\Form\FormBuilderInterface::executeHandlers().
* @see \Drupal\Core\Form\FormBuilderInterface::executeSubmitHandlers()
* @see \Drupal\Core\Form\FormValidatorInterface::executeValidateHandlers()
*/
function form_execute_handlers($type, &$form, &$form_state) {
\Drupal::formBuilder()->executeHandlers($type, $form, $form_state);
if ($type == 'submit') {
\Drupal::formBuilder()->executeSubmitHandlers($form, $form_state);
}
elseif ($type == 'validate') {
\Drupal::service('form_validator')->executeValidateHandlers($form, $form_state);
}
}
/**
......@@ -359,7 +367,7 @@ function form_execute_handlers($type, &$form, &$form_state) {
* @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
* Use \Drupal::formBuilder()->setErrorByName().
*
* @see \Drupal\Core\Form\FormBuilderInterface::setErrorByName().
* @see \Drupal\Core\Form\FormErrorInterface::setErrorByName().
*/
function form_set_error($name, array &$form_state, $message = '') {
\Drupal::formBuilder()->setErrorByName($name, $form_state, $message);
......@@ -383,7 +391,7 @@ function form_clear_error(array &$form_state) {
* @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
* Use \Drupal::formBuilder()->getErrors().
*
* @see \Drupal\Core\Form\FormBuilderInterface::getErrors().
* @see \Drupal\Core\Form\FormErrorInterface::getErrors()
*/
function form_get_errors(array &$form_state) {
return \Drupal::formBuilder()->getErrors($form_state);
......@@ -395,7 +403,7 @@ function form_get_errors(array &$form_state) {
* @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
* Use \Drupal::formBuilder()->getError().
*
* @see \Drupal\Core\Form\FormBuilderInterface::getError().
* @see \Drupal\Core\Form\FormErrorInterface::getError().
*/
function form_get_error($element, array &$form_state) {
return \Drupal::formBuilder()->getError($element, $form_state);
......@@ -407,7 +415,7 @@ function form_get_error($element, array &$form_state) {
* @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
* Use \Drupal::formBuilder()->setError().
*
* @see \Drupal\Core\Form\FormBuilderInterface::setError().
* @see \Drupal\Core\Form\FormErrorInterface::setError().
*/
function form_error(&$element, array &$form_state, $message = '') {
\Drupal::formBuilder()->setError($element, $form_state, $message);
......@@ -844,12 +852,10 @@ function form_set_value($element, $value, &$form_state) {
* An array with all hierarchical elements flattened to a single array.
*
* @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
* Use \Drupal::formBuilder()->flattenOptions().
*
* @see \Drupal\Core\Form\FormBuilderInterface::flattenOptions().
* Use \Drupal\Core\Form\OptGroup::flattenOptions().
*/
function form_options_flatten($array) {
return \Drupal::formBuilder()->flattenOptions($array);
return OptGroup::flattenOptions($array);
}
/**
......
This diff is collapsed.
......@@ -7,8 +7,6 @@
namespace Drupal\Core\Form;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides an interface for form building and processing.
*/
......@@ -387,33 +385,6 @@ public function processForm($form_id, &$form, &$form_state);
*/
public function prepareForm($form_id, &$form, &$form_state);
/**
* Validates user-submitted form data in the $form_state array.
*
* @param $form_id
* A unique string identifying the form for validation, submission,
* theming, and hook_form_alter functions.
* @param $form
* An associative array containing the structure of the form, which is
* passed by reference. Form validation handlers are able to alter the form
* structure (like #process and #after_build callbacks during form building)
* in case of a validation error. If a validation handler alters the form
* structure, it is responsible for validating the values of changed form
* elements in $form_state['values'] to prevent form submit handlers from
* receiving unvalidated values.
* @param $form_state
* A keyed array containing the current state of the form. The current
* user-submitted data is stored in $form_state['values'], though
* form validation functions are passed an explicit copy of the
* values for the sake of simplicity. Validation handlers can also use
* $form_state to pass information on to submit handlers. For example:
* $form_state['data_for_submission'] = $data;
* This technique is useful when validation requires file parsing,
* web service requests, or other expensive requests that should
* not be repeated in the submission step.
*/
public function validateForm($form_id, &$form, &$form_state);
/**
* Redirects the user to a URL after a form has been processed.
*
......@@ -477,14 +448,11 @@ public function validateForm($form_id, &$form, &$form_state);
public function redirectForm($form_state);
/**
* Executes custom validation and submission handlers for a given form.
* 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 $type
* The type of handler to execute. 'validate' or 'submit' are the
* defaults used by Form API.
* @param $form
* An associative array containing the structure of the form.
* @param $form_state
......@@ -492,7 +460,7 @@ public function redirectForm($form_state);
* submitted the form by clicking a button with custom handler functions
* defined, those handlers will be stored here.
*/
public function executeHandlers($type, &$form, &$form_state);
public function executeSubmitHandlers(&$form, &$form_state);
/**
* Builds and processes all elements in the structured form array.
......@@ -620,19 +588,4 @@ public function doBuildForm($form_id, &$element, &$form_state);
*/
public function setValue($element, $value, &$form_state);
/**
* Allows PHP array processing of multiple select options with the same value.
*
* Used for form select elements which need to validate HTML option groups
* and multiple options which may return the same value. Associative PHP
* arrays cannot handle these structures, since they share a common key.
*
* @param array $array
* The form options array to process.
*
* @return array
* An array with all hierarchical elements flattened to a single array.
*/
public function flattenOptions(array $array);
}
This diff is collapsed.
<?php
/**
* @file
* Contains \Drupal\Core\Form\FormValidatorInterface.
*/
namespace Drupal\Core\Form;
/**
* Provides an interface for validating form submissions.
*/
interface FormValidatorInterface extends FormErrorInterface {
/**
* Executes custom validation 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 executeValidateHandlers(&$form, &$form_state);
/**
* Validates user-submitted form data in the $form_state array.
*
* @param $form_id
* A unique string identifying the form for validation, submission,
* theming, and hook_form_alter functions.
* @param $form
* An associative array containing the structure of the form, which is
* passed by reference. Form validation handlers are able to alter the form
* structure (like #process and #after_build callbacks during form building)
* in case of a validation error. If a validation handler alters the form
* structure, it is responsible for validating the values of changed form
* elements in $form_state['values'] to prevent form submit handlers from
* receiving unvalidated values.
* @param $form_state
* A keyed array containing the current state of the form. The current
* user-submitted data is stored in $form_state['values'], though
* form validation functions are passed an explicit copy of the
* values for the sake of simplicity. Validation handlers can also use
* $form_state to pass information on to submit handlers. For example:
* $form_state['data_for_submission'] = $data;
* This technique is useful when validation requires file parsing,
* web service requests, or other expensive requests that should
* not be repeated in the submission step.
*/
public function validateForm($form_id, &$form, &$form_state);
}
<?php
/**
* @file
* Contains \Drupal\Core\Form\OptGroup.
*/
namespace Drupal\Core\Form;
/**
* Provides helpers for HTML option groups.
*/
class OptGroup {
/**
* Allows PHP array processing of multiple select options with the same value.
*
* Used for form select elements which need to validate HTML option groups
* and multiple options which may return the same value. Associative PHP
* arrays cannot handle these structures, since they share a common key.
*
* @param array $array
* The form options array to process.
*
* @return array
* An array with all hierarchical elements flattened to a single array.
*/
public static function flattenOptions(array $array) {
$options = array();
static::doFlattenOptions($array, $options);
return $options;
}
/**
* Iterates over an array building a flat array with duplicate keys removed.
*
* This function also handles cases where objects are passed as array values.
*
* @param array $array
* The form options array to process.
* @param array $options
* The array of flattened options.
*/
protected static function doFlattenOptions(array $array, array &$options) {
foreach ($array as $key => $value) {
if (is_object($value)) {
static::doFlattenOptions($value->option, $options);
}
elseif (is_array($value)) {
static::doFlattenOptions($value, $options);
}
else {
$options[$key] = 1;
}
}
}
}
......@@ -10,6 +10,7 @@
use Drupal\Component\Utility\String;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Form\OptGroup;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\AllowedValuesInterface;
use Drupal\Core\TypedData\DataDefinition;
......@@ -69,7 +70,7 @@ public function getPossibleOptions(AccountInterface $account = NULL) {
public function getSettableValues(AccountInterface $account = NULL) {
// Flatten options first, because "settable options" may contain group
// arrays.
$flatten_options = \Drupal::formBuilder()->flattenOptions($this->getSettableOptions($account));
$flatten_options = OptGroup::flattenOptions($this->getSettableOptions($account));
return array_keys($flatten_options);
}
......
......@@ -8,6 +8,7 @@
namespace Drupal\options\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Form\OptGroup;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\AllowedValuesInterface;
......@@ -32,7 +33,7 @@ public static function defaultSettings() {
public function getPossibleValues(AccountInterface $account = NULL) {
// Flatten options firstly, because Possible Options may contain group
// arrays.
$flatten_options = \Drupal::formBuilder()->flattenOptions($this->getPossibleOptions($account));
$flatten_options = OptGroup::flattenOptions($this->getPossibleOptions($account));
return array_keys($flatten_options);
}
......@@ -49,7 +50,7 @@ public function getPossibleOptions(AccountInterface $account = NULL) {
public function getSettableValues(AccountInterface $account = NULL) {
// Flatten options firstly, because Settable Options may contain group
// arrays.
$flatten_options = \Drupal::formBuilder()->flattenOptions($this->getSettableOptions($account));
$flatten_options = OptGroup::flattenOptions($this->getSettableOptions($account));
return array_keys($flatten_options);
}
......
......@@ -9,6 +9,7 @@
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Form\OptGroup;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\AllowedValuesInterface;
......@@ -48,7 +49,7 @@ public static function defaultSettings() {
public function getPossibleValues(AccountInterface $account = NULL) {
// Flatten options firstly, because Possible Options may contain group
// arrays.
$flatten_options = \Drupal::formBuilder()->flattenOptions($this->getPossibleOptions($account));
$flatten_options = OptGroup::flattenOptions($this->getPossibleOptions($account));
return array_keys($flatten_options);
}
......@@ -65,7 +66,7 @@ public function getPossibleOptions(AccountInterface $account = NULL) {
public function getSettableValues(AccountInterface $account = NULL) {
// Flatten options firstly, because Settable Options may contain group
// arrays.
$flatten_options = \Drupal::formBuilder()->flattenOptions($this->getSettableOptions($account));
$flatten_options = OptGroup::flattenOptions($this->getSettableOptions($account));
return array_keys($flatten_options);
}
......
......@@ -9,6 +9,7 @@
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Form\OptGroup;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -220,34 +221,6 @@ public function testHandleRedirectWithResponse() {
$this->assertSame($response, $form_state['response']);
}
/**
* Tests that form errors during submission throw an exception.
*
* @covers ::setErrorByName
*
* @expectedException \LogicException
* @expectedExceptionMessage Form errors cannot be set after form validation has finished.
*/
public function testFormErrorsDuringSubmission() {
$form_id = 'test_form_id';
$expected_form = $form_id();
$form_arg = $this->getMockForm($form_id, $expected_form);
$form_builder = $this->formBuilder;
$form_arg->expects($this->any())
->method('submitForm')
->will($this->returnCallback(function ($form, &$form_state) use ($form_builder) {
$form_builder->setErrorByName('test', $form_state, 'Hello');
}));
$form_state = array();
$this->formBuilder->getFormId($form_arg, $form_state);
$form_state['values'] = array();
$form_state['input']['form_id'] = $form_id;
$this->simulateFormSubmission($form_id, $form_arg, $form_state, FALSE);
}
/**
* Tests the redirectForm() method when a redirect is expected.
*
......@@ -499,124 +472,13 @@ public function testRebuildForm() {
$this->assertNotSame($original_build_id, $form['#build_id']);
}
/**
* Tests the submitForm() method.
*/
public function testSubmitForm() {
$form_id = 'test_form_id';
$expected_form = $form_id();
$expected_form['test']['#required'] = TRUE;
$expected_form['options']['#required'] = TRUE;
$expected_form['value']['#required'] = TRUE;
$form_arg = $this->getMock('Drupal\Core\Form\FormInterface');
$form_arg->expects($this->exactly(5))
->method('getFormId')
->will($this->returnValue($form_id));
$form_arg->expects($this->exactly(5))
->method('buildForm')
->will($this->returnValue($expected_form));
$form_state = array();
$form_state['values']['test'] = $this->randomName();
$form_state['values']['op'] = 'Submit';
$this->formBuilder->submitForm($form_arg, $form_state);
$errors = $this->formBuilder->getErrors($form_state);
$this->assertNotEmpty($errors['options']);
$form_state = array();
$form_state['values']['test'] = $this->randomName();
$form_state['values']['options'] = 'foo';
$form_state['values']['op'] = 'Submit';
$this->formBuilder->submitForm($form_arg, $form_state);
$errors = $this->formBuilder->getErrors($form_state);
$this->assertEmpty($errors);
$form_state = array();
$form_state['values']['test'] = $this->randomName();
$form_state['values']['options'] = array('foo');
$form_state['values']['op'] = 'Submit';
$this->formBuilder->submitForm($form_arg, $form_state);
$errors = $this->formBuilder->getErrors($form_state);
$this->assertEmpty($errors);
$form_state = array();
$form_state['values']['test'] = $this->randomName();
$form_state['values']['options'] = array('foo', 'baz');
$form_state['values']['op'] = 'Submit';
$this->formBuilder->submitForm($form_arg, $form_state);
$errors = $this->formBuilder->getErrors($form_state);
$this->assertNotEmpty($errors['options']);
$form_state = array();
$form_state['values']['test'] = $this->randomName();
$form_state['values']['options'] = $this->randomName();
$form_state['values']['op'] = 'Submit';
$this->formBuilder->submitForm($form_arg, $form_state);
$errors = $this->formBuilder->getErrors($form_state);
$this->assertNotEmpty($errors['options']);
}
/**
* Tests the 'must_validate' $form_state flag.
*
* @covers ::validateForm
*/
public function testMustValidate() {
$form_id = 'test_form_id';
$expected_form = $form_id();
$form_arg = $this->getMock('Drupal\Core\Form\FormInterface');
$form_arg->expects($this->any())
->method('getFormId')
->will($this->returnValue($form_id));
$form_arg->expects($this->any())
->method('buildForm')
->will($this->returnValue($expected_form));
$form_builder = $this->formBuilder;
$form_arg->expects($this->exactly(2))
->method('validateForm')
->will($this->returnCallback(function (&$form, &$form_state) use ($form_builder) {
$form_builder->setErrorByName('test', $form_state, 'foo');
}));
$form_state = array();
// This submission will trigger validation.
$this->simulateFormSubmission($form_id, $form_arg, $form_state);
$errors = $this->formBuilder->getErrors($form_state);
$this->assertNotEmpty($errors['test']);
// This submission will not re-trigger validation.
$this->simulateFormSubmission($form_id, $form_arg, $form_state);
$errors = $this->formBuilder->getErrors($form_state);
$this->assertNotEmpty($errors['test']);
// The must_validate flag will re-trigger validation.
$form_state['must_validate'] = TRUE;
$this->simulateFormSubmission($form_id, $form_arg, $form_state);
$errors = $this->formBuilder->getErrors($form_state);
$this->assertNotEmpty($errors['test']);
}
/**
* Tests the flattenOptions() method.
*
* @dataProvider providerTestFlattenOptions
*/
public function testFlattenOptions($options) {
$form_id = 'test_form_id';
$expected_form = $form_id();
$expected_form['select']['#required'] = TRUE;
$expected_form['select']['#options'] = $options;
$form_arg = $this->getMockForm($form_id, $expected_form);
$form_state = array();
$form_state['values']['select'] = 'foo';
$form_state['values']['op'] = 'Submit';
$this->formBuilder->submitForm($form_arg, $form_state);
$errors = $this->formBuilder->getErrors($form_state);
$this->assertEmpty($errors);
$this->assertSame(array('foo' => 1), OptGroup::flattenOptions($options));
}
/**
......@@ -625,100 +487,15 @@ public function testFlattenOptions($options) {
* @return array
*/
public function providerTestFlattenOptions() {
$object = new \stdClass();
$object->option = array('foo' => 'foo');
$object1 = new \stdClass();
$object1->option = array('foo' => 'foo');
$object2 = new \stdClass();
$object2->option = array(array('foo' => 'foo'), array('foo' => 'foo'));
return array(
array(array('foo' => 'foo')),
array(array(array('foo' => 'foo'))),
array(array($object)),
);
}
/**
* Tests the setErrorByName() method.
*
* @param array|null $limit_validation_errors
* The errors to limit validation for, NULL will run all validation.
* @param array $expected_errors
* The errors expected to be set.
*
* @dataProvider providerTestSetErrorByName
*/
public function testSetErrorByName($limit_validation_errors, $expected_errors) {
$form_id = 'test_form_id';
$expected_form = $form_id();
$expected_form['actions']['submit']['#submit'][] = 'test_form_id_custom_submit';
$expected_form['actions']['submit']['#limit_validation_errors'] = $limit_validation_errors;
$form_arg = $this->getMockForm($form_id, $expected_form);
$form_builder = $this->formBuilder;
$form_arg->expects($this->once())
->method('validateForm')
->will($this->returnCallback(function (array &$form, array &$form_state) use ($form_builder) {
$form_builder->setErrorByName('test', $form_state, 'Fail 1');
$form_builder->setErrorByName('test', $form_state, 'Fail 2');
$form_builder->setErrorByName('options', $form_state);
}));
$form_state = array();
$form_state['values']['test'] = $this->randomName();
$form_state['values']['options'] = 'foo';
$form_state['values']['op'] = 'Submit';
$this->formBuilder->submitForm($form_arg, $form_state);
$errors = $this->formBuilder->getErrors($form_state);
$this->assertSame($expected_errors, $errors);
}
/**
* Provides test data for testing the setErrorByName() method.
*
* @return array