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

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 $placeholderUniquePrefixes = [];
  25.     private static $placeholders = [];
  26.     protected $name;
  27.     protected $parent;
  28.     protected $normalizationClosures = [];
  29.     protected $finalValidationClosures = [];
  30.     protected $allowOverwrite true;
  31.     protected $required false;
  32.     protected $deprecationMessage null;
  33.     protected $equivalentValues = [];
  34.     protected $attributes = [];
  35.     protected $pathSeparator;
  36.     private $handlingPlaceholder;
  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.     /**
  87.      * @param string $key
  88.      */
  89.     public function setAttribute($key$value)
  90.     {
  91.         $this->attributes[$key] = $value;
  92.     }
  93.     /**
  94.      * @param string $key
  95.      *
  96.      * @return mixed
  97.      */
  98.     public function getAttribute($key$default null)
  99.     {
  100.         return $this->attributes[$key] ?? $default;
  101.     }
  102.     /**
  103.      * @param string $key
  104.      *
  105.      * @return bool
  106.      */
  107.     public function hasAttribute($key)
  108.     {
  109.         return isset($this->attributes[$key]);
  110.     }
  111.     /**
  112.      * @return array
  113.      */
  114.     public function getAttributes()
  115.     {
  116.         return $this->attributes;
  117.     }
  118.     public function setAttributes(array $attributes)
  119.     {
  120.         $this->attributes $attributes;
  121.     }
  122.     /**
  123.      * @param string $key
  124.      */
  125.     public function removeAttribute($key)
  126.     {
  127.         unset($this->attributes[$key]);
  128.     }
  129.     /**
  130.      * Sets an info message.
  131.      *
  132.      * @param string $info
  133.      */
  134.     public function setInfo($info)
  135.     {
  136.         $this->setAttribute('info'$info);
  137.     }
  138.     /**
  139.      * Returns info message.
  140.      *
  141.      * @return string|null The info text
  142.      */
  143.     public function getInfo()
  144.     {
  145.         return $this->getAttribute('info');
  146.     }
  147.     /**
  148.      * Sets the example configuration for this node.
  149.      *
  150.      * @param string|array $example
  151.      */
  152.     public function setExample($example)
  153.     {
  154.         $this->setAttribute('example'$example);
  155.     }
  156.     /**
  157.      * Retrieves the example configuration for this node.
  158.      *
  159.      * @return string|array|null The example
  160.      */
  161.     public function getExample()
  162.     {
  163.         return $this->getAttribute('example');
  164.     }
  165.     /**
  166.      * Adds an equivalent value.
  167.      *
  168.      * @param mixed $originalValue
  169.      * @param mixed $equivalentValue
  170.      */
  171.     public function addEquivalentValue($originalValue$equivalentValue)
  172.     {
  173.         $this->equivalentValues[] = [$originalValue$equivalentValue];
  174.     }
  175.     /**
  176.      * Set this node as required.
  177.      *
  178.      * @param bool $boolean Required node
  179.      */
  180.     public function setRequired($boolean)
  181.     {
  182.         $this->required = (bool) $boolean;
  183.     }
  184.     /**
  185.      * Sets this node as deprecated.
  186.      *
  187.      * You can use %node% and %path% placeholders in your message to display,
  188.      * respectively, the node name and its complete path.
  189.      *
  190.      * @param string|null $message Deprecated message
  191.      */
  192.     public function setDeprecated($message)
  193.     {
  194.         $this->deprecationMessage $message;
  195.     }
  196.     /**
  197.      * Sets if this node can be overridden.
  198.      *
  199.      * @param bool $allow
  200.      */
  201.     public function setAllowOverwrite($allow)
  202.     {
  203.         $this->allowOverwrite = (bool) $allow;
  204.     }
  205.     /**
  206.      * Sets the closures used for normalization.
  207.      *
  208.      * @param \Closure[] $closures An array of Closures used for normalization
  209.      */
  210.     public function setNormalizationClosures(array $closures)
  211.     {
  212.         $this->normalizationClosures $closures;
  213.     }
  214.     /**
  215.      * Sets the closures used for final validation.
  216.      *
  217.      * @param \Closure[] $closures An array of Closures used for final validation
  218.      */
  219.     public function setFinalValidationClosures(array $closures)
  220.     {
  221.         $this->finalValidationClosures $closures;
  222.     }
  223.     /**
  224.      * {@inheritdoc}
  225.      */
  226.     public function isRequired()
  227.     {
  228.         return $this->required;
  229.     }
  230.     /**
  231.      * Checks if this node is deprecated.
  232.      *
  233.      * @return bool
  234.      */
  235.     public function isDeprecated()
  236.     {
  237.         return null !== $this->deprecationMessage;
  238.     }
  239.     /**
  240.      * Returns the deprecated message.
  241.      *
  242.      * @param string $node the configuration node name
  243.      * @param string $path the path of the node
  244.      *
  245.      * @return string
  246.      */
  247.     public function getDeprecationMessage($node$path)
  248.     {
  249.         return strtr($this->deprecationMessage, ['%node%' => $node'%path%' => $path]);
  250.     }
  251.     /**
  252.      * {@inheritdoc}
  253.      */
  254.     public function getName()
  255.     {
  256.         return $this->name;
  257.     }
  258.     /**
  259.      * {@inheritdoc}
  260.      */
  261.     public function getPath()
  262.     {
  263.         if (null !== $this->parent) {
  264.             return $this->parent->getPath().$this->pathSeparator.$this->name;
  265.         }
  266.         return $this->name;
  267.     }
  268.     /**
  269.      * {@inheritdoc}
  270.      */
  271.     final public function merge($leftSide$rightSide)
  272.     {
  273.         if (!$this->allowOverwrite) {
  274.             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()));
  275.         }
  276.         if ($leftSide !== $leftPlaceholders self::resolvePlaceholderValue($leftSide)) {
  277.             foreach ($leftPlaceholders as $leftPlaceholder) {
  278.                 $this->handlingPlaceholder $leftSide;
  279.                 try {
  280.                     $this->merge($leftPlaceholder$rightSide);
  281.                 } finally {
  282.                     $this->handlingPlaceholder null;
  283.                 }
  284.             }
  285.             return $rightSide;
  286.         }
  287.         if ($rightSide !== $rightPlaceholders self::resolvePlaceholderValue($rightSide)) {
  288.             foreach ($rightPlaceholders as $rightPlaceholder) {
  289.                 $this->handlingPlaceholder $rightSide;
  290.                 try {
  291.                     $this->merge($leftSide$rightPlaceholder);
  292.                 } finally {
  293.                     $this->handlingPlaceholder null;
  294.                 }
  295.             }
  296.             return $rightSide;
  297.         }
  298.         $this->doValidateType($leftSide);
  299.         $this->doValidateType($rightSide);
  300.         return $this->mergeValues($leftSide$rightSide);
  301.     }
  302.     /**
  303.      * {@inheritdoc}
  304.      */
  305.     final public function normalize($value)
  306.     {
  307.         $value $this->preNormalize($value);
  308.         // run custom normalization closures
  309.         foreach ($this->normalizationClosures as $closure) {
  310.             $value $closure($value);
  311.         }
  312.         // resolve placeholder value
  313.         if ($value !== $placeholders self::resolvePlaceholderValue($value)) {
  314.             foreach ($placeholders as $placeholder) {
  315.                 $this->handlingPlaceholder $value;
  316.                 try {
  317.                     $this->normalize($placeholder);
  318.                 } finally {
  319.                     $this->handlingPlaceholder null;
  320.                 }
  321.             }
  322.             return $value;
  323.         }
  324.         // replace value with their equivalent
  325.         foreach ($this->equivalentValues as $data) {
  326.             if ($data[0] === $value) {
  327.                 $value $data[1];
  328.             }
  329.         }
  330.         // validate type
  331.         $this->doValidateType($value);
  332.         // normalize value
  333.         return $this->normalizeValue($value);
  334.     }
  335.     /**
  336.      * Normalizes the value before any other normalization is applied.
  337.      *
  338.      * @param mixed $value
  339.      *
  340.      * @return mixed The normalized array value
  341.      */
  342.     protected function preNormalize($value)
  343.     {
  344.         return $value;
  345.     }
  346.     /**
  347.      * Returns parent node for this node.
  348.      *
  349.      * @return NodeInterface|null
  350.      */
  351.     public function getParent()
  352.     {
  353.         return $this->parent;
  354.     }
  355.     /**
  356.      * {@inheritdoc}
  357.      */
  358.     final public function finalize($value)
  359.     {
  360.         if ($value !== $placeholders self::resolvePlaceholderValue($value)) {
  361.             foreach ($placeholders as $placeholder) {
  362.                 $this->handlingPlaceholder $value;
  363.                 try {
  364.                     $this->finalize($placeholder);
  365.                 } finally {
  366.                     $this->handlingPlaceholder null;
  367.                 }
  368.             }
  369.             return $value;
  370.         }
  371.         $this->doValidateType($value);
  372.         $value $this->finalizeValue($value);
  373.         // Perform validation on the final value if a closure has been set.
  374.         // The closure is also allowed to return another value.
  375.         foreach ($this->finalValidationClosures as $closure) {
  376.             try {
  377.                 $value $closure($value);
  378.             } catch (Exception $e) {
  379.                 if ($e instanceof UnsetKeyException && null !== $this->handlingPlaceholder) {
  380.                     continue;
  381.                 }
  382.                 throw $e;
  383.             } catch (\Exception $e) {
  384.                 throw new InvalidConfigurationException(sprintf('Invalid configuration for path "%s": '$this->getPath()).$e->getMessage(), $e->getCode(), $e);
  385.             }
  386.         }
  387.         return $value;
  388.     }
  389.     /**
  390.      * Validates the type of a Node.
  391.      *
  392.      * @param mixed $value The value to validate
  393.      *
  394.      * @throws InvalidTypeException when the value is invalid
  395.      */
  396.     abstract protected function validateType($value);
  397.     /**
  398.      * Normalizes the value.
  399.      *
  400.      * @param mixed $value The value to normalize
  401.      *
  402.      * @return mixed The normalized value
  403.      */
  404.     abstract protected function normalizeValue($value);
  405.     /**
  406.      * Merges two values together.
  407.      *
  408.      * @param mixed $leftSide
  409.      * @param mixed $rightSide
  410.      *
  411.      * @return mixed The merged value
  412.      */
  413.     abstract protected function mergeValues($leftSide$rightSide);
  414.     /**
  415.      * Finalizes a value.
  416.      *
  417.      * @param mixed $value The value to finalize
  418.      *
  419.      * @return mixed The finalized value
  420.      */
  421.     abstract protected function finalizeValue($value);
  422.     /**
  423.      * Tests if placeholder values are allowed for this node.
  424.      */
  425.     protected function allowPlaceholders(): bool
  426.     {
  427.         return true;
  428.     }
  429.     /**
  430.      * Tests if a placeholder is being handled currently.
  431.      */
  432.     protected function isHandlingPlaceholder(): bool
  433.     {
  434.         return null !== $this->handlingPlaceholder;
  435.     }
  436.     /**
  437.      * Gets allowed dynamic types for this node.
  438.      */
  439.     protected function getValidPlaceholderTypes(): array
  440.     {
  441.         return [];
  442.     }
  443.     private static function resolvePlaceholderValue($value)
  444.     {
  445.         if (\is_string($value)) {
  446.             if (isset(self::$placeholders[$value])) {
  447.                 return self::$placeholders[$value];
  448.             }
  449.             foreach (self::$placeholderUniquePrefixes as $placeholderUniquePrefix) {
  450.                 if (str_starts_with($value$placeholderUniquePrefix)) {
  451.                     return [];
  452.                 }
  453.             }
  454.         }
  455.         return $value;
  456.     }
  457.     private function doValidateType($value): void
  458.     {
  459.         if (null !== $this->handlingPlaceholder && !$this->allowPlaceholders()) {
  460.             $e = new InvalidTypeException(sprintf('A dynamic value is not compatible with a "%s" node type at path "%s".', static::class, $this->getPath()));
  461.             $e->setPath($this->getPath());
  462.             throw $e;
  463.         }
  464.         if (null === $this->handlingPlaceholder || null === $value) {
  465.             $this->validateType($value);
  466.             return;
  467.         }
  468.         $knownTypes array_keys(self::$placeholders[$this->handlingPlaceholder]);
  469.         $validTypes $this->getValidPlaceholderTypes();
  470.         if ($validTypes && array_diff($knownTypes$validTypes)) {
  471.             $e = new InvalidTypeException(sprintf(
  472.                 'Invalid type for path "%s". Expected %s, but got %s.',
  473.                 $this->getPath(),
  474.                 === \count($validTypes) ? '"'.reset($validTypes).'"' 'one of "'.implode('", "'$validTypes).'"',
  475.                 === \count($knownTypes) ? '"'.reset($knownTypes).'"' 'one of "'.implode('", "'$knownTypes).'"'
  476.             ));
  477.             if ($hint $this->getInfo()) {
  478.                 $e->addHint($hint);
  479.             }
  480.             $e->setPath($this->getPath());
  481.             throw $e;
  482.         }
  483.         $this->validateType($value);
  484.     }
  485. }