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

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