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

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