InputConfigurator.php

Namespace

Drupal\Core\Recipe

File

core/lib/Drupal/Core/Recipe/InputConfigurator.php

View source
<?php

declare (strict_types=1);
namespace Drupal\Core\Recipe;

use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\Core\TypedData\TypedDataManagerInterface;
use Symfony\Component\Validator\Exception\ValidationFailedException;

/**
 * Collects and validates input values for a recipe.
 *
 * @internal
 *   This API is experimental.
 */
final class InputConfigurator {
    
    /**
     * The input data.
     *
     * @var \Drupal\Core\TypedData\TypedDataInterface[]
     */
    private array $data = [];
    
    /**
     * The collected input values, or NULL if none have been collected yet.
     *
     * @var mixed[]|null
     */
    private ?array $values = NULL;
    
    /**
     * @param array<string, array<string, mixed>> $definitions
     *   The recipe's input definitions, keyed by name. This is an array of arrays
     *   where each sub-array has, at minimum:
     *   - `description`: A short, human-readable description of the input (e.g.,
     *      what the recipe uses it for).
     *   - `data_type`: A primitive data type known to the typed data system.
     *   - `constraints`: An optional array of validation constraints to apply
     *     to the value. This should be an associative array of arrays, keyed by
     *     constraint name, where each sub-array is a set of options for that
     *     constraint (identical to the way validation constraints are defined in
     *     config schema).
     *   - `default`: A default value for the input, if it cannot be collected
     *     the user. See ::getDefaultValue() for more information.
     * @param \Drupal\Core\Recipe\RecipeConfigurator $dependencies
     *   The recipes that this recipe depends on.
     * @param string $prefix
     *   A prefix for each input definition, to give each one a unique name
     *   when collecting input for multiple recipes. Usually this is the unique
     *   name of the recipe.
     * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typedDataManager
     *   The typed data manager service.
     */
    public function __construct(array $definitions, RecipeConfigurator $dependencies, string $prefix, TypedDataManagerInterface $typedDataManager) {
        // Convert the input definitions to typed data definitions.
        foreach ($definitions as $name => $definition) {
            $data_definition = DataDefinition::create($definition['data_type'])->setDescription($definition['description'])
                ->setConstraints($definition['constraints'] ?? []);
            unset($definition['data_type'], $definition['description'], $definition['constraints']);
            $data_definition->setSettings($definition);
            $this->data[$name] = $typedDataManager->create($data_definition);
        }
    }
    
    /**
     * Returns the typed data definitions for the inputs defined by this recipe.
     *
     * This does NOT return the data definitions for inputs defined by this
     * recipe's dependencies.
     *
     * @return \Drupal\Core\TypedData\DataDefinitionInterface[]
     *   The typed data definitions, keyed by input name.
     */
    public function getDataDefinitions() : array {
        return array_map(fn(TypedDataInterface $data) => $data->getDataDefinition(), $this->data);
    }
    
    /**
     * Returns the collected input values, keyed by name.
     *
     * @return mixed[]
     *   The collected input values, keyed by name.
     */
    public function getValues() : array {
        return $this->values ?? [];
    }
    
    /**
     * Returns the description for all inputs of this recipe and its dependencies.
     *
     * @return string[]
     *   The descriptions of every input defined by the recipe and its
     *   dependencies, keyed by the input's fully qualified name (i.e., prefixed
     *   by the name of the recipe that defines it).
     */
    public function describeAll() : array {
        $descriptions = [];
        foreach ($this->dependencies->recipes as $dependency) {
            $descriptions = array_merge($descriptions, $dependency->input
                ->describeAll());
        }
        foreach ($this->getDataDefinitions() as $key => $definition) {
            $name = $this->prefix . '.' . $key;
            $descriptions[$name] = $definition->getDescription();
        }
        return $descriptions;
    }
    
    /**
     * Collects input values for this recipe and its dependencies.
     *
     * @param \Drupal\Core\Recipe\InputCollectorInterface $collector
     *   The input collector to use.
     * @param string[] $processed
     *   The names of the recipes for which input has already been collected.
     *   Internal use only, should not be passed in by calling code.
     *
     * @throws \Symfony\Component\Validator\Exception\ValidationFailedException
     *   Thrown if any of the collected values violate their validation
     *   constraints.
     */
    public function collectAll(InputCollectorInterface $collector, array &$processed = []) : void {
        if (is_array($this->values)) {
            throw new \LogicException('Input values cannot be changed once they have been set.');
        }
        // Don't bother collecting values for a recipe we've already seen.
        if (in_array($this->prefix, $processed, TRUE)) {
            return;
        }
        // First, collect values for the recipe's dependencies.
        
        /** @var \Drupal\Core\Recipe\Recipe $dependency */
        foreach ($this->dependencies->recipes as $dependency) {
            $dependency->input
                ->collectAll($collector, $processed);
        }
        $this->values = [];
        foreach ($this->data as $key => $data) {
            $definition = $data->getDataDefinition();
            $value = $collector->collectValue($this->prefix . '.' . $key, $definition, $this->getDefaultValue($definition));
            $data->setValue($value, FALSE);
            $violations = $data->validate();
            if (count($violations) > 0) {
                throw new ValidationFailedException($value, $violations);
            }
            $this->values[$key] = $data->getCastedValue();
        }
        $processed[] = $this->prefix;
    }
    
    /**
     * Returns the default value for an input definition.
     *
     * @param array $definition
     *   An input definition. Must contain a `source` element, which can be either
     *   'config' or 'value'. If `source` is 'config', then there must also be a
     *   `config` element, which is a two-element indexed array containing
     *   (in order) the name of an extant config object, and a property path
     *   within that object. If `source` is 'value', then there must be a `value`
     *   element, which will be returned as-is.
     *
     * @return mixed
     *   The default value.
     */
    private function getDefaultValue(DataDefinition $definition) : mixed {
        $settings = $definition->getSetting('default');
        if ($settings['source'] === 'config') {
            [
                $name,
                $key,
            ] = $settings['config'];
            $config = \Drupal::config($name);
            if ($config->isNew()) {
                throw new \RuntimeException("The '{$name}' config object does not exist.");
            }
            return $config->get($key);
        }
        return $settings['value'];
    }

}

Classes

Title Deprecated Summary
InputConfigurator Collects and validates input values for a recipe.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.