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

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