From 03fc20425b8a022bedbc431b963145dcc64a6fb5 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org> Date: Mon, 1 Aug 2016 18:40:43 +0100 Subject: [PATCH] Issue #2537732 by Xano, tim.plunkett, gnuget, claudiu.cristea, drunken monkey, bojanz: PluginFormInterface must have access to the complete $form_state (introduce SubFormState for embedded forms) --- .../Core/Condition/ConditionPluginBase.php | 7 + core/lib/Drupal/Core/Form/FormState.php | 66 +- .../Core/Form/FormStateDecoratorBase.php | 749 ++++++++ .../Drupal/Core/Form/FormStateValuesTrait.php | 83 + core/lib/Drupal/Core/Form/SubformState.php | 154 ++ .../Core/Form/SubformStateInterface.php | 51 + .../Core/Plugin/PluginFormInterface.php | 12 +- core/modules/block/src/BlockForm.php | 41 +- core/modules/block/src/Tests/BlockUiTest.php | 16 + .../Block/TestSettingsValidationBlock.php | 41 + .../image/src/Form/ImageEffectFormBase.php | 18 +- .../image/src/Tests/ImageEffectsTest.php | 21 + .../Core/Form/FormStateDecoratorBaseTest.php | 1563 +++++++++++++++++ .../Drupal/Tests/Core/Form/FormStateTest.php | 178 +- .../Core/Form/FormStateValuesTraitTest.php | 263 +++ .../Tests/Core/Form/SubformStateTest.php | 312 ++++ 16 files changed, 3310 insertions(+), 265 deletions(-) create mode 100644 core/lib/Drupal/Core/Form/FormStateDecoratorBase.php create mode 100644 core/lib/Drupal/Core/Form/FormStateValuesTrait.php create mode 100644 core/lib/Drupal/Core/Form/SubformState.php create mode 100644 core/lib/Drupal/Core/Form/SubformStateInterface.php create mode 100644 core/modules/block/tests/modules/block_test/src/Plugin/Block/TestSettingsValidationBlock.php create mode 100644 core/tests/Drupal/Tests/Core/Form/FormStateDecoratorBaseTest.php create mode 100644 core/tests/Drupal/Tests/Core/Form/FormStateValuesTraitTest.php create mode 100644 core/tests/Drupal/Tests/Core/Form/SubformStateTest.php diff --git a/core/lib/Drupal/Core/Condition/ConditionPluginBase.php b/core/lib/Drupal/Core/Condition/ConditionPluginBase.php index 37b08ca67799..aaa7ee817c8f 100644 --- a/core/lib/Drupal/Core/Condition/ConditionPluginBase.php +++ b/core/lib/Drupal/Core/Condition/ConditionPluginBase.php @@ -5,6 +5,7 @@ use Drupal\Core\Executable\ExecutableManagerInterface; use Drupal\Core\Executable\ExecutablePluginBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Form\SubformStateInterface; use Drupal\Core\Plugin\ContextAwarePluginAssignmentTrait; /** @@ -47,6 +48,9 @@ public function isNegated() { * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + if ($form_state instanceof SubformStateInterface) { + $form_state = $form_state->getCompleteFormState(); + } $contexts = $form_state->getTemporaryValue('gathered_contexts') ?: []; $form['context_mapping'] = $this->addContextAssignmentElement($this, $contexts); $form['negate'] = array( @@ -68,6 +72,9 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form */ public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { $this->configuration['negate'] = $form_state->getValue('negate'); + if ($form_state->hasValue('context_mapping')) { + $this->setContextMapping($form_state->getValue('context_mapping')); + } } /** diff --git a/core/lib/Drupal/Core/Form/FormState.php b/core/lib/Drupal/Core/Form/FormState.php index 08323dd59d43..ca7355dceb61 100644 --- a/core/lib/Drupal/Core/Form/FormState.php +++ b/core/lib/Drupal/Core/Form/FormState.php @@ -11,6 +11,8 @@ */ class FormState implements FormStateInterface { + use FormStateValuesTrait; + /** * Tracks if any errors have been set on any form. * @@ -246,7 +248,8 @@ class FormState implements FormStateInterface { * * This property is uncacheable. * - * @var array + * @var array|null + * The submitted user input array, or NULL if no input was submitted yet. */ protected $input; @@ -977,67 +980,6 @@ public function &getValues() { return $this->values; } - /** - * {@inheritdoc} - */ - public function &getValue($key, $default = NULL) { - $exists = NULL; - $value = &NestedArray::getValue($this->getValues(), (array) $key, $exists); - if (!$exists) { - $value = $default; - } - return $value; - } - - /** - * {@inheritdoc} - */ - public function setValues(array $values) { - $this->values = $values; - return $this; - } - - /** - * {@inheritdoc} - */ - public function setValue($key, $value) { - NestedArray::setValue($this->getValues(), (array) $key, $value, TRUE); - return $this; - } - - /** - * {@inheritdoc} - */ - public function unsetValue($key) { - NestedArray::unsetValue($this->getValues(), (array) $key); - return $this; - } - - /** - * {@inheritdoc} - */ - public function hasValue($key) { - $exists = NULL; - $value = NestedArray::getValue($this->getValues(), (array) $key, $exists); - return $exists && isset($value); - } - - /** - * {@inheritdoc} - */ - public function isValueEmpty($key) { - $exists = NULL; - $value = NestedArray::getValue($this->getValues(), (array) $key, $exists); - return !$exists || empty($value); - } - - /** - * {@inheritdoc} - */ - public function setValueForElement(array $element, $value) { - return $this->setValue($element['#parents'], $value); - } - /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Form/FormStateDecoratorBase.php b/core/lib/Drupal/Core/Form/FormStateDecoratorBase.php new file mode 100644 index 000000000000..c06366646449 --- /dev/null +++ b/core/lib/Drupal/Core/Form/FormStateDecoratorBase.php @@ -0,0 +1,749 @@ +<?php + +namespace Drupal\Core\Form; + +use Drupal\Core\Url; +use Symfony\Component\HttpFoundation\Response; + +/** + * Decorates another form state. + */ +abstract class FormStateDecoratorBase implements FormStateInterface { + + /** + * The decorated form state. + * + * @var \Drupal\Core\Form\FormStateInterface + */ + protected $decoratedFormState; + + /** + * {@inheritdoc} + */ + public function setFormState(array $form_state_additions) { + $this->decoratedFormState->setFormState($form_state_additions); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setAlwaysProcess($always_process = TRUE) { + $this->decoratedFormState->setAlwaysProcess($always_process); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getAlwaysProcess() { + return $this->decoratedFormState->getAlwaysProcess(); + } + + /** + * {@inheritdoc} + */ + public function setButtons(array $buttons) { + $this->decoratedFormState->setButtons($buttons); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getButtons() { + return $this->decoratedFormState->getButtons(); + } + + /** + * {@inheritdoc} + */ + public function setCached($cache = TRUE) { + $this->decoratedFormState->setCached($cache); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function isCached() { + return $this->decoratedFormState->isCached(); + } + + /** + * {@inheritdoc} + */ + public function disableCache() { + $this->decoratedFormState->disableCache(); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setExecuted() { + $this->decoratedFormState->setExecuted(); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function isExecuted() { + return $this->decoratedFormState->isExecuted(); + } + + /** + * {@inheritdoc} + */ + public function setGroups(array $groups) { + $this->decoratedFormState->setGroups($groups); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function &getGroups() { + return $this->decoratedFormState->getGroups(); + } + + /** + * {@inheritdoc} + */ + public function setHasFileElement($has_file_element = TRUE) { + $this->decoratedFormState->setHasFileElement($has_file_element); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function hasFileElement() { + return $this->decoratedFormState->hasFileElement(); + } + + /** + * {@inheritdoc} + */ + public function setLimitValidationErrors($limit_validation_errors) { + $this->decoratedFormState->setLimitValidationErrors($limit_validation_errors); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getLimitValidationErrors() { + return $this->decoratedFormState->getLimitValidationErrors(); + } + + /** + * {@inheritdoc} + */ + public function setMethod($method) { + $this->decoratedFormState->setMethod($method); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function isMethodType($method_type) { + return $this->decoratedFormState->isMethodType($method_type); + } + + /** + * {@inheritdoc} + */ + public function setRequestMethod($method) { + $this->decoratedFormState->setRequestMethod($method); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setValidationEnforced($must_validate = TRUE) { + $this->decoratedFormState->setValidationEnforced($must_validate); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function isValidationEnforced() { + return $this->decoratedFormState->isValidationEnforced(); + } + + /** + * {@inheritdoc} + */ + public function disableRedirect($no_redirect = TRUE) { + $this->decoratedFormState->disableRedirect($no_redirect); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function isRedirectDisabled() { + return $this->decoratedFormState->isRedirectDisabled(); + } + + /** + * {@inheritdoc} + */ + public function setProcessInput($process_input = TRUE) { + $this->decoratedFormState->setProcessInput($process_input); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function isProcessingInput() { + return $this->decoratedFormState->isProcessingInput(); + } + + /** + * {@inheritdoc} + */ + public function setProgrammed($programmed = TRUE) { + $this->decoratedFormState->setProgrammed($programmed); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function isProgrammed() { + return $this->decoratedFormState->isProgrammed(); + } + + /** + * {@inheritdoc} + */ + public function setProgrammedBypassAccessCheck($programmed_bypass_access_check = TRUE) { + $this->decoratedFormState->setProgrammedBypassAccessCheck($programmed_bypass_access_check); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function isBypassingProgrammedAccessChecks() { + return $this->decoratedFormState->isBypassingProgrammedAccessChecks(); + } + + /** + * {@inheritdoc} + */ + public function setRebuildInfo(array $rebuild_info) { + $this->decoratedFormState->setRebuildInfo($rebuild_info); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getRebuildInfo() { + return $this->decoratedFormState->getRebuildInfo(); + } + + /** + * {@inheritdoc} + */ + public function addRebuildInfo($property, $value) { + $this->decoratedFormState->addRebuildInfo($property, $value); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setStorage(array $storage) { + $this->decoratedFormState->setStorage($storage); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function &getStorage() { + return $this->decoratedFormState->getStorage(); + } + + /** + * {@inheritdoc} + */ + public function setSubmitHandlers(array $submit_handlers) { + $this->decoratedFormState->setSubmitHandlers($submit_handlers); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getSubmitHandlers() { + return $this->decoratedFormState->getSubmitHandlers(); + } + + /** + * {@inheritdoc} + */ + public function setSubmitted() { + $this->decoratedFormState->setSubmitted(); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function isSubmitted() { + return $this->decoratedFormState->isSubmitted(); + } + + /** + * {@inheritdoc} + */ + public function setTemporary(array $temporary) { + $this->decoratedFormState->setTemporary($temporary); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getTemporary() { + return $this->decoratedFormState->getTemporary(); + } + + /** + * {@inheritdoc} + */ + public function &getTemporaryValue($key) { + return $this->decoratedFormState->getTemporaryValue($key); + } + + /** + * {@inheritdoc} + */ + public function setTemporaryValue($key, $value) { + $this->decoratedFormState->setTemporaryValue($key, $value); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function hasTemporaryValue($key) { + return $this->decoratedFormState->hasTemporaryValue($key); + } + + /** + * {@inheritdoc} + */ + public function setTriggeringElement($triggering_element) { + $this->decoratedFormState->setTriggeringElement($triggering_element); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function &getTriggeringElement() { + return $this->decoratedFormState->getTriggeringElement(); + } + + /** + * {@inheritdoc} + */ + public function setValidateHandlers(array $validate_handlers) { + $this->decoratedFormState->setValidateHandlers($validate_handlers); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getValidateHandlers() { + return $this->decoratedFormState->getValidateHandlers(); + } + + /** + * {@inheritdoc} + */ + public function setValidationComplete($validation_complete = TRUE) { + $this->decoratedFormState->setValidationComplete($validation_complete); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function isValidationComplete() { + return $this->decoratedFormState->isValidationComplete(); + } + + /** + * {@inheritdoc} + */ + public function loadInclude($module, $type, $name = NULL) { + return $this->decoratedFormState->loadInclude($module, $type, $name); + } + + /** + * {@inheritdoc} + */ + public function getCacheableArray() { + return $this->decoratedFormState->getCacheableArray(); + } + + /** + * {@inheritdoc} + */ + public function setCompleteForm(array &$complete_form) { + $this->decoratedFormState->setCompleteForm($complete_form); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function &getCompleteForm() { + return $this->decoratedFormState->getCompleteForm(); + } + + /** + * {@inheritdoc} + */ + public function &get($property) { + return $this->decoratedFormState->get($property); + } + + /** + * {@inheritdoc} + */ + public function set($property, $value) { + $this->decoratedFormState->set($property, $value); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function has($property) { + return $this->decoratedFormState->has($property); + } + + /** + * {@inheritdoc} + */ + public function setBuildInfo(array $build_info) { + $this->decoratedFormState->setBuildInfo($build_info); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getBuildInfo() { + return $this->decoratedFormState->getBuildInfo(); + } + + /** + * {@inheritdoc} + */ + public function addBuildInfo($property, $value) { + $this->decoratedFormState->addBuildInfo($property, $value); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function &getUserInput() { + return $this->decoratedFormState->getUserInput(); + } + + /** + * {@inheritdoc} + */ + public function setUserInput(array $user_input) { + $this->decoratedFormState->setUserInput($user_input); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function &getValues() { + return $this->decoratedFormState->getValues(); + } + + /** + * {@inheritdoc} + */ + public function &getValue($key, $default = NULL) { + return $this->decoratedFormState->getValue($key, $default); + } + + /** + * {@inheritdoc} + */ + public function setValues(array $values) { + $this->decoratedFormState->setValues($values); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setValue($key, $value) { + $this->decoratedFormState->setValue($key, $value); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function unsetValue($key) { + $this->decoratedFormState->unsetValue($key); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function hasValue($key) { + return $this->decoratedFormState->hasValue($key); + } + + /** + * {@inheritdoc} + */ + public function isValueEmpty($key) { + return $this->decoratedFormState->isValueEmpty($key); + } + + /** + * {@inheritdoc} + */ + public function setValueForElement(array $element, $value) { + $this->decoratedFormState->setValueForElement($element, $value); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setResponse(Response $response) { + $this->decoratedFormState->setResponse($response); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getResponse() { + return $this->decoratedFormState->getResponse(); + } + + /** + * {@inheritdoc} + */ + public function setRedirect($route_name, array $route_parameters = [], array $options = []) { + $this->decoratedFormState->setRedirect($route_name, $route_parameters, $options); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setRedirectUrl(Url $url) { + $this->decoratedFormState->setRedirectUrl($url); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getRedirect() { + return $this->decoratedFormState->getRedirect(); + } + + /** + * {@inheritdoc} + */ + public static function hasAnyErrors() { + return FormState::hasAnyErrors(); + } + + /** + * {@inheritdoc} + */ + public function setErrorByName($name, $message = '') { + $this->decoratedFormState->setErrorByName($name, $message); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setError(array &$element, $message = '') { + $this->decoratedFormState->setError($element, $message); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function clearErrors() { + $this->decoratedFormState->clearErrors(); + } + + /** + * {@inheritdoc} + */ + public function getError(array $element) { + return $this->decoratedFormState->getError($element); + } + + /** + * {@inheritdoc} + */ + public function getErrors() { + return $this->decoratedFormState->getErrors(); + } + + /** + * {@inheritdoc} + */ + public function setRebuild($rebuild = TRUE) { + $this->decoratedFormState->setRebuild($rebuild); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function isRebuilding() { + return $this->decoratedFormState->isRebuilding(); + } + + /** + * {@inheritdoc} + */ + public function setInvalidToken($invalid_token) { + $this->decoratedFormState->setInvalidToken($invalid_token); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function hasInvalidToken() { + return $this->decoratedFormState->hasInvalidToken(); + } + + /** + * {@inheritdoc} + */ + public function prepareCallback($callback) { + return $this->decoratedFormState->prepareCallback($callback); + } + + /** + * {@inheritdoc} + */ + public function setFormObject(FormInterface $form_object) { + $this->decoratedFormState->setFormObject($form_object); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getFormObject() { + return $this->decoratedFormState->getFormObject(); + } + + /** + * {@inheritdoc} + */ + public function getCleanValueKeys() { + return $this->decoratedFormState->getCleanValueKeys(); + } + + /** + * {@inheritdoc} + */ + public function setCleanValueKeys(array $cleanValueKeys) { + $this->decoratedFormState->setCleanValueKeys($cleanValueKeys); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function addCleanValueKey($cleanValueKey) { + $this->decoratedFormState->addCleanValueKey($cleanValueKey); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function cleanValues() { + $this->decoratedFormState->cleanValues(); + + return $this; + } + +} diff --git a/core/lib/Drupal/Core/Form/FormStateValuesTrait.php b/core/lib/Drupal/Core/Form/FormStateValuesTrait.php new file mode 100644 index 000000000000..87f7d465550a --- /dev/null +++ b/core/lib/Drupal/Core/Form/FormStateValuesTrait.php @@ -0,0 +1,83 @@ +<?php + +namespace Drupal\Core\Form; + +use Drupal\Component\Utility\NestedArray; + +/** + * Provides methods to manage form state values. + * + * @see \Drupal\Core\Form\FormStateInterface + * + * @ingroup form_api + */ +trait FormStateValuesTrait { + + /** + * Implements \Drupal\Core\Form\FormStateInterface::getValues() + */ + abstract public function &getValues(); + + /** + * Implements \Drupal\Core\Form\FormStateInterface::getValue() + */ + public function &getValue($key, $default = NULL) { + $exists = NULL; + $value = &NestedArray::getValue($this->getValues(), (array) $key, $exists); + if (!$exists) { + $value = $default; + } + return $value; + } + + /** + * Implements \Drupal\Core\Form\FormStateInterface::setValues() + */ + public function setValues(array $values) { + $existing_values = &$this->getValues(); + $existing_values = $values; + return $this; + } + + /** + * Implements \Drupal\Core\Form\FormStateInterface::setValue() + */ + public function setValue($key, $value) { + NestedArray::setValue($this->getValues(), (array) $key, $value, TRUE); + return $this; + } + + /** + * Implements \Drupal\Core\Form\FormStateInterface::unsetValue() + */ + public function unsetValue($key) { + NestedArray::unsetValue($this->getValues(), (array) $key); + return $this; + } + + /** + * Implements \Drupal\Core\Form\FormStateInterface::hasValue() + */ + public function hasValue($key) { + $exists = NULL; + $value = NestedArray::getValue($this->getValues(), (array) $key, $exists); + return $exists && isset($value); + } + + /** + * Implements \Drupal\Core\Form\FormStateInterface::isValueEmpty() + */ + public function isValueEmpty($key) { + $exists = NULL; + $value = NestedArray::getValue($this->getValues(), (array) $key, $exists); + return !$exists || empty($value); + } + + /** + * Implements \Drupal\Core\Form\FormStateInterface::setValueForElement() + */ + public function setValueForElement(array $element, $value) { + return $this->setValue($element['#parents'], $value); + } + +} diff --git a/core/lib/Drupal/Core/Form/SubformState.php b/core/lib/Drupal/Core/Form/SubformState.php new file mode 100644 index 000000000000..50f39117c258 --- /dev/null +++ b/core/lib/Drupal/Core/Form/SubformState.php @@ -0,0 +1,154 @@ +<?php + +namespace Drupal\Core\Form; + +use Drupal\Component\Utility\NestedArray; + +/** + * Stores information about the state of a subform. + */ +class SubformState extends FormStateDecoratorBase implements SubformStateInterface { + + use FormStateValuesTrait; + + /** + * The parent form. + * + * @var mixed[] + */ + protected $parentForm; + + /** + * The subform. + * + * @var mixed[] + */ + protected $subform; + + /** + * Constructs a new instance. + * + * @param mixed[] $subform + * The subform for which to create a form state. + * @param mixed[] $parent_form + * The subform's parent form. + * @param \Drupal\Core\Form\FormStateInterface $parent_form_state + * The parent form state. + */ + protected function __construct(array &$subform, array &$parent_form, FormStateInterface $parent_form_state) { + $this->decoratedFormState = $parent_form_state; + $this->parentForm = $parent_form; + $this->subform = $subform; + } + + /** + * Creates a new instance for a subform. + * + * @param mixed[] $subform + * The subform for which to create a form state. + * @param mixed[] $parent_form + * The subform's parent form. + * @param \Drupal\Core\Form\FormStateInterface $parent_form_state + * The parent form state. + * + * @return static + */ + public static function createForSubform(array &$subform, array &$parent_form, FormStateInterface $parent_form_state) { + return new static($subform, $parent_form, $parent_form_state); + } + + /** + * Gets the subform's parents relative to its parent form. + * + * @param string $property + * The property name (#parents or #array_parents). + * + * @return mixed + * + * @throws \InvalidArgumentException + * Thrown when the requested property does not exist. + * @throws \UnexpectedValueException + * Thrown when the subform is not contained by the given parent form. + */ + protected function getParents($property) { + foreach ([$this->subform, $this->parentForm] as $form) { + if (!isset($form[$property]) || !is_array($form[$property])) { + throw new \RuntimeException(sprintf('The subform and parent form must contain the %s property, which must be an array. Try calling this method from a #process callback instead.', $property)); + } + } + + $relative_subform_parents = $this->subform[$property]; + // Remove all of the subform's parents that are also the parent form's + // parents, so we are left with the parents relative to the parent form. + foreach ($this->parentForm[$property] as $parent_form_parent) { + if ($parent_form_parent !== $relative_subform_parents[0]) { + // The parent form's parents are the subform's parents as well. If we + // find no match, that means the given subform is not contained by the + // given parent form. + throw new \UnexpectedValueException('The subform is not contained by the given parent form.'); + } + array_shift($relative_subform_parents); + } + + return $relative_subform_parents; + } + + /** + * {@inheritdoc} + */ + public function &getValues() { + $exists = NULL; + $values = &NestedArray::getValue(parent::getValues(), $this->getParents('#parents'), $exists); + if (!$exists) { + $values = []; + } + elseif (!is_array($values)) { + throw new \UnexpectedValueException('The form state values do not belong to the subform.'); + } + + return $values; + } + + /** + * {@inheritdoc} + */ + public function getCompleteFormState() { + return $this->decoratedFormState instanceof SubformStateInterface ? $this->decoratedFormState->getCompleteFormState() : $this->decoratedFormState; + } + + /** + * {@inheritdoc} + */ + public function setLimitValidationErrors($limit_validation_errors) { + if (is_array($limit_validation_errors)) { + $limit_validation_errors = array_merge($this->getParents('#parents'), $limit_validation_errors); + } + + return parent::setLimitValidationErrors($limit_validation_errors); + } + + /** + * {@inheritdoc} + */ + public function getLimitValidationErrors() { + $limit_validation_errors = parent::getLimitValidationErrors(); + if (is_array($limit_validation_errors)) { + return array_slice($limit_validation_errors, count($this->getParents('#parents'))); + + } + return $limit_validation_errors; + } + + /** + * {@inheritdoc} + */ + public function setErrorByName($name, $message = '') { + $parents = $this->subform['#array_parents']; + $parents[] = $name; + $name = implode('][', $parents); + parent::setErrorByName($name, $message); + + return $this; + } + +} diff --git a/core/lib/Drupal/Core/Form/SubformStateInterface.php b/core/lib/Drupal/Core/Form/SubformStateInterface.php new file mode 100644 index 000000000000..99bef57d256a --- /dev/null +++ b/core/lib/Drupal/Core/Form/SubformStateInterface.php @@ -0,0 +1,51 @@ +<?php + +namespace Drupal\Core\Form; + +/** + * Stores information about the state of a subform. + * + * In the context of Drupal's Form API, a subform is a form definition array + * that will be nested into a "parent" form. For instance: + * + * @code + * $subform = [ + * 'method' => [ + * '#type' => 'select', + * // … + * ], + * ]; + * $form = [ + * // … + * 'settings' => $subform, + * ]; + * @endcode + * + * All input fields nested under "settings" are then considered part of that + * "subform". The concept is used mostly when the subform is defined by a + * different class (potentially even in a different module) than the parent + * form. This is often the case for plugins: a plugin's buildConfigurationForm() + * would then be handed an instance of this interface as the second parameter. + * + * The benefit of doing this is that the plugin can then just define the form – + * and use the form state – as if it would define a "proper" form, not nested in + * some other form structure. This means that it won't have to know the key(s) + * under which its form structure will be nested – for instance, when retrieving + * the form values during form validation or submission. + * + * Contrary to "proper" forms, subforms don't translate to a <form> tag in the + * HTML response. Instead, they can only be discerned in the HTML code by the + * nesting of the input tags' names. + * + * @see \Drupal\Core\Plugin\PluginFormInterface::buildConfigurationForm() + */ +interface SubformStateInterface extends FormStateInterface { + + /** + * Gets the complete form state. + * + * @return \Drupal\Core\Form\FormStateInterface + */ + public function getCompleteFormState(); + +} diff --git a/core/lib/Drupal/Core/Plugin/PluginFormInterface.php b/core/lib/Drupal/Core/Plugin/PluginFormInterface.php index 397c98f0863d..27a3192e0360 100644 --- a/core/lib/Drupal/Core/Plugin/PluginFormInterface.php +++ b/core/lib/Drupal/Core/Plugin/PluginFormInterface.php @@ -31,7 +31,9 @@ interface PluginFormInterface { * @param array $form * An associative array containing the initial structure of the plugin form. * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the complete form. + * The current state of the form. Calling code should pass on a subform + * state created through + * \Drupal\Core\Form\SubformState::createForSubform(). * * @return array * The form structure. @@ -45,7 +47,9 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta * An associative array containing the structure of the plugin form as built * by static::buildConfigurationForm(). * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the complete form. + * The current state of the form. Calling code should pass on a subform + * state created through + * \Drupal\Core\Form\SubformState::createForSubform(). */ public function validateConfigurationForm(array &$form, FormStateInterface $form_state); @@ -56,7 +60,9 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form * An associative array containing the structure of the plugin form as built * by static::buildConfigurationForm(). * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the complete form. + * The current state of the form. Calling code should pass on a subform + * state created through + * \Drupal\Core\Form\SubformState::createForSubform(). */ public function submitConfigurationForm(array &$form, FormStateInterface $form_state); diff --git a/core/modules/block/src/BlockForm.php b/core/modules/block/src/BlockForm.php index e343c033462b..1b1f4278fec0 100644 --- a/core/modules/block/src/BlockForm.php +++ b/core/modules/block/src/BlockForm.php @@ -9,8 +9,8 @@ use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Executable\ExecutableManagerInterface; use Drupal\Core\Extension\ThemeHandlerInterface; -use Drupal\Core\Form\FormState; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Form\SubformState; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Plugin\ContextAwarePluginInterface; use Drupal\Core\Plugin\Context\ContextRepositoryInterface; @@ -134,7 +134,9 @@ public function form(array $form, FormStateInterface $form_state) { $form_state->setTemporaryValue('gathered_contexts', $this->contextRepository->getAvailableContexts()); $form['#tree'] = TRUE; - $form['settings'] = $this->getPluginForm($entity->getPlugin())->buildConfigurationForm(array(), $form_state); + $form['settings'] = []; + $subform_state = SubformState::createForSubform($form['settings'], $form, $form_state); + $form['settings'] = $this->getPluginForm($entity->getPlugin())->buildConfigurationForm($form['settings'], $subform_state); $form['visibility'] = $this->buildVisibilityInterface([], $form_state); // If creating a new block, calculate a safe default machine name. @@ -294,11 +296,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) { // The Block Entity form puts all block plugin form elements in the // settings form element, so just pass that to the block for validation. - $settings = (new FormState())->setValues($form_state->getValue('settings')); - // Call the plugin validate handler. - $this->getPluginForm($this->entity->getPlugin())->validateConfigurationForm($form, $settings); - // Update the original form values. - $form_state->setValue('settings', $settings->getValues()); + $this->getPluginForm($this->entity->getPlugin())->validateConfigurationForm($form['settings'], SubformState::createForSubform($form['settings'], $form, $form_state)); $this->validateVisibility($form, $form_state); } @@ -322,11 +320,7 @@ protected function validateVisibility(array $form, FormStateInterface $form_stat // Allow the condition to validate the form. $condition = $form_state->get(['conditions', $condition_id]); - $condition_values = (new FormState()) - ->setValues($values); - $condition->validateConfigurationForm($form, $condition_values); - // Update the original form values. - $form_state->setValue(['visibility', $condition_id], $condition_values->getValues()); + $condition->validateConfigurationForm($form['visibility'][$condition_id], SubformState::createForSubform($form['visibility'][$condition_id], $form, $form_state)); } } @@ -339,19 +333,15 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $entity = $this->entity; // The Block Entity form puts all block plugin form elements in the // settings form element, so just pass that to the block for submission. - // @todo Find a way to avoid this manipulation. - $settings = (new FormState())->setValues($form_state->getValue('settings')); - + $sub_form_state = SubformState::createForSubform($form['settings'], $form, $form_state); // Call the plugin submit handler. $block = $entity->getPlugin(); - $this->getPluginForm($block)->submitConfigurationForm($form, $settings); + $this->getPluginForm($block)->submitConfigurationForm($form, $sub_form_state); // If this block is context-aware, set the context mapping. if ($block instanceof ContextAwarePluginInterface && $block->getContextDefinitions()) { - $context_mapping = $settings->getValue('context_mapping', []); + $context_mapping = $sub_form_state->getValue('context_mapping', []); $block->setContextMapping($context_mapping); } - // Update the original form values. - $form_state->setValue('settings', $settings->getValues()); $this->submitVisibility($form, $form_state); @@ -380,16 +370,19 @@ protected function submitVisibility(array $form, FormStateInterface $form_state) foreach ($form_state->getValue('visibility') as $condition_id => $values) { // Allow the condition to submit the form. $condition = $form_state->get(['conditions', $condition_id]); - $condition_values = (new FormState()) - ->setValues($values); - $condition->submitConfigurationForm($form, $condition_values); + $condition->submitConfigurationForm($form['visibility'][$condition_id], SubformState::createForSubform($form['visibility'][$condition_id], $form, $form_state)); + + // Setting conditions' context mappings is the plugins' responsibility. + // This code exists for backwards compatibility, because + // \Drupal\Core\Condition\ConditionPluginBase::submitConfigurationForm() + // did not set its own mappings until Drupal 8.2 + // @todo Remove the code that sets context mappings in Drupal 9.0.0. if ($condition instanceof ContextAwarePluginInterface) { $context_mapping = isset($values['context_mapping']) ? $values['context_mapping'] : []; $condition->setContextMapping($context_mapping); } - // Update the original form values. + $condition_configuration = $condition->getConfiguration(); - $form_state->setValue(['visibility', $condition_id], $condition_configuration); // Update the visibility conditions on the block. $this->entity->getVisibilityConditions()->addInstanceId($condition_id, $condition_configuration); } diff --git a/core/modules/block/src/Tests/BlockUiTest.php b/core/modules/block/src/Tests/BlockUiTest.php index 5ea62cd3ae72..ec720e9c921a 100644 --- a/core/modules/block/src/Tests/BlockUiTest.php +++ b/core/modules/block/src/Tests/BlockUiTest.php @@ -288,4 +288,20 @@ public function testBlockPlacementIndicator() { $this->assertUrl('admin/structure/block/list/classy'); } + /** + * Tests if validation errors are passed plugin form to the parent form. + */ + public function testBlockValidateErrors() { + $this->drupalPostForm('admin/structure/block/add/test_settings_validation/classy', ['settings[digits]' => 'abc'], t('Save block')); + + $arguments = [':message' => 'Only digits are allowed']; + $pattern = '//div[contains(@class,"messages messages--error")]/div[contains(text()[2],:message)]'; + $elements = $this->xpath($pattern, $arguments); + $this->assertTrue($elements, 'Plugin error message found in parent form.'); + + $error_class_pattern = '//div[contains(@class,"form-item-settings-digits")]/input[contains(@class,"error")]'; + $error_class = $this->xpath($error_class_pattern); + $this->assertTrue($error_class, 'Plugin error class found in parent form.'); + } + } diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestSettingsValidationBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestSettingsValidationBlock.php new file mode 100644 index 000000000000..d1f16d7fda40 --- /dev/null +++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestSettingsValidationBlock.php @@ -0,0 +1,41 @@ +<?php + +namespace Drupal\block_test\Plugin\Block; + +use Drupal\Core\Block\BlockBase; +use Drupal\Core\Form\FormStateInterface; + +/** + * Provides a test settings validation block. + * + * @Block( + * id = "test_settings_validation", + * admin_label = @Translation("Test settings validation block"), + * ) + */ +class TestSettingsValidationBlock extends BlockBase { + + /** + * {@inheritdoc} + */ + public function blockForm($form, FormStateInterface $form_state) { + return ['digits' => ['#type' => 'textfield']] + $form; + } + + /** + * {@inheritdoc} + */ + public function blockValidate($form, FormStateInterface $form_state) { + if (!ctype_digit($form_state->getValue('digits'))) { + $form_state->setErrorByName('digits', $this->t('Only digits are allowed')); + } + } + + /** + * {@inheritdoc} + */ + public function build() { + return ['#markup' => 'foo']; + } + +} diff --git a/core/modules/image/src/Form/ImageEffectFormBase.php b/core/modules/image/src/Form/ImageEffectFormBase.php index b777d07ac75b..9a69e05b3d25 100644 --- a/core/modules/image/src/Form/ImageEffectFormBase.php +++ b/core/modules/image/src/Form/ImageEffectFormBase.php @@ -3,8 +3,8 @@ namespace Drupal\image\Form; use Drupal\Core\Form\FormBase; -use Drupal\Core\Form\FormState; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Form\SubformState; use Drupal\image\ConfigurableImageEffectInterface; use Drupal\image\ImageStyleInterface; use Drupal\Component\Plugin\Exception\PluginNotFoundException; @@ -25,7 +25,7 @@ abstract class ImageEffectFormBase extends FormBase { /** * The image effect. * - * @var \Drupal\image\ImageEffectInterface + * @var \Drupal\image\ImageEffectInterface|\Drupal\image\ConfigurableImageEffectInterface */ protected $imageEffect; @@ -73,7 +73,9 @@ public function buildForm(array $form, FormStateInterface $form_state, ImageStyl '#value' => $this->imageEffect->getPluginId(), ); - $form['data'] = $this->imageEffect->buildConfigurationForm(array(), $form_state); + $form['data'] = []; + $subform_state = SubformState::createForSubform($form['data'], $form, $form_state); + $form['data'] = $this->imageEffect->buildConfigurationForm($form['data'], $subform_state); $form['data']['#tree'] = TRUE; // Check the URL for a weight, then the image effect, otherwise use default. @@ -102,10 +104,7 @@ public function buildForm(array $form, FormStateInterface $form_state, ImageStyl public function validateForm(array &$form, FormStateInterface $form_state) { // The image effect configuration is stored in the 'data' key in the form, // pass that through for validation. - $effect_data = (new FormState())->setValues($form_state->getValue('data')); - $this->imageEffect->validateConfigurationForm($form, $effect_data); - // Update the original form values. - $form_state->setValue('data', $effect_data->getValues()); + $this->imageEffect->validateConfigurationForm($form['data'], SubformState::createForSubform($form['data'], $form, $form_state)); } /** @@ -116,10 +115,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { // The image effect configuration is stored in the 'data' key in the form, // pass that through for submission. - $effect_data = (new FormState())->setValues($form_state->getValue('data')); - $this->imageEffect->submitConfigurationForm($form, $effect_data); - // Update the original form values. - $form_state->setValue('data', $effect_data->getValues()); + $this->imageEffect->submitConfigurationForm($form['data'], SubformState::createForSubform($form['data'], $form, $form_state)); $this->imageEffect->setWeight($form_state->getValue('weight')); if (!$this->imageEffect->getUuid()) { diff --git a/core/modules/image/src/Tests/ImageEffectsTest.php b/core/modules/image/src/Tests/ImageEffectsTest.php index e56807073a12..895457f10439 100644 --- a/core/modules/image/src/Tests/ImageEffectsTest.php +++ b/core/modules/image/src/Tests/ImageEffectsTest.php @@ -2,6 +2,7 @@ namespace Drupal\image\Tests; +use Drupal\image\Entity\ImageStyle; use Drupal\system\Tests\Image\ToolkitTestBase; /** @@ -162,6 +163,26 @@ function testImageEffectsCaching() { $this->assertTrue($effects == $cached_effects, 'Cached effects are the same as generated effects.'); } + /** + * Tests if validation errors are passed plugin form to the parent form. + */ + public function testEffectFormValidationErrors() { + $account = $this->drupalCreateUser(['administer image styles']); + $this->drupalLogin($account); + /** @var \Drupal\image\ImageStyleInterface $style */ + $style = ImageStyle::load('thumbnail'); + // Image Scale is the only effect shipped with 'thumbnail', by default. + $uuids = $style->getEffects()->getInstanceIds(); + $uuid = key($uuids); + + // We are posting the form with both, width and height, empty. + $edit = ['data[width]' => '', 'data[height]' => '']; + $path = 'admin/config/media/image-styles/manage/thumbnail/effects/' . $uuid; + $this->drupalPostForm($path, $edit, t('Update effect')); + // Check that the error message has been displayed. + $this->assertText(t('Width and height can not both be blank.')); + } + /** * Asserts the effect processing of an image effect plugin. * diff --git a/core/tests/Drupal/Tests/Core/Form/FormStateDecoratorBaseTest.php b/core/tests/Drupal/Tests/Core/Form/FormStateDecoratorBaseTest.php new file mode 100644 index 000000000000..0f1c27bb3411 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Form/FormStateDecoratorBaseTest.php @@ -0,0 +1,1563 @@ +<?php + +namespace Drupal\Tests\Core\Form; + +use Drupal\Core\Form\FormInterface; +use Drupal\Core\Form\FormStateDecoratorBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; +use Drupal\Tests\UnitTestCase; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Response; + +/** + * @coversDefaultClass \Drupal\Core\Form\FormStateDecoratorBase + * + * @group Form + */ +class FormStateDecoratorBaseTest extends UnitTestCase { + + /** + * The decorated form state. + * + * @var \Drupal\Core\Form\FormStateInterface|\Prophecy\Prophecy\ObjectProphecy + */ + protected $decoratedFormState; + + /** + * The form state decorator base under test. + * + * @var \Drupal\Core\Form\FormStateDecoratorBase + */ + protected $formStateDecoratorBase; + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + + $this->decoratedFormState = $this->prophesize(FormStateInterface::class); + + $this->formStateDecoratorBase = new NonAbstractFormStateDecoratorBase($this->decoratedFormState->reveal()); + } + + /** + * Provides data to test methods that take a single boolean argument. + */ + public function providerSingleBooleanArgument() { + return [ + [TRUE], + [FALSE], + ]; + } + + /** + * @covers ::setFormState + */ + public function testSetFormState() { + $form_state_additions = [ + 'foo' => 'bar', + ]; + + $this->decoratedFormState->setFormState($form_state_additions) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setFormState($form_state_additions)); + } + + /** + * @covers ::setAlwaysProcess + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $always_process + */ + public function testSetAlwaysProcess($always_process) { + $this->decoratedFormState->setAlwaysProcess($always_process) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setAlwaysProcess($always_process)); + } + + /** + * @covers ::getAlwaysProcess + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $always_process + */ + public function testGetAlwaysProcess($always_process) { + $this->decoratedFormState->getAlwaysProcess() + ->willReturn($always_process) + ->shouldBeCalled(); + + $this->assertSame($always_process, $this->formStateDecoratorBase->getAlwaysProcess()); + } + + /** + * @covers ::setButtons + */ + public function testSetButtons() { + $buttons = [ + 'FOO' => 'BAR', + ]; + + $this->decoratedFormState->setButtons($buttons) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setButtons($buttons)); + } + + /** + * @covers ::getButtons + */ + public function testGetButtons() { + $buttons = [ + 'FOO' => 'BAR', + ]; + + $this->decoratedFormState->getButtons() + ->willReturn($buttons) + ->shouldBeCalled(); + + $this->assertSame($buttons, $this->formStateDecoratorBase->getButtons()); + } + + /** + * @covers ::setCached + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $cache + */ + public function testSetCached($cache) { + $this->decoratedFormState->setCached($cache) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setCached($cache)); + } + + /** + * @covers ::isCached + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $cache + */ + public function testIsCached($cache) { + $this->decoratedFormState->isCached() + ->willReturn($cache) + ->shouldBeCalled(); + $this->assertSame($cache, $this->formStateDecoratorBase->isCached()); + } + + /** + * @covers ::setCached + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $cache + * + * @expectedException \LogicException + */ + public function testSetCachedWithLogicException($cache) { + $this->decoratedFormState->setCached($cache) + ->willThrow(\LogicException::class); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setCached($cache)); + } + + /** + * @covers ::disableCache + */ + public function testDisableCache() { + $this->decoratedFormState->disableCache() + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->disableCache()); + } + + /** + * @covers ::setExecuted + */ + public function testSetExecuted() { + $this->decoratedFormState->setExecuted() + ->shouldBecalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setExecuted()); + } + + /** + * @covers ::isExecuted + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $executed + */ + public function testIsExecuted($executed) { + $this->decoratedFormState->isExecuted() + ->willReturn($executed) + ->shouldBeCalled(); + + $this->assertSame($executed, $this->formStateDecoratorBase->isExecuted()); + } + + /** + * @covers ::setGroups + */ + public function testSetGroups() { + $groups = [ + 'FOO' => 'BAR', + ]; + + $this->decoratedFormState->setGroups($groups) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setGroups($groups)); + } + + /** + * @covers ::getGroups + */ + public function testGetGroups() { + $groups = [ + 'FOO' => 'BAR', + ]; + + // Use PHPUnit for mocking, because Prophecy cannot mock methods that return + // by reference. See \Prophecy\Doubler\Generator\Node::getCode(). + $decorated_form_state = $this->getMock(FormStateInterface::class); + $decorated_form_state->expects($this->once()) + ->method('getGroups') + ->willReturn($groups); + + $this->formStateDecoratorBase = new NonAbstractFormStateDecoratorBase($decorated_form_state); + + $this->assertSame($groups, $this->formStateDecoratorBase->getGroups()); + } + + /** + * @covers ::setHasFileElement + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $has_file_element + */ + public function testSetHasFileElement($has_file_element) { + $this->decoratedFormState->setHasFileElement($has_file_element) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setHasFileElement($has_file_element)); + } + + /** + * @covers ::hasFileElement + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $has_file_element + */ + public function testHasFileElement($has_file_element) { + $this->decoratedFormState->hasFileElement() + ->willReturn($has_file_element) + ->shouldBeCalled(); + + $this->assertSame($has_file_element, $this->formStateDecoratorBase->hasFileElement()); + } + + /** + * @covers ::setLimitValidationErrors + * + * @dataProvider providerLimitValidationErrors + * + * @param array{}|null $limit_validation_errors + * Any valid value for + * \Drupal\Core\Form\FormStateInterface::setLimitValidationErrors()'s + * $limit_validation_errors argument; + */ + public function testSetLimitValidationErrors($limit_validation_errors) { + $this->decoratedFormState->setLimitValidationErrors($limit_validation_errors) + ->shouldBecalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setLimitValidationErrors($limit_validation_errors)); + } + + /** + * @covers ::getLimitValidationErrors + * + * @dataProvider providerLimitValidationErrors + * + * @param array[]|null $limit_validation_errors + * Any valid vlaue for + * \Drupal\Core\Form\FormStateInterface::getLimitValidationErrors()'s + * return value; + */ + public function testGetLimitValidationErrors($limit_validation_errors) { + $this->decoratedFormState->getLimitValidationErrors() + ->willReturn($limit_validation_errors) + ->shouldBeCalled(); + + $this->assertSame($limit_validation_errors, $this->formStateDecoratorBase->getLimitValidationErrors()); + } + + /** + * Provides data to self::testGetLimitValidationErrors() and self::testGetLimitValidationErrors(). + */ + public function providerLimitValidationErrors() { + return [ + [NULL], + [ + [ + ['foo', 'bar', 'baz'], + ], + ], + ]; + } + + /** + * @covers ::setMethod + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $method + */ + public function testSetMethod($method) { + $this->decoratedFormState->setMethod($method) + ->shouldBecalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setMethod($method)); + } + + /** + * @covers ::isMethodType + * + * @dataProvider providerIsMethodType + * + * @param bool $expected_return_value + * @param string $method_type + * Either "GET" or "POST". + */ + public function testIsMethodType($expected_return_value, $method_type) { + $this->decoratedFormState->isMethodType($method_type) + ->willReturn($expected_return_value) + ->shouldBecalled(); + + $this->assertSame($expected_return_value, $this->formStateDecoratorBase->isMethodType($method_type)); + } + + /** + * Provides data to self::testIsMethodType(). + */ + public function providerIsMethodType() { + return [ + [TRUE, 'GET'], + [TRUE, 'POST'], + [FALSE, 'GET'], + [FALSE, 'POST'], + ]; + } + + /** + * @covers ::setRequestMethod + * + * @dataProvider providerSetRequestMethod + * + * @param bool $method + */ + public function testSetRequestMethod($method) { + $this->decoratedFormState->setRequestMethod($method) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setRequestMethod($method)); + } + + /** + * Provides data to self::testSetMethod(). + */ + public function providerSetRequestMethod() { + return [ + ['GET'], + ['POST'], + ]; + } + + /** + * @covers ::setValidationEnforced + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $must_validate + */ + public function testSetValidationEnforced($must_validate) { + $this->decoratedFormState->setValidationEnforced($must_validate) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setValidationEnforced($must_validate)); + } + + /** + * @covers ::isValidationEnforced + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $must_validate + */ + public function testIsValidationEnforced($must_validate) { + $this->decoratedFormState->isValidationEnforced() + ->willReturn($must_validate) + ->shouldBecalled(); + + $this->assertSame($must_validate, $this->formStateDecoratorBase->isValidationEnforced()); + } + + /** + * @covers ::disableRedirect + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $no_redirect + */ + public function testDisableRedirect($no_redirect) { + $this->decoratedFormState->disableRedirect($no_redirect) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->disableRedirect($no_redirect)); + } + + /** + * @covers ::isRedirectDisabled + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $no_redirect + */ + public function testIsRedirectDisabled($no_redirect) { + $this->decoratedFormState->isRedirectDisabled() + ->willReturn($no_redirect) + ->shouldBeCalled(); + + $this->assertSame($no_redirect, $this->formStateDecoratorBase->isRedirectDisabled()); + } + + /** + * @covers ::setProcessInput + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $process_input + */ + public function testSetProcessInput($process_input) { + $this->decoratedFormState->setProcessInput($process_input) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setProcessInput($process_input)); + } + + /** + * @covers ::isProcessingInput + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $process_input + */ + public function testIsProcessingInput($process_input) { + $this->decoratedFormState->isProcessingInput() + ->willReturn($process_input) + ->shouldBecalled(); + + $this->assertSame($process_input, $this->formStateDecoratorBase->isProcessingInput()); + } + + /** + * @covers ::setProgrammed + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $programmed + */ + public function testSetProgrammed($programmed) { + $this->decoratedFormState->setProgrammed($programmed) + ->shouldBecalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setProgrammed($programmed)); + } + + /** + * @covers ::isProgrammed + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $programmed + */ + public function testIsProgrammed($programmed) { + $this->decoratedFormState->isProgrammed() + ->willReturn($programmed) + ->shouldBecalled(); + + $this->assertSame($programmed, $this->formStateDecoratorBase->isProgrammed()); + } + + /** + * @covers ::setProgrammedBypassAccessCheck + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $programmed_bypass_access_check + */ + public function testSetProgrammedBypassAccessCheck($programmed_bypass_access_check) { + $this->decoratedFormState->setProgrammedBypassAccessCheck($programmed_bypass_access_check) + ->shouldBecalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setProgrammedBypassAccessCheck($programmed_bypass_access_check)); + } + + /** + * @covers ::isBypassingProgrammedAccessChecks + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $programmed_bypass_access_check + */ + public function testIsBypassingProgrammedAccessChecks($programmed_bypass_access_check) { + $this->decoratedFormState->isBypassingProgrammedAccessChecks() + ->willReturn($programmed_bypass_access_check) + ->shouldBeCalled(); + + $this->assertSame($programmed_bypass_access_check, $this->formStateDecoratorBase->isBypassingProgrammedAccessChecks()); + } + + /** + * @covers ::setRebuildInfo + */ + public function testSetRebuildInfo() { + $rebuild_info = [ + 'FOO' => 'BAR', + ]; + + $this->decoratedFormState->setRebuildInfo($rebuild_info) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setRebuildInfo($rebuild_info)); + } + + /** + * @covers ::getRebuildInfo + */ + public function testGetRebuildInfo() { + $rebuild_info = [ + 'FOO' => 'BAR', + ]; + + $this->decoratedFormState->getRebuildInfo() + ->willReturn($rebuild_info) + ->shouldBeCalled(); + + $this->assertSame($rebuild_info, $this->formStateDecoratorBase->getRebuildInfo()); + } + + /** + * @covers ::addRebuildInfo + */ + public function testAddRebuildInfo() { + $property = 'FOO'; + $value = 'BAR'; + + $this->decoratedFormState->addRebuildInfo($property, $value); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->addRebuildInfo($property, $value)); + } + + /** + * @covers ::setStorage + */ + public function testSetStorage() { + $storage = [ + 'FOO' => 'BAR', + ]; + + $this->decoratedFormState->setStorage($storage) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setStorage($storage)); + } + + /** + * @covers ::getStorage + */ + public function testGetStorage() { + $storage = [ + 'FOO' => 'BAR', + ]; + + // Use PHPUnit for mocking, because Prophecy cannot mock methods that return + // by reference. See \Prophecy\Doubler\Generator\Node::getCode(). + $decorated_form_state = $this->getMock(FormStateInterface::class); + $decorated_form_state->expects($this->once()) + ->method('getStorage') + ->willReturn($storage); + + $this->formStateDecoratorBase = new NonAbstractFormStateDecoratorBase($decorated_form_state); + + $this->assertSame($storage, $this->formStateDecoratorBase->getStorage()); + } + + /** + * @covers ::setSubmitHandlers + */ + public function testSetSubmitHandlers() { + $submit_handlers = [ + 'FOO' => 'BAR', + ]; + + $this->decoratedFormState->setSubmitHandlers($submit_handlers) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setSubmitHandlers($submit_handlers)); + } + + /** + * @covers ::getSubmitHandlers + */ + public function testGetSubmitHandlers() { + $submit_handlers = [ + 'FOO' => 'BAR', + ]; + + $this->decoratedFormState->getSubmitHandlers() + ->willReturn($submit_handlers) + ->shouldBeCalled(); + + $this->assertSame($submit_handlers, $this->formStateDecoratorBase->getSubmitHandlers()); + } + + /** + * @covers ::setSubmitted + */ + public function testSetSubmitted() { + $this->decoratedFormState->setSubmitted() + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setSubmitted()); + } + + /** + * @covers ::isSubmitted + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $submitted + */ + public function testIsSubmitted($submitted) { + $this->decoratedFormState->isSubmitted() + ->willReturn($submitted); + + $this->assertSame($submitted, $this->formStateDecoratorBase->isSubmitted()); + } + + /** + * @covers ::setTemporary + */ + public function testSetTemporary() { + $temporary = [ + 'FOO' => 'BAR', + ]; + + $this->decoratedFormState->setTemporary($temporary) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setTemporary($temporary)); + } + + /** + * @covers ::getTemporary + */ + public function testGetTemporary() { + $temporary = [ + 'FOO' => 'BAR', + ]; + + $this->decoratedFormState->getTemporary() + ->willReturn($temporary) + ->shouldBeCalled(); + + $this->assertSame($temporary, $this->formStateDecoratorBase->getTemporary()); + } + + /** + * @covers ::setTemporaryValue + * + * @dataProvider providerSetTemporaryValue + * + * @param string $key + * @param mixed $value + */ + public function testSetTemporaryValue($key, $value) { + $this->decoratedFormState->setTemporaryValue($key, $value) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setTemporaryValue($key, $value)); + } + + /** + * Provides data to self::testSetTemporaryValue(). + */ + public function providerSetTemporaryValue() { + return [ + ['FOO', 'BAR'], + ['FOO', NULL], + ]; + } + + /** + * @covers ::getTemporaryValue + * + * @dataProvider providerGetTemporaryValue + * + * @param string $key + * @param mixed $value + */ + public function testGetTemporaryValue($key, $value = NULL) { + // Use PHPUnit for mocking, because Prophecy cannot mock methods that return + // by reference. See \Prophecy\Doubler\Generator\Node::getCode(). + $decorated_form_state = $this->getMock(FormStateInterface::class); + $decorated_form_state->expects($this->once()) + ->method('getTemporaryValue') + ->with($key) + ->willReturn($value); + + $this->formStateDecoratorBase = new NonAbstractFormStateDecoratorBase($decorated_form_state); + + $this->assertSame($value, $this->formStateDecoratorBase->getTemporaryValue($key)); + } + + /** + * Provides data to self::testGetTemporaryValue(). + */ + public function providerGetTemporaryValue() { + return [ + [TRUE, 'FOO', 'BAR'], + [TRUE, 'FOO', NULL], + ]; + } + + /** + * @covers ::hasTemporaryValue + * + * @dataProvider providerHasTemporaryValue + * + * @param bool $exists + * @param string $key + */ + public function testHasTemporaryValue($exists, $key) { + $this->decoratedFormState->hasTemporaryValue($key) + ->willReturn($exists) + ->shouldBeCalled(); + + $this->assertSame($exists, $this->formStateDecoratorBase->hasTemporaryValue($key)); + } + + /** + * Provides data to self::testHasTemporaryValue(). + */ + public function providerHasTemporaryValue() { + return [ + [TRUE, 'FOO'], + [FALSE, 'FOO'], + ]; + } + + /** + * @covers ::setTriggeringElement + */ + public function testSetTriggeringElement() { + $triggering_element = [ + 'FOO' => 'BAR', + ]; + + $this->decoratedFormState->setTriggeringElement($triggering_element) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setTriggeringElement($triggering_element)); + } + + /** + * @covers ::getTriggeringElement + */ + public function testGetTriggeringElement() { + $triggering_element = [ + 'FOO' => 'BAR', + ]; + + // Use PHPUnit for mocking, because Prophecy cannot mock methods that return + // by reference. See \Prophecy\Doubler\Generator\Node::getCode(). + $decorated_form_state = $this->getMock(FormStateInterface::class); + $decorated_form_state->expects($this->once()) + ->method('getTriggeringElement') + ->willReturn($triggering_element); + + $this->formStateDecoratorBase = new NonAbstractFormStateDecoratorBase($decorated_form_state); + + $this->assertSame($triggering_element, $this->formStateDecoratorBase->getTriggeringElement()); + } + + /** + * @covers ::setValidateHandlers + */ + public function testSetValidateHandlers() { + $validate_handlers = [ + 'FOO' => 'BAR', + ]; + + $this->decoratedFormState->setValidateHandlers($validate_handlers) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setValidateHandlers($validate_handlers)); + } + + /** + * @covers ::getValidateHandlers + */ + public function testGetValidateHandlers() { + $validate_handlers = [ + 'FOO' => 'BAR', + ]; + + $this->decoratedFormState->getValidateHandlers() + ->willReturn($validate_handlers) + ->shouldBecalled(); + + $this->assertSame($validate_handlers, $this->formStateDecoratorBase->getValidateHandlers()); + } + + /** + * @covers ::setValidationComplete + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $complete + */ + public function testSetValidationComplete($complete) { + $this->decoratedFormState->setValidationComplete($complete) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setValidationComplete($complete)); + } + + /** + * @covers ::isValidationComplete + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $complete + */ + public function testIsValidationComplete($complete) { + $this->decoratedFormState->isValidationComplete() + ->willReturn($complete) + ->shouldBeCalled(); + + $this->assertSame($complete, $this->formStateDecoratorBase->isValidationComplete()); + } + + /** + * @covers ::loadInclude + * + * @dataProvider providerLoadInclude + * + * @param string|false $expected + * @param string $module + * @param string $type + * @param string|null $name + */ + public function testLoadInclude($expected, $module, $type, $name) { + $this->decoratedFormState->loadInclude($module, $type, $name) + ->willReturn($expected) + ->shouldBeCalled(); + + $this->assertSame($expected, $this->formStateDecoratorBase->loadInclude($module, $type, $name)); + } + + /** + * Provides data to self::testLoadInclude(). + */ + public function providerLoadInclude() { + return [ + // Existing files. + [__FILE__, 'foo', 'inc', 'foo'], + [__FILE__, 'foo', 'inc', 'foo.admin'], + [__FILE__, 'bar', 'inc', 'bar'], + // Non-existent files. + [FALSE, 'foo', 'php', 'foo'], + [FALSE, 'bar', 'php', 'foo'], + ]; + } + + /** + * @covers ::getCacheableArray + */ + public function testGetCacheableArray() { + $cacheable_array = [ + 'foo' => 'bar', + ]; + + $this->decoratedFormState->getCacheableArray() + ->willReturn($cacheable_array) + ->shouldBeCalled(); + + $this->assertSame($cacheable_array, $this->formStateDecoratorBase->getCacheableArray()); + } + + /** + * @covers ::setCompleteForm + */ + public function testSetCompleteForm() { + $complete_form = [ + 'FOO' => 'BAR', + ]; + + $this->decoratedFormState->setCompleteForm($complete_form) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setCompleteForm($complete_form)); + } + + /** + * @covers ::getCompleteForm + */ + public function testGetCompleteForm() { + $complete_form = [ + 'FOO' => 'BAR', + ]; + + // Use PHPUnit for mocking, because Prophecy cannot mock methods that return + // by reference. See \Prophecy\Doubler\Generator\Node::getCode(). + $decorated_form_state = $this->getMock(FormStateInterface::class); + $decorated_form_state->expects($this->once()) + ->method('getCompleteForm') + ->willReturn($complete_form); + + $this->formStateDecoratorBase = new NonAbstractFormStateDecoratorBase($decorated_form_state); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setCompleteForm($complete_form)); + $this->assertSame($complete_form, $this->formStateDecoratorBase->getCompleteForm()); + } + + /** + * @covers ::set + * + * @dataProvider providerSet + * + * @param string $key + * @param mixed $value + */ + public function testSet($key, $value) { + $this->decoratedFormState->set($key, $value) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->set($key, $value)); + } + + /** + * Provides data to self::testSet(). + */ + public function providerSet() { + return [ + ['FOO', 'BAR'], + ['FOO', NULL], + ]; + } + + /** + * @covers ::get + * + * @dataProvider providerGet + * + * @param string $key + * @param mixed $value + */ + public function testGet($key, $value = NULL) { + + // Use PHPUnit for mocking, because Prophecy cannot mock methods that return + // by reference. See \Prophecy\Doubler\Generator\Node::getCode(). + $decorated_form_state = $this->getMock(FormStateInterface::class); + $decorated_form_state->expects($this->once()) + ->method('get') + ->with($key) + ->willReturn($value); + + $this->formStateDecoratorBase = new NonAbstractFormStateDecoratorBase($decorated_form_state); + + $this->assertSame($value, $this->formStateDecoratorBase->get($key)); + } + + /** + * Provides data to self::testGet(). + */ + public function providerGet() { + return [ + ['FOO', 'BAR'], + ['FOO', NULL], + ]; + } + + /** + * @covers ::has + * + * @dataProvider providerHas + * + * @param bool $exists + * @param string $key + */ + public function testHas($exists, $key) { + $this->decoratedFormState->has($key) + ->willReturn($exists) + ->shouldBeCalled(); + + $this->assertSame($exists, $this->formStateDecoratorBase->has($key)); + } + + /** + * Provides data to self::testHas(). + */ + public function providerHas() { + return [ + [TRUE, 'FOO'], + [FALSE, 'FOO'], + ]; + } + + /** + * @covers ::setBuildInfo + */ + public function testSetBuildInfo() { + $build_info = [ + 'FOO' => 'BAR', + ]; + + $this->decoratedFormState->setBuildInfo($build_info) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setBuildInfo($build_info)); + } + + /** + * @covers ::getBuildInfo + */ + public function testGetBuildInfo() { + $build_info = [ + 'FOO' => 'BAR', + ]; + + $this->decoratedFormState->getBuildInfo() + ->willReturn($build_info) + ->shouldBeCalled(); + + $this->assertSame($build_info, $this->formStateDecoratorBase->getBuildInfo()); + } + + /** + * @covers ::addBuildInfo + */ + public function testAddBuildInfo() { + $property = 'FOO'; + $value = 'BAR'; + + $this->decoratedFormState->addBuildInfo($property, $value) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->addBuildInfo($property, $value)); + } + + /** + * @covers ::setUserInput + */ + public function testSetUserInput() { + $user_input = [ + 'FOO' => 'BAR', + ]; + + $this->decoratedFormState->setUserInput($user_input) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setUserInput($user_input)); + } + + /** + * @covers ::getUserInput + */ + public function testGetUserInput() { + $user_input = [ + 'FOO' => 'BAR', + ]; + + // Use PHPUnit for mocking, because Prophecy cannot mock methods that return + // by reference. See \Prophecy\Doubler\Generator\Node::getCode(). + $decorated_form_state = $this->getMock(FormStateInterface::class); + $decorated_form_state->expects($this->once()) + ->method('getUserInput') + ->willReturn($user_input); + + $this->formStateDecoratorBase = new NonAbstractFormStateDecoratorBase($decorated_form_state); + + $this->assertSame($user_input, $this->formStateDecoratorBase->getUserInput()); + } + + /** + * @covers ::getValues + */ + public function testGetValues() { + $values = [ + 'FOO' => 'BAR', + ]; + + // Use PHPUnit for mocking, because Prophecy cannot mock methods that return + // by reference. See \Prophecy\Doubler\Generator\Node::getCode(). + $decorated_form_state = $this->getMock(FormStateInterface::class); + $decorated_form_state->expects($this->once()) + ->method('getValues') + ->willReturn($values); + + $this->formStateDecoratorBase = new NonAbstractFormStateDecoratorBase($decorated_form_state); + + $this->assertSame($values, $this->formStateDecoratorBase->getValues()); + } + /** + * @covers ::getValue + */ + public function testGetValue() { + $key = 'FOO'; + $value = 'BAR'; + + // Use PHPUnit for mocking, because Prophecy cannot mock methods that return + // by reference. See \Prophecy\Doubler\Generator\Node::getCode(). + $decorated_form_state = $this->getMock(FormStateInterface::class); + $decorated_form_state->expects($this->once()) + ->method('getValue') + ->with($key, $value) + ->willReturn($value); + + $this->formStateDecoratorBase = new NonAbstractFormStateDecoratorBase($decorated_form_state); + + $this->assertSame($value, $this->formStateDecoratorBase->getValue($key, $value)); + } + + /** + * @covers ::setValues + */ + public function testSetValues() { + $values = [ + 'foo' => 'Foo', + 'bar' => ['Bar'], + ]; + + $this->decoratedFormState->setValues($values) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setValues($values)); + } + + /** + * @covers ::setValue + */ + public function testSetValue() { + $key = 'FOO'; + $value = 'BAR'; + + $this->decoratedFormState->setValue($key, $value) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setValue($key, $value)); + } + + /** + * @covers ::unsetValue + */ + public function testUnsetValue() { + $key = 'FOO'; + + $this->decoratedFormState->unsetValue($key) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->unsetValue($key)); + } + + /** + * @covers ::hasValue + */ + public function testHasValue() { + $key = ['foo', 'bar']; + $has = TRUE; + + $this->decoratedFormState->hasValue($key) + ->willReturn($has) + ->shouldBeCalled(); + + $this->assertSame($has, $this->formStateDecoratorBase->hasValue($key)); + } + + /** + * @covers ::isValueEmpty + */ + public function testIsValueEmpty() { + $key = ['foo', 'bar']; + $is_empty = TRUE; + + $this->decoratedFormState->isValueEmpty($key) + ->willReturn($is_empty) + ->shouldBeCalled(); + + $this->assertSame($is_empty, $this->formStateDecoratorBase->isValueEmpty($key)); + } + + /** + * @covers ::setValueForElement + */ + public function testSetValueForElement() { + $element = [ + '#type' => 'foo', + ]; + $value = 'BAR'; + + $this->decoratedFormState->setValueForElement($element, $value) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setValueForElement($element, $value)); + } + + /** + * @covers ::setResponse + */ + public function testSetResponse() { + $response = $this->getMock(Response::class); + + $this->decoratedFormState->setResponse($response) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setResponse($response)); + } + + /** + * @covers ::getResponse + */ + public function testGetResponse() { + $response = $this->getMock(Response::class); + + $this->decoratedFormState->getResponse() + ->willReturn($response) + ->shouldBeCalled(); + + $this->assertSame($response, $this->formStateDecoratorBase->getResponse()); + } + + /** + * @covers ::setRedirect + */ + public function testSetRedirect() { + $route_name = 'foo'; + $route_parameters = [ + 'bar' => 'baz' + ]; + $options = [ + 'qux' => 'foo', + ]; + + $this->decoratedFormState->setRedirect($route_name, $route_parameters, $options) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setRedirect($route_name, $route_parameters, $options)); + } + + /** + * @covers ::setRedirectUrl + */ + public function testSetRedirectUrl() { + $url = new Url('foo'); + + $this->decoratedFormState->setRedirectUrl($url) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setRedirectUrl($url)); + } + + /** + * @covers ::getRedirect + * + * @dataProvider providerGetRedirect + * + * @param bool $expected + */ + public function testGetRedirect($expected) { + $this->decoratedFormState->getRedirect() + ->willReturn($expected) + ->shouldBeCalled(); + + $this->assertSame($expected, $this->formStateDecoratorBase->getRedirect()); + } + + /** + * Provides data to self::testGetRedirect(). + */ + public function providerGetRedirect() { + return [ + [NULL], + [FALSE], + [new Url('foo')], + [new RedirectResponse('http://example.com')], + ]; + } + + /** + * @covers ::setErrorByName + */ + public function testSetErrorByName() { + $name = 'foo'; + $message = 'bar'; + + $this->decoratedFormState->setErrorByName($name, $message) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setErrorByName($name, $message)); + } + + /** + * @covers ::setError + */ + public function testSetError() { + $element = [ + '#foo' => 'bar', + ]; + $message = 'bar'; + + $this->decoratedFormState->setError($element, $message) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setError($element, $message)); + } + + /** + * @covers ::clearErrors + */ + public function testClearErrors() { + $this->decoratedFormState->clearErrors() + ->shouldBeCalled(); + + $this->formStateDecoratorBase->clearErrors(); + } + + /** + * @covers ::getError + */ + public function testGetError() { + $element = [ + '#foo' => 'bar', + ]; + $message = 'bar'; + + $this->decoratedFormState->getError($element) + ->willReturn($message) + ->shouldBeCalled(); + + $this->assertSame($message, $this->formStateDecoratorBase->getError($element)); + } + + /** + * @covers ::getErrors + */ + public function testGetErrors() { + $errors = [ + 'foo' => 'bar', + ]; + $this->decoratedFormState->getErrors() + ->willReturn($errors) + ->shouldBeCalled(); + + $this->assertSame($errors, $this->formStateDecoratorBase->getErrors()); + } + + /** + * @covers ::setRebuild + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $rebuild + */ + public function testSetRebuild($rebuild) { + $this->decoratedFormState->setRebuild($rebuild) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setRebuild($rebuild)); + } + + /** + * @covers ::isRebuilding + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $rebuild + */ + public function testIsRebuilding($rebuild) { + $this->decoratedFormState->isRebuilding() + ->willReturn($rebuild) + ->shouldBeCalled(); + + $this->assertSame($rebuild, $this->formStateDecoratorBase->isRebuilding()); + } + + /** + * @covers ::setInvalidToken + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $expected + */ + public function testSetInvalidToken($expected) { + $this->decoratedFormState->setInvalidToken($expected) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setInvalidToken($expected)); + } + + /** + * @covers ::hasInvalidToken + * + * @dataProvider providerSingleBooleanArgument + * + * @param bool $expected + */ + public function testHasInvalidToken($expected) { + $this->decoratedFormState->hasInvalidToken() + ->willReturn($expected) + ->shouldBeCalled(); + + $this->assertSame($expected, $this->formStateDecoratorBase->hasInvalidToken()); + } + + /** + * @covers ::prepareCallback + * + * @dataProvider providerPrepareCallback + * + * @param string|callable $unprepared_callback + * @param callable $prepared_callback + */ + public function testPrepareCallback($unprepared_callback, callable $prepared_callback) { + $this->decoratedFormState->prepareCallback($unprepared_callback) + ->willReturn($prepared_callback) + ->shouldBeCalled(); + + $this->assertSame($prepared_callback, $this->formStateDecoratorBase->prepareCallback($unprepared_callback)); + } + + /** + * Provides data to self::testPrepareCallback(). + */ + public function providerPrepareCallback() { + $function = 'sleep'; + $shorthand_form_method = '::submit()'; + $closure = function() {}; + $static_method_string = __METHOD__; + $static_method_array = [__CLASS__, __FUNCTION__]; + $object_method_array = [$this, __FUNCTION__]; + + return [ + // A shorthand form method is generally expanded to become a method on an + // object. + [$shorthand_form_method, $object_method_array], + // Functions, closures, and static method calls generally remain the same. + [$function, $function], + [$closure, $closure], + [$static_method_string, $static_method_string], + [$static_method_array, $static_method_array], + ]; + } + + /** + * @covers ::setFormObject + */ + public function testSetFormObject() { + $form = $this->getMock(FormInterface::class); + + $this->decoratedFormState->setFormObject($form) + ->shouldBeCalled();; + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setFormObject($form)); + } + + /** + * @covers ::getFormObject + */ + public function testGetFormObject() { + $form = $this->getMock(FormInterface::class); + + $this->decoratedFormState->getFormObject() + ->willReturn($form) + ->shouldBeCalled(); + + $this->assertSame($form, $this->formStateDecoratorBase->getFormObject()); + } + + /** + * @covers ::setCleanValueKeys + */ + public function testSetCleanValueKeys() { + $keys = ['BAR']; + + $this->decoratedFormState->setCleanValueKeys($keys) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->setCleanValueKeys($keys)); + } + + /** + * @covers ::getCleanValueKeys + */ + public function testGetCleanValueKeys() { + $keys = ['BAR']; + + $this->decoratedFormState->getCleanValueKeys() + ->willReturn($keys) + ->shouldBeCalled(); + + $this->assertSame($keys, $this->formStateDecoratorBase->getCleanValueKeys()); + } + + /** + * @covers ::addCleanValueKey + */ + public function testAddCleanValueKey() { + $key = 'BAR'; + + $this->decoratedFormState->addCleanValueKey($key) + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->addCleanValueKey($key)); + } + + /** + * @covers ::cleanValues + */ + public function testCleanValues() { + $this->decoratedFormState->cleanValues() + ->shouldBeCalled(); + + $this->assertSame($this->formStateDecoratorBase, $this->formStateDecoratorBase->cleanValues()); + } + +} + +/** + * Provides a non-abstract version of the class under test. + */ +class NonAbstractFormStateDecoratorBase extends FormStateDecoratorBase { + + /** + * Creates a new instance. + * + * @param \Drupal\Core\Form\FormStateInterface $decorated_form_state + * The decorated form state. + */ + public function __construct(FormStateInterface $decorated_form_state) { + $this->decoratedFormState = $decorated_form_state; + } + +} diff --git a/core/tests/Drupal/Tests/Core/Form/FormStateTest.php b/core/tests/Drupal/Tests/Core/Form/FormStateTest.php index f014d772c42a..69118a8f77ee 100644 --- a/core/tests/Drupal/Tests/Core/Form/FormStateTest.php +++ b/core/tests/Drupal/Tests/Core/Form/FormStateTest.php @@ -142,75 +142,6 @@ public function testFormErrorsDuringSubmission() { $form_state->setErrorByName('test', 'message'); } - /** - * Tests that setting the value for an element adds to the values. - * - * @covers ::setValueForElement - */ - public function testSetValueForElement() { - $element = array( - '#parents' => array( - 'foo', - 'bar', - ), - ); - $value = $this->randomMachineName(); - - $form_state = new FormState(); - $form_state->setValueForElement($element, $value); - $expected = array( - 'foo' => array( - 'bar' => $value, - ), - ); - $this->assertSame($expected, $form_state->getValues()); - } - - /** - * @covers ::getValue - * - * @dataProvider providerTestGetValue - */ - public function testGetValue($key, $expected, $default = NULL) { - $form_state = (new FormState())->setValues([ - 'foo' => 'one', - 'bar' => array( - 'baz' => 'two', - ), - ]); - $this->assertSame($expected, $form_state->getValue($key, $default)); - } - - public function providerTestGetValue() { - $data = array(); - $data[] = array( - 'foo', 'one', - ); - $data[] = array( - array('bar', 'baz'), 'two', - ); - $data[] = array( - array('foo', 'bar', 'baz'), NULL, - ); - $data[] = array( - 'baz', 'baz', 'baz', - ); - return $data; - } - - /** - * @covers ::setValue - * - * @dataProvider providerTestSetValue - */ - public function testSetValue($key, $value, $expected) { - $form_state = (new FormState())->setValues([ - 'bar' => 'wrong', - ]); - $form_state->setValue($key, $value); - $this->assertSame($expected, $form_state->getValues()); - } - /** * @covers ::prepareCallback */ @@ -243,102 +174,6 @@ public function testPrepareCallbackArray() { $this->assertEquals($callback, $processed_callback); } - public function providerTestSetValue() { - $data = array(); - $data[] = array( - 'foo', 'one', array('bar' => 'wrong', 'foo' => 'one'), - ); - $data[] = array( - array('bar', 'baz'), 'two', array('bar' => array('baz' => 'two')), - ); - $data[] = array( - array('foo', 'bar', 'baz'), NULL, array('bar' => 'wrong', 'foo' => array('bar' => array('baz' => NULL))), - ); - return $data; - } - - /** - * @covers ::hasValue - * - * @dataProvider providerTestHasValue - */ - public function testHasValue($key, $expected) { - $form_state = (new FormState())->setValues([ - 'foo' => 'one', - 'bar' => array( - 'baz' => 'two', - ), - 'true' => TRUE, - 'false' => FALSE, - 'null' => NULL, - ]); - $this->assertSame($expected, $form_state->hasValue($key)); - } - - public function providerTestHasValue() { - $data = array(); - $data[] = array( - 'foo', TRUE, - ); - $data[] = array( - array('bar', 'baz'), TRUE, - ); - $data[] = array( - array('foo', 'bar', 'baz'), FALSE, - ); - $data[] = array( - 'true', TRUE, - ); - $data[] = array( - 'false', TRUE, - ); - $data[] = array( - 'null', FALSE, - ); - return $data; - } - - /** - * @covers ::isValueEmpty - * - * @dataProvider providerTestIsValueEmpty - */ - public function testIsValueEmpty($key, $expected) { - $form_state = (new FormState())->setValues([ - 'foo' => 'one', - 'bar' => array( - 'baz' => 'two', - ), - 'true' => TRUE, - 'false' => FALSE, - 'null' => NULL, - ]); - $this->assertSame($expected, $form_state->isValueEmpty($key)); - } - - public function providerTestIsValueEmpty() { - $data = array(); - $data[] = array( - 'foo', FALSE, - ); - $data[] = array( - array('bar', 'baz'), FALSE, - ); - $data[] = array( - array('foo', 'bar', 'baz'), TRUE, - ); - $data[] = array( - 'true', FALSE, - ); - $data[] = array( - 'false', TRUE, - ); - $data[] = array( - 'null', TRUE, - ); - return $data; - } - /** * @covers ::loadInclude */ @@ -584,6 +419,19 @@ public function testCleanValues($form_state) { $this->assertSame($form_state->cleanValues()->getValues(), ['value_to_keep' => 'magic_ponies']); } + /** + * @covers ::setValues + * @covers ::getValues + */ + public function testGetValues() { + $values = [ + 'foo' => 'bar', + ]; + $form_state = new FormState(); + $form_state->setValues($values); + $this->assertSame($values, $form_state->getValues()); + } + } /** diff --git a/core/tests/Drupal/Tests/Core/Form/FormStateValuesTraitTest.php b/core/tests/Drupal/Tests/Core/Form/FormStateValuesTraitTest.php new file mode 100644 index 000000000000..f5b2147b64ad --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Form/FormStateValuesTraitTest.php @@ -0,0 +1,263 @@ +<?php + +namespace Drupal\Tests\Core\Form; + +use Drupal\Core\Form\FormStateValuesTrait; +use Drupal\Tests\UnitTestCase; + +/** + * @coversDefaultClass \Drupal\Core\Form\FormStateValuesTrait + * + * @group Form + */ +class FormStateValuesTraitTest extends UnitTestCase { + + /** + * Tests that setting the value for an element adds to the values. + * + * @covers ::setValueForElement + */ + public function testSetValueForElement() { + $element = [ + '#parents' => [ + 'foo', + 'bar', + ], + ]; + $value = $this->randomMachineName(); + + $form_state = new FormStateValuesTraitStub(); + $form_state->setValueForElement($element, $value); + $expected = [ + 'foo' => [ + 'bar' => $value, + ], + ]; + $this->assertSame($expected, $form_state->getValues()); + } + + /** + * @covers ::getValue + * + * @dataProvider providerGetValue + */ + public function testGetValue($key, $expected, $default = NULL) { + $form_state = (new FormStateValuesTraitStub())->setValues([ + 'foo' => 'one', + 'bar' => [ + 'baz' => 'two', + ], + ]); + $this->assertSame($expected, $form_state->getValue($key, $default)); + } + + /** + * Provides data to self::testGetValue(). + * + * @return array[] + * Items are arrays of two items: + * - The key for which to get the value (string) + * - The expected value (mixed). + * - The default value (mixed). + */ + public function providerGetValue() { + $data = []; + $data[] = [ + 'foo', 'one', + ]; + $data[] = [ + ['bar', 'baz'], 'two', + ]; + $data[] = [ + ['foo', 'bar', 'baz'], NULL, + ]; + $data[] = [ + 'baz', 'baz', 'baz', + ]; + $data[] = [ + NULL, + [ + 'foo' => 'one', + 'bar' => [ + 'baz' => 'two', + ], + ], + ]; + return $data; + } + + /** + * @covers ::getValue + */ + public function testGetValueModifyReturn() { + $initial_values = $values = [ + 'foo' => 'one', + 'bar' => [ + 'baz' => 'two', + ], + ]; + $form_state = (new FormStateValuesTraitStub())->setValues($values); + + $value = &$form_state->getValue(NULL); + $this->assertSame($initial_values, $value); + $value = ['bing' => 'bang']; + $this->assertSame(['bing' => 'bang'], $form_state->getValues()); + $this->assertSame('bang', $form_state->getValue('bing')); + $this->assertSame(['bing' => 'bang'], $form_state->getValue(NULL)); + } + + /** + * @covers ::setValue + * + * @dataProvider providerSetValue + */ + public function testSetValue($key, $value, $expected) { + $form_state = (new FormStateValuesTraitStub())->setValues([ + 'bar' => 'wrong', + ]); + $form_state->setValue($key, $value); + $this->assertSame($expected, $form_state->getValues()); + } + + /** + * Provides data to self::testSetValue(). + * + * @return array[] + * Items are arrays of two items: + * - The key for which to set a new value (string) + * - The new value to set (mixed). + * - The expected form state values after setting the new value (mixed[]). + */ + public function providerSetValue() { + $data = []; + $data[] = [ + 'foo', 'one', ['bar' => 'wrong', 'foo' => 'one'], + ]; + $data[] = [ + ['bar', 'baz'], 'two', ['bar' => ['baz' => 'two']], + ]; + $data[] = [ + ['foo', 'bar', 'baz'], NULL, ['bar' => 'wrong', 'foo' => ['bar' => ['baz' => NULL]]], + ]; + return $data; + } + + /** + * @covers ::hasValue + * + * @dataProvider providerHasValue + */ + public function testHasValue($key, $expected) { + $form_state = (new FormStateValuesTraitStub())->setValues([ + 'foo' => 'one', + 'bar' => [ + 'baz' => 'two', + ], + 'true' => TRUE, + 'false' => FALSE, + 'null' => NULL, + ]); + $this->assertSame($expected, $form_state->hasValue($key)); + } + + /** + * Provides data to self::testHasValue(). + * + * @return array[] + * Items are arrays of two items: + * - The key to check for in the form state (string) + * - Whether the form state has an item with that key (bool). + */ + public function providerHasValue() { + $data = []; + $data[] = [ + 'foo', TRUE, + ]; + $data[] = [ + ['bar', 'baz'], TRUE, + ]; + $data[] = [ + ['foo', 'bar', 'baz'], FALSE, + ]; + $data[] = [ + 'true', TRUE, + ]; + $data[] = [ + 'false', TRUE, + ]; + $data[] = [ + 'null', FALSE, + ]; + return $data; + } + + /** + * @covers ::isValueEmpty + * + * @dataProvider providerIsValueEmpty + */ + public function testIsValueEmpty($key, $expected) { + $form_state = (new FormStateValuesTraitStub())->setValues([ + 'foo' => 'one', + 'bar' => [ + 'baz' => 'two', + ], + 'true' => TRUE, + 'false' => FALSE, + 'null' => NULL, + ]); + $this->assertSame($expected, $form_state->isValueEmpty($key)); + } + + /** + * Provides data to self::testIsValueEmpty(). + * + * @return array[] + * Items are arrays of two items: + * - The key to check for in the form state (string) + * - Whether the value is empty or not (bool). + */ + public function providerIsValueEmpty() { + $data = []; + $data[] = [ + 'foo', FALSE, + ]; + $data[] = [ + ['bar', 'baz'], FALSE, + ]; + $data[] = [ + ['foo', 'bar', 'baz'], TRUE, + ]; + $data[] = [ + 'true', FALSE, + ]; + $data[] = [ + 'false', TRUE, + ]; + $data[] = [ + 'null', TRUE, + ]; + return $data; + } + +} + +class FormStateValuesTraitStub { + + use FormStateValuesTrait; + + /** + * The submitted form values. + * + * @var mixed[] + */ + protected $values = []; + + /** + * {@inheritdoc} + */ + public function &getValues() { + return $this->values; + } + +} diff --git a/core/tests/Drupal/Tests/Core/Form/SubformStateTest.php b/core/tests/Drupal/Tests/Core/Form/SubformStateTest.php new file mode 100644 index 000000000000..ec2ac8bb2fa6 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Form/SubformStateTest.php @@ -0,0 +1,312 @@ +<?php + +namespace Drupal\Tests\Core\Form; + +use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Form\FormState; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Form\SubformState; +use Drupal\Core\Form\SubformStateInterface; +use Drupal\Tests\UnitTestCase; + +/** + * @coversDefaultClass \Drupal\Core\Form\SubformState + * + * @group Form + */ +class SubformStateTest extends UnitTestCase { + + /** + * The form state's values test fixture. + * + * @var mixed[] + */ + protected $formStateValues = [ + 'foo' => 'bar', + 'dog' => [ + 'breed' => 'Pit bull', + 'name' => 'Dodger', + ], + ]; + + /** + * The parent form. + * + * @var mixed[] + */ + protected $parentForm = [ + '#parents' => [], + 'foo' => [ + '#parents' => ['foo'], + '#array_parents' => ['foo'], + ], + 'dog' => [ + '#parents' => ['dog'], + '#array_parents' => ['dog'], + 'breed' => [ + '#parents' => ['dog', 'breed'], + '#array_parents' => ['dog', 'breed'], + ], + 'name' => [ + '#parents' => ['dog', 'name'], + '#array_parents' => ['dog', 'name'], + ], + ], + ]; + + /** + * @covers ::getValues + * @covers ::getParents + * + * @dataProvider providerGetValues + * + * @param string[] $parents + * @param string $expected + */ + public function testGetValues(array $parents, $expected) { + $parent_form_state = new FormState(); + $parent_form_state->setValues($this->formStateValues); + + $subform = NestedArray::getValue($this->parentForm, $parents); + $subform_state = SubformState::createForSubform($subform, $this->parentForm, $parent_form_state); + $subform_state_values = &$subform_state->getValues(); + $this->assertSame($expected, $subform_state_values); + + // Modify the retrieved values and confirm they are modified by reference in + // the parent form state. + $subform_state_values['fish'] = 'Jim'; + $this->assertSame($subform_state_values, $subform_state->getValues()); + } + + /** + * Provides data to self::testGetValues(). + */ + public function providerGetValues() { + $data = []; + $data['exist'] = [ + ['dog'], + $this->formStateValues['dog'], + ]; + + return $data; + } + + /** + * @covers ::getValues + * @covers ::getParents + * + * @dataProvider providerGetValuesBroken + * + * @expectedException \UnexpectedValueException + * + * @param string[] $parents + * @param string $expected + */ + public function testGetValuesBroken(array $parents, $expected) { + $this->testGetValues($parents, $expected); + } + + /** + * Provides data to self::testGetValuesBroken(). + */ + public function providerGetValuesBroken() { + $data = []; + $data['exist'] = [ + ['foo'], + $this->formStateValues['foo'], + ]; + $data['nested'] = [ + ['dog', 'name'], + 'Dodger', + ]; + + return $data; + } + + /** + * @covers ::getValue + * + * @dataProvider providerTestGetValue + */ + public function testGetValue($parents, $key, $expected, $default = NULL) { + $parent_form_state = new FormState(); + $parent_form_state->setValues($this->formStateValues); + + $subform = NestedArray::getValue($this->parentForm, $parents); + $subform_state = SubformState::createForSubform($subform, $this->parentForm, $parent_form_state); + $subform_state_value = &$subform_state->getValue($key, $default); + $this->assertSame($expected, $subform_state_value); + + // Modify the retrieved values and confirm they are modified by reference in + // the parent form state. + $subform_state_value = 'Jim'; + $this->assertSame($subform_state_value, $subform_state->getValue($key)); + } + + /** + * Provides data to self::testGetValue(). + */ + public function providerTestGetValue() { + $data = []; + $data['exist'] = [ + ['dog'], + 'name', + 'Dodger', + ]; + + return $data; + } + + /** + * @covers ::getValue + * + * @dataProvider providerTestGetValueBroken + * + * @expectedException \UnexpectedValueException + */ + public function testGetValueBroken(array $parents, $key, $expected, $default = NULL) { + $this->testGetValue($parents, $key, $expected, $default); + } + + /** + * Provides data to self::testGetValueBroken(). + */ + public function providerTestGetValueBroken() { + $data = []; + $data['nested'] = [ + ['dog', 'name'], + NULL, + 'Dodger', + ]; + + return $data; + } + + /** + * @covers ::setValues + * + * @dataProvider providerTestSetValues + */ + public function testSetValues($parents, $new_values, $expected) { + $parent_form_state = new FormState(); + $parent_form_state->setValues($this->formStateValues); + + $subform = NestedArray::getValue($this->parentForm, $parents); + $subform_state = SubformState::createForSubform($subform, $this->parentForm, $parent_form_state); + $this->assertSame($subform_state, $subform_state->setValues($new_values)); + $this->assertSame($expected, $parent_form_state->getValues()); + } + + /** + * Provides data to self::testSetValues(). + */ + public function providerTestSetValues() { + $data = []; + $data['exist'] = [ + ['dog'], + [], + [ + 'foo' => 'bar', + 'dog' => [], + ], + ]; + return $data; + } + + /** + * @covers ::setValues + * + * @dataProvider providerTestSetValuesBroken + * + * @expectedException \UnexpectedValueException + */ + public function testSetValuesBroken($parents, $new_values, $expected) { + $this->testSetValues($parents, $new_values, $expected); + } + + /** + * Provides data to self::testSetValuesBroken(). + */ + public function providerTestSetValuesBroken() { + $data = []; + $data['exist'] = [ + ['foo'], + [], + [ + 'foo' => [], + 'dog' => $this->formStateValues['dog'], + ], + ]; + return $data; + } + + /** + * @covers ::getCompleteFormState + */ + public function testGetCompleteFormStateWithParentCompleteForm() { + $parent_form_state = $this->prophesize(FormStateInterface::class); + $subform_state = SubformState::createForSubform($this->parentForm['dog'], $this->parentForm, $parent_form_state->reveal()); + $this->assertSame($parent_form_state->reveal(), $subform_state->getCompleteFormState()); + } + + /** + * @covers ::getCompleteFormState + */ + public function testGetCompleteFormStateWithParentSubform() { + $complete_form_state = $this->prophesize(FormStateInterface::class); + $parent_form_state = $this->prophesize(SubformStateInterface::class); + $parent_form_state->getCompleteFormState() + ->willReturn($complete_form_state->reveal()) + ->shouldBeCalled(); + $subform_state = SubformState::createForSubform($this->parentForm['dog'], $this->parentForm, $parent_form_state->reveal()); + $this->assertSame($complete_form_state->reveal(), $subform_state->getCompleteFormState()); + } + + /** + * @covers ::setLimitValidationErrors + */ + public function testSetLimitValidationErrors() { + $parent_limit_validation_errors = ['dog', 'name']; + $limit_validation_errors = ['name']; + + $parent_form_state = $this->prophesize(FormStateInterface::class); + $parent_form_state->setLimitValidationErrors($parent_limit_validation_errors) + ->shouldBeCalled(); + + $subform_state = SubformState::createForSubform($this->parentForm['dog'], $this->parentForm, $parent_form_state->reveal()); + $this->assertSame($subform_state, $subform_state->setLimitValidationErrors($limit_validation_errors)); + } + + /** + * @covers ::getLimitValidationErrors + */ + public function testGetLimitValidationErrors() { + $parent_limit_validation_errors = ['dog', 'name']; + $limit_validation_errors = ['name']; + + $parent_form_state = $this->prophesize(FormStateInterface::class); + $parent_form_state->getLimitValidationErrors() + ->willReturn($parent_limit_validation_errors) + ->shouldBeCalled(); + + $subform_state = SubformState::createForSubform($this->parentForm['dog'], $this->parentForm, $parent_form_state->reveal()); + $this->assertSame($limit_validation_errors, $subform_state->getLimitValidationErrors()); + } + + /** + * @covers ::setErrorByName + */ + public function testSetErrorByName() { + $parent_form_error_name = 'dog][name'; + $subform_error_name = 'name'; + $message = 'De kat krabt de krullen van de trap.'; + + $parent_form_state = $this->prophesize(FormStateInterface::class); + $parent_form_state->setErrorByName($parent_form_error_name, $message) + ->shouldBeCalled(); + + $subform_state = SubformState::createForSubform($this->parentForm['dog'], $this->parentForm, $parent_form_state->reveal()); + $this->assertSame($subform_state, $subform_state->setErrorByName($subform_error_name, $message)); + } + +} -- GitLab