ConfigTarget.php

Same filename and directory in other branches
  1. 10 core/lib/Drupal/Core/Form/ConfigTarget.php

Namespace

Drupal\Core\Form

File

core/lib/Drupal/Core/Form/ConfigTarget.php

View source
<?php

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

use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Config\Config;

/**
 * Represents the mapping of a config property to a form element.
 *
 * @see \Drupal\Core\Form\ToConfig
 */
final class ConfigTarget {
    
    /**
     * The parents of the form element which maps to this config property.
     *
     * @var array
     *
     * @see \Drupal\Core\Form\ConfigFormBase::storeConfigKeyToFormElementMap()
     *
     * @internal
     *   This property is for internal use only.
     */
    public array $elementParents;
    
    /**
     * The property paths to target.
     *
     * @var string[]
     */
    public readonly array $propertyPaths;
    
    /**
     * Transforms a value loaded from config before it gets displayed by the form.
     *
     * @var callable|null
     *
     * @see ::getValue()
     */
    public readonly mixed $fromConfig;
    
    /**
     * Transforms a value submitted by the form before it is set in the config.
     *
     * @var callable|null
     *
     * @see ::setValue()
     */
    public readonly mixed $toConfig;
    
    /**
     * Constructs a ConfigTarget object.
     *
     * @param string $configName
     *   The name of the config object being read from or written to, e.g.
     *   `system.site`.
     * @param string|array $propertyPath
     *   The property path(s) being read or written, e.g., `page.front`.
     * @param callable|null $fromConfig
     *   (optional) A callback which should transform the value loaded from
     *   config before it gets displayed by the form. If NULL, no transformation
     *   will be done. The callback will receive all of the values loaded from
     *   config as separate arguments, in the order specified by
     *   $this->propertyPaths. Defaults to NULL.
     * @param callable|null $toConfig
     *   (optional) A callback which should transform the value submitted by the
     *   form before it is set in the config object. If NULL, no transformation
     *   will be done. The callback will receive the value submitted through the
     *   form; if this object is targeting multiple property paths, the value will
     *   be an array of the submitted values, keyed by property path, and must
     *   return an array with the transformed values, also keyed by property path.
     *   The callback will receive the form state object as its second argument.
     *   The callback may return a special values:
     *   - ToConfig::NoMapping, to indicate that the given form value does not
     *     need to be mapped onto the Config object
     *   - ToConfig::DeleteKey to indicate that the targeted property path should
     *     be deleted from config.
     *   Defaults to NULL.
     */
    public function __construct(string $configName, string|array $propertyPath, ?callable $fromConfig = NULL, ?callable $toConfig = NULL) {
        $this->fromConfig = $fromConfig;
        $this->toConfig = $toConfig;
        if (is_string($propertyPath)) {
            $propertyPath = [
                $propertyPath,
            ];
        }
        elseif (count($propertyPath) > 1 && (empty($fromConfig) || empty($toConfig))) {
            throw new \LogicException('The $fromConfig and $toConfig arguments must be passed to ' . __METHOD__ . '() if multiple property paths are targeted.');
        }
        $this->propertyPaths = array_values($propertyPath);
    }
    
    /**
     * Creates a ConfigTarget object.
     *
     * @param string $target
     *   The name of the config object, and property path, being read from or
     *   written to, in the form `CONFIG_NAME:PROPERTY_PATH`. For example,
     *   `system.site:page.front`.
     * @param string|null $fromConfig
     *   (optional) A callback which should transform the value loaded from
     *   config before it gets displayed by the form. If NULL, no transformation
     *   will be done. The callback will receive all of the values loaded from
     *   config as separate arguments, in the order specified by
     *   $this->propertyPaths. Defaults to NULL.
     * @param string|null $toConfig
     *   (optional) A callback which should transform the value submitted by the
     *   form before it is set in the config object. If NULL, no transformation
     *   will be done. The callback will receive the value submitted through the
     *   form; if this object is targeting multiple property paths, the value will
     *   be an array of the submitted values, keyed by property path, and must
     *   return an array with the transformed values, also keyed by property path.
     *   The callback will receive the form state object as its second argument.
     *   Defaults to NULL.
     *
     * @return self
     *   A ConfigTarget instance.
     */
    public static function fromString(string $target, ?string $fromConfig = NULL, ?string $toConfig = NULL) : self {
        [
            $configName,
            $propertyPath,
        ] = explode(':', $target, 2);
        return new self($configName, $propertyPath, $fromConfig, $toConfig);
    }
    
