Loading core/lib/Drupal/Core/Recipe/InputConfigurator.php +6 −6 Original line number Diff line number Diff line Loading @@ -72,7 +72,7 @@ public function __construct( $definition['constraints'], ); $data_definition->setSettings($definition); $this->data[$name] = $typedDataManager->create($data_definition, name: "$prefix.$name"); $this->data[$name] = $typedDataManager->create($data_definition); } } Loading Loading @@ -112,9 +112,9 @@ public function describeAll(): array { foreach ($this->dependencies->recipes as $dependency) { $descriptions = array_merge($descriptions, $dependency->input->describeAll()); } foreach ($this->data as $data) { $name = $data->getName(); $descriptions[$name] = $data->getDataDefinition()->getDescription(); foreach ($this->getDataDefinitions() as $key => $definition) { $name = $this->prefix . '.' . $key; $descriptions[$name] = $definition->getDescription(); } return $descriptions; } Loading Loading @@ -153,7 +153,7 @@ public function collectAll(InputCollectorInterface $collector, array &$processed $definition = $data->getDataDefinition(); $value = $collector->collectValue( $data->getName(), $this->prefix . '.' . $key, $definition, $this->getDefaultValue($definition), ); Loading @@ -161,7 +161,7 @@ public function collectAll(InputCollectorInterface $collector, array &$processed $violations = $data->validate(); if (count($violations) > 0) { throw new ValidationFailedException($data, $violations); throw new ValidationFailedException($value, $violations); } $this->values[$key] = $data->getCastedValue(); } Loading core/lib/Drupal/Core/Recipe/Recipe.php +2 −16 Original line number Diff line number Diff line Loading @@ -9,7 +9,6 @@ use Drupal\Core\Extension\ModuleExtensionList; use Drupal\Core\Extension\ThemeExtensionList; use Drupal\Component\Serialization\Yaml; use Drupal\Core\Render\Element; use Drupal\Core\TypedData\PrimitiveInterface; use Drupal\Core\Validation\Plugin\Validation\Constraint\RegexConstraint; use Symfony\Component\Validator\Constraints\All; Loading Loading @@ -204,8 +203,8 @@ private static function parse(string $file): array { 'interface' => PrimitiveInterface::class, ]), ], // The `prompt` and `form` elements, though optional, have their // own sets of constraints, // If there is a `prompt` element, it has its own set of // constraints. 'prompt' => new Optional([ new Collection([ 'method' => [ Loading @@ -216,19 +215,6 @@ private static function parse(string $file): array { ]), ]), ]), 'form' => new Optional([ new Sequentially([ new Type('associative_array'), // Every element in the `form` array has to be a form API // property, prefixed with `#`. Because recipe inputs can only // be primitive data types, child elements aren't allowed. new Callback(function (array $element, ExecutionContextInterface $context): void { if (Element::children($element)) { $context->addViolation('Form elements for recipe inputs cannot have child elements.'); } }), ]), ]), // Every input must define a default value. 'default' => new Required([ new Collection([ Loading core/lib/Drupal/Core/Recipe/RecipeInputFormTrait.phpdeleted 100644 → 0 +0 −151 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\Core\Recipe; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\TypedData\DataDefinitionInterface; use Drupal\Core\TypedData\TypedDataInterface; use Symfony\Component\Validator\Exception\ValidationFailedException; /** * Defines helper methods for forms which collect input on behalf of recipes. */ trait RecipeInputFormTrait { /** * Generates a tree of form elements for a recipe's inputs. * * @param \Drupal\Core\Recipe\Recipe $recipe * A recipe. * * @return array[] * A nested array of form elements for collecting input values for the given * recipe and its dependencies. The elements will be grouped by the recipe * that defined the input -- for example, $return['recipe_name']['input1'], * $return['recipe_name']['input2'], $return['dependency']['input_name'], * and so forth. The returned array will have the `#tree` property set to * TRUE. */ protected function buildRecipeInputForm(Recipe $recipe): array { $collector = new class () implements InputCollectorInterface { /** * A form array containing the input elements for the given recipe. * * This will be a tree of input elements, grouped by the name of the * recipe that defines them. For example: * * @code * $form = [ * 'recipe_1' => [ * 'input_1' => [ * '#type' => 'textfield', * '#title' => 'Some input value', * ], * 'input_2' => [ * '#type' => 'checkbox', * '#title' => 'Enable some feature or other?', * ], * ], * 'dependency_recipe' => [ * 'input_1' => [ * '#type' => 'textarea', * '#title' => 'An input defined by a dependency of recipe_1', * ], * ], * '#tree' => TRUE, * ]; * @endcode * * The `#tree` property will always be set to TRUE. * * @var array */ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis.UnusedVariable public array $form = []; /** * {@inheritdoc} */ public function collectValue(string $name, DataDefinitionInterface $definition, mixed $default_value): mixed { $element = $definition->getSetting('form'); if ($element) { $element += [ '#description' => $definition->getDescription(), '#default_value' => $default_value, ]; // Recipe inputs are always required. $element['#required'] = TRUE; NestedArray::setValue($this->form, explode('.', $name, 2), $element); // Always return the input elements as a tree. $this->form['#tree'] = TRUE; } return $default_value; } }; $recipe->input->collectAll($collector); return $collector->form; } /** * Validates user-inputted values to a recipe and its dependencies. * * @param \Drupal\Core\Recipe\Recipe $recipe * A recipe. * @param array $form * The form being validated, which should include the tree of elements * returned by ::buildRecipeInputForm(). * @param \Drupal\Core\Form\FormStateInterface $form_state * The current form state. The values should be organized in the tree * structure that was returned by ::buildRecipeInputForm(). */ protected function validateRecipeInput(Recipe $recipe, array &$form, FormStateInterface $form_state): void { try { $this->setRecipeInput($recipe, $form_state); } catch (ValidationFailedException $e) { $data = $e->getValue(); if ($data instanceof TypedDataInterface) { $element = NestedArray::getValue($form, explode('.', $data->getName(), 2)); $form_state->setError($element, $e->getMessage()); } else { // If the data isn't a typed data object, we have no idea how to handle // the situation, so just re-throw the exception. throw $e; } } } /** * Supplies user-inputted values to a recipe and its dependencies. * * @param \Drupal\Core\Recipe\Recipe $recipe * A recipe. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current form state. The values should be organized in the tree * structure that was returned by ::buildRecipeInputForm(). */ protected function setRecipeInput(Recipe $recipe, FormStateInterface $form_state): void { $recipe->input->collectAll(new class ($form_state) implements InputCollectorInterface { public function __construct(private readonly FormStateInterface $formState) { } /** * {@inheritdoc} */ public function collectValue(string $name, DataDefinitionInterface $definition, mixed $default_value): mixed { return $this->formState->getValue(explode('.', $name, 2), $default_value); } }); } } core/modules/system/tests/modules/form_test/form_test.routing.yml +0 −7 Original line number Diff line number Diff line Loading @@ -564,10 +564,3 @@ form_test.incorrect_config_target: _admin_route: TRUE requirements: _access: 'TRUE' form_test.recipe_input: path: '/form-test/recipe-input' defaults: _form: '\Drupal\form_test\Form\FormTestRecipeInputForm' requirements: _access: 'TRUE' core/modules/system/tests/modules/form_test/src/Form/FormTestRecipeInputForm.phpdeleted 100644 → 0 +0 −63 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\form_test\Form; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Recipe\Recipe; use Drupal\Core\Recipe\RecipeInputFormTrait; use Drupal\Core\Recipe\RecipeRunner; class FormTestRecipeInputForm extends FormBase { use RecipeInputFormTrait; /** * {@inheritdoc} */ public function getFormId(): string { return 'form_test_recipe_input'; } /** * Returns the recipe object under test. * * @return \Drupal\Core\Recipe\Recipe * A Recipe object for the input_test recipe. */ private function getRecipe(): Recipe { return Recipe::createFromDirectory('core/tests/fixtures/recipes/input_test'); } /** * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state): array { $form += $this->buildRecipeInputForm($this->getRecipe()); $form['apply'] = [ '#type' => 'submit', '#value' => $this->t('Apply recipe'), ]; return $form; } /** * {@inheritdoc} */ public function validateForm(array &$form, FormStateInterface $form_state): void { $this->validateRecipeInput($this->getRecipe(), $form, $form_state); } /** * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state): void { $recipe = $this->getRecipe(); $this->setRecipeInput($recipe, $form_state); RecipeRunner::processRecipe($recipe); } } Loading
core/lib/Drupal/Core/Recipe/InputConfigurator.php +6 −6 Original line number Diff line number Diff line Loading @@ -72,7 +72,7 @@ public function __construct( $definition['constraints'], ); $data_definition->setSettings($definition); $this->data[$name] = $typedDataManager->create($data_definition, name: "$prefix.$name"); $this->data[$name] = $typedDataManager->create($data_definition); } } Loading Loading @@ -112,9 +112,9 @@ public function describeAll(): array { foreach ($this->dependencies->recipes as $dependency) { $descriptions = array_merge($descriptions, $dependency->input->describeAll()); } foreach ($this->data as $data) { $name = $data->getName(); $descriptions[$name] = $data->getDataDefinition()->getDescription(); foreach ($this->getDataDefinitions() as $key => $definition) { $name = $this->prefix . '.' . $key; $descriptions[$name] = $definition->getDescription(); } return $descriptions; } Loading Loading @@ -153,7 +153,7 @@ public function collectAll(InputCollectorInterface $collector, array &$processed $definition = $data->getDataDefinition(); $value = $collector->collectValue( $data->getName(), $this->prefix . '.' . $key, $definition, $this->getDefaultValue($definition), ); Loading @@ -161,7 +161,7 @@ public function collectAll(InputCollectorInterface $collector, array &$processed $violations = $data->validate(); if (count($violations) > 0) { throw new ValidationFailedException($data, $violations); throw new ValidationFailedException($value, $violations); } $this->values[$key] = $data->getCastedValue(); } Loading
core/lib/Drupal/Core/Recipe/Recipe.php +2 −16 Original line number Diff line number Diff line Loading @@ -9,7 +9,6 @@ use Drupal\Core\Extension\ModuleExtensionList; use Drupal\Core\Extension\ThemeExtensionList; use Drupal\Component\Serialization\Yaml; use Drupal\Core\Render\Element; use Drupal\Core\TypedData\PrimitiveInterface; use Drupal\Core\Validation\Plugin\Validation\Constraint\RegexConstraint; use Symfony\Component\Validator\Constraints\All; Loading Loading @@ -204,8 +203,8 @@ private static function parse(string $file): array { 'interface' => PrimitiveInterface::class, ]), ], // The `prompt` and `form` elements, though optional, have their // own sets of constraints, // If there is a `prompt` element, it has its own set of // constraints. 'prompt' => new Optional([ new Collection([ 'method' => [ Loading @@ -216,19 +215,6 @@ private static function parse(string $file): array { ]), ]), ]), 'form' => new Optional([ new Sequentially([ new Type('associative_array'), // Every element in the `form` array has to be a form API // property, prefixed with `#`. Because recipe inputs can only // be primitive data types, child elements aren't allowed. new Callback(function (array $element, ExecutionContextInterface $context): void { if (Element::children($element)) { $context->addViolation('Form elements for recipe inputs cannot have child elements.'); } }), ]), ]), // Every input must define a default value. 'default' => new Required([ new Collection([ Loading
core/lib/Drupal/Core/Recipe/RecipeInputFormTrait.phpdeleted 100644 → 0 +0 −151 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\Core\Recipe; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\TypedData\DataDefinitionInterface; use Drupal\Core\TypedData\TypedDataInterface; use Symfony\Component\Validator\Exception\ValidationFailedException; /** * Defines helper methods for forms which collect input on behalf of recipes. */ trait RecipeInputFormTrait { /** * Generates a tree of form elements for a recipe's inputs. * * @param \Drupal\Core\Recipe\Recipe $recipe * A recipe. * * @return array[] * A nested array of form elements for collecting input values for the given * recipe and its dependencies. The elements will be grouped by the recipe * that defined the input -- for example, $return['recipe_name']['input1'], * $return['recipe_name']['input2'], $return['dependency']['input_name'], * and so forth. The returned array will have the `#tree` property set to * TRUE. */ protected function buildRecipeInputForm(Recipe $recipe): array { $collector = new class () implements InputCollectorInterface { /** * A form array containing the input elements for the given recipe. * * This will be a tree of input elements, grouped by the name of the * recipe that defines them. For example: * * @code * $form = [ * 'recipe_1' => [ * 'input_1' => [ * '#type' => 'textfield', * '#title' => 'Some input value', * ], * 'input_2' => [ * '#type' => 'checkbox', * '#title' => 'Enable some feature or other?', * ], * ], * 'dependency_recipe' => [ * 'input_1' => [ * '#type' => 'textarea', * '#title' => 'An input defined by a dependency of recipe_1', * ], * ], * '#tree' => TRUE, * ]; * @endcode * * The `#tree` property will always be set to TRUE. * * @var array */ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis.UnusedVariable public array $form = []; /** * {@inheritdoc} */ public function collectValue(string $name, DataDefinitionInterface $definition, mixed $default_value): mixed { $element = $definition->getSetting('form'); if ($element) { $element += [ '#description' => $definition->getDescription(), '#default_value' => $default_value, ]; // Recipe inputs are always required. $element['#required'] = TRUE; NestedArray::setValue($this->form, explode('.', $name, 2), $element); // Always return the input elements as a tree. $this->form['#tree'] = TRUE; } return $default_value; } }; $recipe->input->collectAll($collector); return $collector->form; } /** * Validates user-inputted values to a recipe and its dependencies. * * @param \Drupal\Core\Recipe\Recipe $recipe * A recipe. * @param array $form * The form being validated, which should include the tree of elements * returned by ::buildRecipeInputForm(). * @param \Drupal\Core\Form\FormStateInterface $form_state * The current form state. The values should be organized in the tree * structure that was returned by ::buildRecipeInputForm(). */ protected function validateRecipeInput(Recipe $recipe, array &$form, FormStateInterface $form_state): void { try { $this->setRecipeInput($recipe, $form_state); } catch (ValidationFailedException $e) { $data = $e->getValue(); if ($data instanceof TypedDataInterface) { $element = NestedArray::getValue($form, explode('.', $data->getName(), 2)); $form_state->setError($element, $e->getMessage()); } else { // If the data isn't a typed data object, we have no idea how to handle // the situation, so just re-throw the exception. throw $e; } } } /** * Supplies user-inputted values to a recipe and its dependencies. * * @param \Drupal\Core\Recipe\Recipe $recipe * A recipe. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current form state. The values should be organized in the tree * structure that was returned by ::buildRecipeInputForm(). */ protected function setRecipeInput(Recipe $recipe, FormStateInterface $form_state): void { $recipe->input->collectAll(new class ($form_state) implements InputCollectorInterface { public function __construct(private readonly FormStateInterface $formState) { } /** * {@inheritdoc} */ public function collectValue(string $name, DataDefinitionInterface $definition, mixed $default_value): mixed { return $this->formState->getValue(explode('.', $name, 2), $default_value); } }); } }
core/modules/system/tests/modules/form_test/form_test.routing.yml +0 −7 Original line number Diff line number Diff line Loading @@ -564,10 +564,3 @@ form_test.incorrect_config_target: _admin_route: TRUE requirements: _access: 'TRUE' form_test.recipe_input: path: '/form-test/recipe-input' defaults: _form: '\Drupal\form_test\Form\FormTestRecipeInputForm' requirements: _access: 'TRUE'
core/modules/system/tests/modules/form_test/src/Form/FormTestRecipeInputForm.phpdeleted 100644 → 0 +0 −63 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\form_test\Form; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Recipe\Recipe; use Drupal\Core\Recipe\RecipeInputFormTrait; use Drupal\Core\Recipe\RecipeRunner; class FormTestRecipeInputForm extends FormBase { use RecipeInputFormTrait; /** * {@inheritdoc} */ public function getFormId(): string { return 'form_test_recipe_input'; } /** * Returns the recipe object under test. * * @return \Drupal\Core\Recipe\Recipe * A Recipe object for the input_test recipe. */ private function getRecipe(): Recipe { return Recipe::createFromDirectory('core/tests/fixtures/recipes/input_test'); } /** * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state): array { $form += $this->buildRecipeInputForm($this->getRecipe()); $form['apply'] = [ '#type' => 'submit', '#value' => $this->t('Apply recipe'), ]; return $form; } /** * {@inheritdoc} */ public function validateForm(array &$form, FormStateInterface $form_state): void { $this->validateRecipeInput($this->getRecipe(), $form, $form_state); } /** * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state): void { $recipe = $this->getRecipe(); $this->setRecipeInput($recipe, $form_state); RecipeRunner::processRecipe($recipe); } }