vendor/symfony/config/Definition/BaseNode.php line 394

  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Config\Definition;
  11. use Symfony\Component\Config\Definition\Exception\Exception;
  12. use Symfony\Component\Config\Definition\Exception\ForbiddenOverwriteException;
  13. use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
  14. use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
  15. use Symfony\Component\Config\Definition\Exception\UnsetKeyException;
  16. /**
  17.  * The base node class.
  18.  *
  19.  * @author Johannes M. Schmitt <schmittjoh@gmail.com>
  20.  */
  21. abstract class BaseNode implements NodeInterface
  22. {
  23.     public const DEFAULT_PATH_SEPARATOR '.';
  24.     private static array $placeholderUniquePrefixes = [];
  25.     private static array $placeholders = [];
  26.     protected $name;
  27.     protected $parent;
  28.     protected $normalizationClosures = [];
  29.     protected $normalizedTypes = [];
  30.     protected $finalValidationClosures = [];
  31.     protected $allowOverwrite true;
  32.     protected $required false;
  33.     protected $deprecation = [];
  34.     protected $equivalentValues = [];
  35.     protected $attributes = [];
  36.     protected $pathSeparator;
  37.     private mixed $handlingPlaceholder null;
  38.     /**
  39.      * @throws \InvalidArgumentException if the name contains a period
  40.      */
  41.     public function __construct(?string $nameNodeInterface $parent nullstring $pathSeparator self::DEFAULT_PATH_SEPARATOR)
  42.     {
  43.         if (str_contains($name = (string) $name$pathSeparator)) {
  44.             throw new \InvalidArgumentException('The name must not contain ".'.$pathSeparator.'".');
  45.         }
  46.         $this->name $name;
  47.         $this->parent $parent;
  48.         $this->pathSeparator $pathSeparator;
  49.     }
  50.     /**
  51.      * Register possible (dummy) values for a dynamic placeholder value.
  52.      *
  53.      * Matching configuration values will be processed with a provided value, one by one. After a provided value is
  54.      * successfully processed the configuration value is returned as is, thus preserving the placeholder.
  55.      *
  56.      * @internal
  57.      */
  58.     public static function setPlaceholder(string $placeholder, array $values): void
  59.     {
  60.         if (!$values) {
  61.             throw new \InvalidArgumentException('At least one value must be provided.');
  62.         }
  63.         self::$placeholders[$placeholder] = $values;
  64.     }
  65.     /**
  66.      * Adds a common prefix for dynamic placeholder values.
  67.      *
  68.      * Matching configuration values will be skipped from being processed and are returned as is, thus preserving the
  69.      * placeholder. An exact match provided by {@see setPlaceholder()} might take precedence.
  70.      *
  71.      * @internal
  72.      */
  73.     public static function setPlaceholderUniquePrefix(string $prefix): void
  74.     {
  75.         self::$placeholderUniquePrefixes[] = $prefix;
  76.     }
  77.     /**
  78.      * Resets all current placeholders available.
  79.      *
  80.      * @internal
  81.      */
  82.     public static function resetPlaceholders(): void
  83.     {
  84.         self::$placeholderUniquePrefixes = [];
  85.         self::$placeholders = [];
  86.     }
  87.     public function setAttribute(string $keymixed $value)
  88.     {
  89.         $this->attributes[$key] = $value;
  90.     }
  91.     public function getAttribute(string $keymixed $default null): mixed
  92.     {
  93.         return $this->attributes[$key] ?? $default;
  94.     }
  95.     public function hasAttribute(string $key): bool
  96.     {
  97.         return isset($this->attributes[$key]);
  98.     }
  99.     public function getAttributes(): array
  100.     {
  101.         return $this->attributes;
  102.     }
  103.     public function setAttributes(array $attributes)
  104.     {
  105.         $this->attributes $attributes;
  106.     }
  107.     public function removeAttribute(string $key)
  108.     {
  109.         unset($this->attributes[$key]);
  110.     }
  111.     /**
  112.      * Sets an info message.
  113.      */
  114.     public function setInfo(string $info)
  115.     {
  116.         $this->setAttribute('info'$info);
  117.     }
  118.     /**
  119.      * Returns info message.
  120.      */
  121.     public function getInfo(): ?string
  122.     {
  123.         return $this->getAttribute('info');
  124.     }
  125.     /**
  126.      * Sets the example configuration for this node.
  127.      */
  128.     public function setExample(string|array $example)
  129.     {
  130.         $this->setAttribute('example'$example);
  131.     }
  132.     /**
  133.      * Retrieves the example configuration for this node.
  134.      */
  135.     public function getExample(): string|array|null
  136.     {
  137.         return $this->getAttribute('example');
  138.     }
  139.     /**
  140.      * Adds an equivalent value.
  141.      */
  142.     public function addEquivalentValue(mixed $originalValuemixed $equivalentValue)
  143.     {
  144.         $this->equivalentValues[] = [$originalValue$equivalentValue];
  145.     }
  146.     /**
  147.      * Set this node as required.
  148.      */
  149.     public function setRequired(bool $boolean)
  150.     {
  151.         $this->required $boolean;
  152.     }
  153.     /**
  154.      * Sets this node as deprecated.
  155.      *
  156.      * @param string $package The name of the composer package that is triggering the deprecation
  157.      * @param string $version The version of the package that introduced the deprecation
  158.      * @param string $message the deprecation message to use
  159.      *
  160.      * You can use %node% and %path% placeholders in your message to display,
  161.      * respectively, the node name and its complete path
  162.      */
  163.     public function setDeprecated(string $packagestring $versionstring $message 'The child node "%node%" at path "%path%" is deprecated.')
  164.     {
  165.         $this->deprecation = [
  166.             'package' => $package,
  167.             'version' => $version,
  168.             'message' => $message,
  169.         ];
  170.     }
  171.     /**
  172.      * Sets if this node can be overridden.
  173.      */
  174.     public function setAllowOverwrite(bool $allow)
  175.     {
  176.         $this->allowOverwrite $allow;
  177.     }
  178.     /**
  179.      * Sets the closures used for normalization.
  180.      *
  181.      * @param \Closure[] $closures An array of Closures used for normalization
  182.      */
  183.     public function setNormalizationClosures(array $closures)
  184.     {
  185.         $this->normalizationClosures $closures;
  186.     }
  187.     /**
  188.      * Sets the list of types supported by normalization.
  189.      *
  190.      * see ExprBuilder::TYPE_* constants.
  191.      */
  192.     public function setNormalizedTypes(array $types)
  193.     {
  194.         $this->normalizedTypes $types;
  195.     }
  196.     /**
  197.      * Gets the list of types supported by normalization.
  198.      *
  199.      * see ExprBuilder::TYPE_* constants.
  200.      */
  201.     public function getNormalizedTypes(): array
  202.     {
  203.         return $this->normalizedTypes;
  204.     }
  205.     /**
  206.      * Sets the closures used for final validation.
  207.      *
  208.      * @param \Closure[] $closures An array of Closures used for final validation
  209.      */
  210.     public function setFinalValidationClosures(array $closures)
  211.     {
  212.         $this->finalValidationClosures $closures;
  213.     }
  214.     public function isRequired(): bool
  215.     {
  216.         return $this->required;
  217.     }
  218.     /**
  219.      * Checks if this node is deprecated.
  220.      */
  221.     public function isDeprecated(): bool
  222.     {
  223.         return (bool) $this->deprecation;
  224.     }
  225.     /**
  226.      * @param string $node The configuration node name
  227.      * @param string $path The path of the node
  228.      */
  229.     public function getDeprecation(string $nodestring $path): array
  230.     {
  231.         return [
  232.             'package' => $this->deprecation['package'],
  233.             'version' => $this->deprecation['version'],
  234.             'message' => strtr($this->deprecation['message'], ['%node%' => $node'%path%' => $path]),
  235.         ];
  236.     }
  237.     public function getName(): string
  238.     {
  239.         return $this->name;
  240.     }
  241.     public function getPath(): string
  242.     {
  243.         if (null !== $this->parent) {
  244.             return $this->parent->getPath().$this->pathSeparator.$this->name;
  245.         }
  246.         return $this->name;
  247.     }
  248.     final public function merge(mixed $leftSidemixed $rightSide): mixed
  249.     {
  250.         if (!$this->allowOverwrite) {
  251.             throw new ForbiddenOverwriteException(sprintf('Configuration path "%s" cannot be overwritten. You have to define all options for this path, and any of its sub-paths in one configuration section.'$this->getPath()));
  252.         }
  253.         if ($leftSide !== $leftPlaceholders self::resolvePlaceholderValue($leftSide)) {
  254.             foreach ($leftPlaceholders as $leftPlaceholder) {
  255.                 $this->handlingPlaceholder $leftSide;
  256.                 try {
  257.                     $this->merge($leftPlaceholder$rightSide);
  258.                 } finally {
  259.                     $this->handlingPlaceholder null;
  260.                 }
  261.             }
  262.             return $rightSide;
  263.         }
  264.         if ($rightSide !== $rightPlaceholders self::resolvePlaceholderValue($rightSide)) {
  265.             foreach ($rightPlaceholders as $rightPlaceholder) {
  266.                 $this->handlingPlaceholder $rightSide;
  267.                 try {
  268.                     $this->merge($leftSide$rightPlaceholder);
  269.                 } finally {
  270.                     $this->handlingPlaceholder null;
  271.                 }
  272.             }
  273.             return $rightSide;
  274.         }
  275.         $this->doValidateType($leftSide);
  276.         $this->doValidateType($rightSide);
  277.         return $this->mergeValues($leftSide$rightSide);
  278.     }
  279.     final public function normalize(mixed $value): mixed
  280.     {
  281.         $value $this->preNormalize($value);
  282.         // run custom normalization closures
  283.         foreach ($this->normalizationClosures as $closure) {
  284.             $value $closure($value);
  285.         }
  286.         // resolve placeholder value
  287.         if ($value !== $placeholders self::resolvePlaceholderValue($value)) {
  288.             foreach ($placeholders as $placeholder) {
  289.                 $this->handlingPlaceholder $value;
  290.                 try {
  291.                     $this->normalize($placeholder);
  292.                 } finally {
  293.                     $this->handlingPlaceholder null;
  294.                 }
  295.             }
  296.             return $value;
  297.         }
  298.         // replace value with their equivalent
  299.         foreach ($this->equivalentValues as $data) {
  300.             if ($data[0] === $value) {
  301.                 $value $data[1];
  302.             }
  303.         }
  304.         // validate type
  305.         $this->doValidateType($value);
  306.         // normalize value
  307.         return $this->normalizeValue($value);
  308.     }
  309.     /**
  310.      * Normalizes the value before any other normalization is applied.
  311.      */
  312.     protected function preNormalize(mixed $value): mixed
  313.     {
  314.         return $value;
  315.     }
  316.     /**
  317.      * Returns parent node for this node.
  318.      */
  319.     public function getParent(): ?NodeInterface
  320.     {
  321.         return $this->parent;
  322.     }
  323.     final public function finalize(mixed $value): mixed
  324.     {
  325.         if ($value !== $placeholders self::resolvePlaceholderValue($value)) {
  326.             foreach ($placeholders as $placeholder) {
  327.                 $this->handlingPlaceholder $value;
  328.                 try {
  329.                     $this->finalize($placeholder);
  330.                 } finally {
  331.                     $this->handlingPlaceholder null;
  332.                 }
  333.             }
  334.             return $value;
  335.         }
  336.         $this->doValidateType($value);
  337.         $value $this->finalizeValue($value);
  338.         // Perform validation on the final value if a closure has been set.
  339.         // The closure is also allowed to return another value.
  340.         foreach ($this->finalValidationClosures as $closure) {
  341.             try {
  342.                 $value $closure($value);
  343.             } catch (Exception $e) {
  344.                 if ($e instanceof UnsetKeyException && null !== $this->handlingPlaceholder) {
  345.                     continue;
  346.                 }
  347.                 throw $e;
  348.             } catch (\Exception $e) {
  349.                 throw new InvalidConfigurationException(sprintf('Invalid configuration for path "%s": '$this->getPath()).$e->getMessage(), $e->getCode(), $e);
  350.             }
  351.         }
  352.         return $value;
  353.     }
  354.     /**
  355.      * Validates the type of a Node.
  356.      *
  357.      * @throws InvalidTypeException when the value is invalid
  358.      */
  359.     abstract protected function validateType(mixed $value);
  360.     /**
  361.      * Normalizes the value.
  362.      */
  363.     abstract protected function normalizeValue(mixed $value): mixed;
  364.     /**
  365.      * Merges two values together.
  366.      */
  367.     abstract protected function mergeValues(mixed $leftSidemixed $rightSide): mixed;
  368.     /**
  369.      * Finalizes a value.
  370.      */
  371.     abstract protected function finalizeValue(mixed $value): mixed;
  372.     /**
  373.      * Tests if placeholder values are allowed for this node.
  374.      */
  375.     protected function allowPlaceholders(): bool
  376.     {
  377.         return true;
  378.     }
  379.     /**
  380.      * Tests if a placeholder is being handled currently.
  381.      */
  382.     protected function isHandlingPlaceholder(): bool
  383.     {
  384.         return null !== $this->handlingPlaceholder;
  385.     }
  386.     /**
  387.      * Gets allowed dynamic types for this node.
  388.      */
  389.     protected function getValidPlaceholderTypes(): array
  390.     {
  391.         return [];
  392.     }
  393.     private static function resolvePlaceholderValue(mixed $value): mixed
  394.     {
  395.         if (\is_string($value)) {
  396.             if (isset(self::$placeholders[$value])) {
  397.                 return self::$placeholders[$value];
  398.             }
  399.             foreach (self::$placeholderUniquePrefixes as $placeholderUniquePrefix) {
  400.                 if (str_starts_with($value$placeholderUniquePrefix)) {
  401.                     return [];
  402.                 }
  403.             }
  404.         }
  405.         return $value;
  406.     }
  407.     private function doValidateType(mixed $value): void
  408.     {
  409.         if (null !== $this->handlingPlaceholder && !$this->allowPlaceholders()) {
  410.             $e = new InvalidTypeException(sprintf('A dynamic value is not compatible with a "%s" node type at path "%s".', static::class, $this->getPath()));
  411.             $e->setPath($this->getPath());
  412.             throw $e;
  413.         }
  414.         if (null === $this->handlingPlaceholder || null === $value) {
  415.             $this->validateType($value);
  416.             return;
  417.         }
  418.         $knownTypes array_keys(self::$placeholders[$this->handlingPlaceholder]);
  419.         $validTypes $this->getValidPlaceholderTypes();
  420.         if ($validTypes && array_diff($knownTypes$validTypes)) {
  421.             $e = new InvalidTypeException(sprintf(
  422.                 'Invalid type for path "%s". Expected %s, but got %s.',
  423.                 $this->getPath(),
  424.                 === \count($validTypes) ? '"'.reset($validTypes).'"' 'one of "'.implode('", "'$validTypes).'"',
  425.                 === \count($knownTypes) ? '"'.reset($knownTypes).'"' 'one of "'.implode('", "'$knownTypes).'"'
  426.             ));
  427.             if ($hint $this->getInfo()) {
  428.                 $e->addHint($hint);
  429.             }
  430.             $e->setPath($this->getPath());
  431.             throw $e;
  432.         }
  433.         $this->validateType($value);
  434.     }
  435. }