Commit 03fc2042 authored by catch's avatar catch

Issue #2537732 by Xano, tim.plunkett, gnuget, claudiu.cristea, drunken monkey,...

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)
parent 2ec23349
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
use Drupal\Core\Executable\ExecutableManagerInterface; use Drupal\Core\Executable\ExecutableManagerInterface;
use Drupal\Core\Executable\ExecutablePluginBase; use Drupal\Core\Executable\ExecutablePluginBase;
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformStateInterface;
use Drupal\Core\Plugin\ContextAwarePluginAssignmentTrait; use Drupal\Core\Plugin\ContextAwarePluginAssignmentTrait;
/** /**
...@@ -47,6 +48,9 @@ public function isNegated() { ...@@ -47,6 +48,9 @@ public function isNegated() {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function buildConfigurationForm(array $form, FormStateInterface $form_state) { public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
if ($form_state instanceof SubformStateInterface) {
$form_state = $form_state->getCompleteFormState();
}
$contexts = $form_state->getTemporaryValue('gathered_contexts') ?: []; $contexts = $form_state->getTemporaryValue('gathered_contexts') ?: [];
$form['context_mapping'] = $this->addContextAssignmentElement($this, $contexts); $form['context_mapping'] = $this->addContextAssignmentElement($this, $contexts);
$form['negate'] = array( $form['negate'] = array(
...@@ -68,6 +72,9 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form ...@@ -68,6 +72,9 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form
*/ */
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['negate'] = $form_state->getValue('negate'); $this->configuration['negate'] = $form_state->getValue('negate');
if ($form_state->hasValue('context_mapping')) {
$this->setContextMapping($form_state->getValue('context_mapping'));
}
} }
/** /**
......
...@@ -11,6 +11,8 @@ ...@@ -11,6 +11,8 @@
*/ */
class FormState implements FormStateInterface { class FormState implements FormStateInterface {
use FormStateValuesTrait;
/** /**
* Tracks if any errors have been set on any form. * Tracks if any errors have been set on any form.
* *
...@@ -246,7 +248,8 @@ class FormState implements FormStateInterface { ...@@ -246,7 +248,8 @@ class FormState implements FormStateInterface {
* *
* This property is uncacheable. * This property is uncacheable.
* *
* @var array * @var array|null
* The submitted user input array, or NULL if no input was submitted yet.
*/ */
protected $input; protected $input;
...@@ -977,67 +980,6 @@ public function &getValues() { ...@@ -977,67 +980,6 @@ public function &getValues() {
return $this->values; 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} * {@inheritdoc}
*/ */
......
This diff is collapsed.
<?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);
}
}
<?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;
}
}
<?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();
}
...@@ -31,7 +31,9 @@ interface PluginFormInterface { ...@@ -31,7 +31,9 @@ interface PluginFormInterface {
* @param array $form * @param array $form
* An associative array containing the initial structure of the plugin form. * An associative array containing the initial structure of the plugin form.
* @param \Drupal\Core\Form\FormStateInterface $form_state * @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 * @return array
* The form structure. * The form structure.
...@@ -45,7 +47,9 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta ...@@ -45,7 +47,9 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
* An associative array containing the structure of the plugin form as built * An associative array containing the structure of the plugin form as built
* by static::buildConfigurationForm(). * by static::buildConfigurationForm().
* @param \Drupal\Core\Form\FormStateInterface $form_state * @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); public function validateConfigurationForm(array &$form, FormStateInterface $form_state);
...@@ -56,7 +60,9 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form ...@@ -56,7 +60,9 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form
* An associative array containing the structure of the plugin form as built * An associative array containing the structure of the plugin form as built
* by static::buildConfigurationForm(). * by static::buildConfigurationForm().
* @param \Drupal\Core\Form\FormStateInterface $form_state * @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); public function submitConfigurationForm(array &$form, FormStateInterface $form_state);
......
...@@ -9,8 +9,8 @@ ...@@ -9,8 +9,8 @@
use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Executable\ExecutableManagerInterface; use Drupal\Core\Executable\ExecutableManagerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface; use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Plugin\Context\ContextRepositoryInterface; use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
...@@ -134,7 +134,9 @@ public function form(array $form, FormStateInterface $form_state) { ...@@ -134,7 +134,9 @@ public function form(array $form, FormStateInterface $form_state) {
$form_state->setTemporaryValue('gathered_contexts', $this->contextRepository->getAvailableContexts()); $form_state->setTemporaryValue('gathered_contexts', $this->contextRepository->getAvailableContexts());
$form['#tree'] = TRUE; $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); $form['visibility'] = $this->buildVisibilityInterface([], $form_state);
// If creating a new block, calculate a safe default machine name. // If creating a new block, calculate a safe default machine name.
...@@ -294,11 +296,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) { ...@@ -294,11 +296,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
// The Block Entity form puts all block plugin form elements in the // 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 form element, so just pass that to the block for validation.
$settings = (new FormState())->setValues($form_state->getValue('settings')); $this->getPluginForm($this->entity->getPlugin())->validateConfigurationForm($form['settings'], SubformState::createForSubform($form['settings'], $form, $form_state));
// 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->validateVisibility($form, $form_state); $this->validateVisibility($form, $form_state);
} }
...@@ -322,11 +320,7 @@ protected function validateVisibility(array $form, FormStateInterface $form_stat ...@@ -322,11 +320,7 @@ protected function validateVisibility(array $form, FormStateInterface $form_stat
// Allow the condition to validate the form. // Allow the condition to validate the form.
$condition = $form_state->get(['conditions', $condition_id]); $condition = $form_state->get(['conditions', $condition_id]);
$condition_values = (new FormState()) $condition->validateConfigurationForm($form['visibility'][$condition_id], SubformState::createForSubform($form['visibility'][$condition_id], $form, $form_state));
->setValues($values);
$condition->validateConfigurationForm($form, $condition_values);
// Update the original form values.
$form_state->setValue(['visibility', $condition_id], $condition_values->getValues());
} }
} }
...@@ -339,19 +333,15 @@ public function submitForm(array &$form, FormStateInterface $form_state) { ...@@ -339,19 +333,15 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
$entity = $this->entity; $entity = $this->entity;
// The Block Entity form puts all block plugin form elements in the // The Block Entity form puts all block plugin form elements in the
// settings form element, so just pass that to the block for submission. // settings form element, so just pass that to the block for submission.
// @todo Find a way to avoid this manipulation. $sub_form_state = SubformState::createForSubform($form['settings'], $form, $form_state);
$settings = (new FormState())->setValues($form_state->getValue('settings'));
// Call the plugin submit handler. // Call the plugin submit handler.
$block = $entity->getPlugin(); $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 this block is context-aware, set the context mapping.
if ($block instanceof ContextAwarePluginInterface && $block->getContextDefinitions()) { if ($block instanceof ContextAwarePluginInterface && $block->getContextDefinitions()) {
$context_mapping = $settings->getValue('context_mapping', []); $context_mapping = $sub_form_state->getValue('context_mapping', []);
$block->setContextMapping($context_mapping); $block->setContextMapping($context_mapping);
} }
// Update the original form values.
$form_state->setValue('settings', $settings->getValues());
$this->submitVisibility($form, $form_state); $this->submitVisibility($form, $form_state);
...@@ -380,16 +370,19 @@ protected function submitVisibility(array $form, FormStateInterface $form_state) ...@@ -380,16 +370,19 @@ protected function submitVisibility(array $form, FormStateInterface $form_state)
foreach ($form_state->getValue('visibility') as $condition_id => $values) { foreach ($form_state->getValue('visibility') as $condition_id => $values) {
// Allow the condition to submit the form. // Allow the condition to submit the form.
$condition = $form_state->get(['conditions', $condition_id]); $condition = $form_state->get(['conditions', $condition_id]);
$condition_values = (new FormState()) $condition->submitConfigurationForm($form['visibility'][$condition_id], SubformState::createForSubform($form['visibility'][$condition_id], $form, $form_state));
->setValues($values);
$condition->submitConfigurationForm($form, $condition_values); // 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) { if ($condition instanceof ContextAwarePluginInterface) {
$context_mapping = isset($values['context_mapping']) ? $values['context_mapping'] : []; $context_mapping = isset($values['context_mapping']) ? $values['context_mapping'] : [];
$condition->setContextMapping($context_mapping); $condition->setContextMapping($context_mapping);
} }
// Update the original form values.
$condition_configuration = $condition->getConfiguration(); $condition_configuration = $condition->getConfiguration();
$form_state->setValue(['visibility', $condition_id], $condition_configuration);
// Update the visibility conditions on the block. // Update the visibility conditions on the block.
$this->entity->getVisibilityConditions()->addInstanceId($condition_id, $condition_configuration); $this->entity->getVisibilityConditions()->addInstanceId($condition_id, $condition_configuration);
} }
......
...@@ -288,4 +288,20 @@ public function testBlockPlacementIndicator() { ...@@ -288,4 +288,20 @@ public function testBlockPlacementIndicator() {
$this->assertUrl('admin/structure/block/list/classy'); $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.');
}