    /**
     * Gets the config target object for an element from a form array.
     *
     * @param array $array_parents
     *   The array to locate the element in the form.
     * @param array $form
     *   The form array.
     *
     * @return self
     *   A ConfigTarget instance.
     */
    public static function fromForm(array $array_parents, array $form) : self {
        $element = NestedArray::getValue($form, $array_parents);
        if (!isset($element['#config_target'])) {
            throw new \LogicException('The form element [' . implode('][', $array_parents) . '] does not have the #config_target property set');
        }
        $target = $element['#config_target'];
        if (is_string($target)) {
            $target = ConfigTarget::fromString($target);
        }
        if (!$target instanceof ConfigTarget) {
            throw new \LogicException('The form element [' . implode('][', $array_parents) . '] #config_target property is not a string or a ConfigTarget object');
        }
        // Add the element information to the config target object.
        $target->elementParents = $element['#parents'];
        return $target;
    }
    
    /**
     * Retrieves the mapped value from config.
     *
     * @param \Drupal\Core\Config\Config $config
     *   The config object we're reading from.
     *
     * @return mixed
     *   The mapped value, with any transformations applied.
     *
     * @throws \InvalidArgumentException
     *   Thrown if the given config object is not the one being targeted by
     *   $this->configName.
     */
    public function getValue(Config $config) : mixed {
        if ($config->getName() !== $this->configName) {
            throw new \InvalidArgumentException(sprintf('Config target is associated with %s but %s given.', $this->configName, $config->getName()));
        }
        $is_multi_target = $this->isMultiTarget();
        $value = $is_multi_target ? array_map($config->get(...), $this->propertyPaths) : $config->get($this->propertyPaths[0]);
        if ($this->fromConfig) {
            $value = $is_multi_target ? ($this->fromConfig)(...$value) : ($this->fromConfig)($value);
        }
        return $value;
    }
    
    /**
     * Sets the submitted value from config.
     *
     * @param \Drupal\Core\Config\Config $config
     *   The config object we're changing.
     * @param mixed $value
     *   The value(s) to set. If this object is targeting multiple property paths,
     *   this must be an array with the values to set, keyed by property path.
     * @param \Drupal\Core\Form\FormStateInterface $form_state
     *   The current form state.
     *
     * @throws \InvalidArgumentException
     *   Thrown if the given config object is not the one being targeted by
     *   $this->configName.
     * @throws \LogicException
     *   Thrown if this object is targeting multiple property paths and $value
     *   does not contain a value for every targeted property path.
     */
    public function setValue(Config $config, mixed $value, FormStateInterface $form_state) : void {
        if ($config->getName() !== $this->configName) {
            throw new \InvalidArgumentException(sprintf('Config target is associated with %s but %s given.', $this->configName, $config->getName()));
        }
        $is_multi_target = $this->isMultiTarget();
        if ($this->toConfig) {
            $value = ($this->toConfig)($value, $form_state);
            if ($is_multi_target) {
                // If we're targeting multiple property paths, $value needs to be an array
                // with every targeted property path.
                if (!is_array($value)) {
                    throw new \LogicException(sprintf('The toConfig callable returned a %s, but it must be an array with a key-value pair for each of the targeted property paths.', gettype($value)));
                }
                elseif ($missing_keys = array_diff($this->propertyPaths, array_keys($value))) {
                    throw new \LogicException(sprintf('The toConfig callable returned an array that is missing key-value pairs for the following targeted property paths: %s.', implode(', ', $missing_keys)));
                }
                elseif ($unknown_keys = array_diff(array_keys($value), $this->propertyPaths)) {
                    throw new \LogicException(sprintf('The toConfig callable returned an array that contains key-value pairs that do not match targeted property paths: %s.', implode(', ', $unknown_keys)));
                }
            }
        }
        // Match the structure expected for a multi-target ConfigTarget.
        if (!$is_multi_target) {
            $value = [
                $this->propertyPaths[0] => $value,
            ];
        }
        // Set the returned value, or if a special value (one of the cases in the
        // ConfigTargetValue enum): apply the appropriate action.
        array_walk($value, fn(mixed $value, string $property) => match ($value) {    ToConfig::NoOp => NULL,
            ToConfig::DeleteKey => $config->clear($property),
            default => $config->set($property, $value),
        
        });
    }
    
    /**
     * Indicates if this object targets multiple property paths.
     *
     * @return bool
     *   TRUE if this object is targeting multiple property paths, otherwise
     *   FALSE.
     */
    private function isMultiTarget() : bool {
        return count($this->propertyPaths) > 1;
    }

}

Classes

Title Deprecated Summary
ConfigTarget Represents the mapping of a config property to a form element.

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