123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591 |
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <fabien@symfony.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Config\Definition;
- use Symfony\Component\Config\Definition\Exception\Exception;
- use Symfony\Component\Config\Definition\Exception\ForbiddenOverwriteException;
- use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
- use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
- use Symfony\Component\Config\Definition\Exception\UnsetKeyException;
- /**
- * The base node class.
- *
- * @author Johannes M. Schmitt <schmittjoh@gmail.com>
- */
- abstract class BaseNode implements NodeInterface
- {
- public const DEFAULT_PATH_SEPARATOR = '.';
- private static $placeholderUniquePrefixes = [];
- private static $placeholders = [];
- protected $name;
- protected $parent;
- protected $normalizationClosures = [];
- protected $finalValidationClosures = [];
- protected $allowOverwrite = true;
- protected $required = false;
- protected $deprecation = [];
- protected $equivalentValues = [];
- protected $attributes = [];
- protected $pathSeparator;
- private $handlingPlaceholder;
- /**
- * @throws \InvalidArgumentException if the name contains a period
- */
- public function __construct(?string $name, NodeInterface $parent = null, string $pathSeparator = self::DEFAULT_PATH_SEPARATOR)
- {
- if (false !== strpos($name = (string) $name, $pathSeparator)) {
- throw new \InvalidArgumentException('The name must not contain ".'.$pathSeparator.'".');
- }
- $this->name = $name;
- $this->parent = $parent;
- $this->pathSeparator = $pathSeparator;
- }
- /**
- * Register possible (dummy) values for a dynamic placeholder value.
- *
- * Matching configuration values will be processed with a provided value, one by one. After a provided value is
- * successfully processed the configuration value is returned as is, thus preserving the placeholder.
- *
- * @internal
- */
- public static function setPlaceholder(string $placeholder, array $values): void
- {
- if (!$values) {
- throw new \InvalidArgumentException('At least one value must be provided.');
- }
- self::$placeholders[$placeholder] = $values;
- }
- /**
- * Adds a common prefix for dynamic placeholder values.
- *
- * Matching configuration values will be skipped from being processed and are returned as is, thus preserving the
- * placeholder. An exact match provided by {@see setPlaceholder()} might take precedence.
- *
- * @internal
- */
- public static function setPlaceholderUniquePrefix(string $prefix): void
- {
- self::$placeholderUniquePrefixes[] = $prefix;
- }
- /**
- * Resets all current placeholders available.
- *
- * @internal
- */
- public static function resetPlaceholders(): void
- {
- self::$placeholderUniquePrefixes = [];
- self::$placeholders = [];
- }
- public function setAttribute(string $key, $value)
- {
- $this->attributes[$key] = $value;
- }
- /**
- * @return mixed
- */
- public function getAttribute(string $key, $default = null)
- {
- return $this->attributes[$key] ?? $default;
- }
- /**
- * @return bool
- */
- public function hasAttribute(string $key)
- {
- return isset($this->attributes[$key]);
- }
- /**
- * @return array
- */
- public function getAttributes()
- {
- return $this->attributes;
- }
- public function setAttributes(array $attributes)
- {
- $this->attributes = $attributes;
- }
- public function removeAttribute(string $key)
- {
- unset($this->attributes[$key]);
- }
- /**
- * Sets an info message.
- */
- public function setInfo(string $info)
- {
- $this->setAttribute('info', $info);
- }
- /**
- * Returns info message.
- *
- * @return string|null The info text
- */
- public function getInfo()
- {
- return $this->getAttribute('info');
- }
- /**
- * Sets the example configuration for this node.
- *
- * @param string|array $example
- */
- public function setExample($example)
- {
- $this->setAttribute('example', $example);
- }
- /**
- * Retrieves the example configuration for this node.
- *
- * @return string|array|null The example
- */
- public function getExample()
- {
- return $this->getAttribute('example');
- }
- /**
- * Adds an equivalent value.
- *
- * @param mixed $originalValue
- * @param mixed $equivalentValue
- */
- public function addEquivalentValue($originalValue, $equivalentValue)
- {
- $this->equivalentValues[] = [$originalValue, $equivalentValue];
- }
- /**
- * Set this node as required.
- *
- * @param bool $boolean Required node
- */
- public function setRequired(bool $boolean)
- {
- $this->required = $boolean;
- }
- /**
- * Sets this node as deprecated.
- *
- * @param string $package The name of the composer package that is triggering the deprecation
- * @param string $version The version of the package that introduced the deprecation
- * @param string $message the deprecation message to use
- *
- * You can use %node% and %path% placeholders in your message to display,
- * respectively, the node name and its complete path
- */
- public function setDeprecated(?string $package/*, string $version, string $message = 'The child node "%node%" at path "%path%" is deprecated.' */)
- {
- $args = \func_get_args();
- if (\func_num_args() < 2) {
- 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__);
- if (!isset($args[0])) {
- trigger_deprecation('symfony/config', '5.1', 'Passing a null message to un-deprecate a node is deprecated.');
- $this->deprecation = [];
- return;
- }
- $message = (string) $args[0];
- $package = $version = '';
- } else {
- $package = (string) $args[0];
- $version = (string) $args[1];
- $message = (string) ($args[2] ?? 'The child node "%node%" at path "%path%" is deprecated.');
- }
- $this->deprecation = [
- 'package' => $package,
- 'version' => $version,
- 'message' => $message,
- ];
- }
- /**
- * Sets if this node can be overridden.
- */
- public function setAllowOverwrite(bool $allow)
- {
- $this->allowOverwrite = $allow;
- }
- /**
- * Sets the closures used for normalization.
- *
- * @param \Closure[] $closures An array of Closures used for normalization
- */
- public function setNormalizationClosures(array $closures)
- {
- $this->normalizationClosures = $closures;
- }
- /**
- * Sets the closures used for final validation.
- *
- * @param \Closure[] $closures An array of Closures used for final validation
- */
- public function setFinalValidationClosures(array $closures)
- {
- $this->finalValidationClosures = $closures;
- }
- /**
- * {@inheritdoc}
- */
- public function isRequired()
- {
- return $this->required;
- }
- /**
- * Checks if this node is deprecated.
- *
- * @return bool
- */
- public function isDeprecated()
- {
- return (bool) $this->deprecation;
- }
- /**
- * Returns the deprecated message.
- *
- * @param string $node the configuration node name
- * @param string $path the path of the node
- *
- * @return string
- *
- * @deprecated since Symfony 5.1, use "getDeprecation()" instead.
- */
- public function getDeprecationMessage(string $node, string $path)
- {
- trigger_deprecation('symfony/config', '5.1', 'The "%s()" method is deprecated, use "getDeprecation()" instead.', __METHOD__);
- return $this->getDeprecation($node, $path)['message'];
- }
- /**
- * @param string $node The configuration node name
- * @param string $path The path of the node
- */
- public function getDeprecation(string $node, string $path): array
- {
- return [
- 'package' => $this->deprecation['package'] ?? '',
- 'version' => $this->deprecation['version'] ?? '',
- 'message' => strtr($this->deprecation['message'] ?? '', ['%node%' => $node, '%path%' => $path]),
- ];
- }
- /**
- * {@inheritdoc}
- */
- public function getName()
- {
- return $this->name;
- }
- /**
- * {@inheritdoc}
- */
- public function getPath()
- {
- if (null !== $this->parent) {
- return $this->parent->getPath().$this->pathSeparator.$this->name;
- }
- return $this->name;
- }
- /**
- * {@inheritdoc}
- */
- final public function merge($leftSide, $rightSide)
- {
- if (!$this->allowOverwrite) {
- 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()));
- }
- if ($leftSide !== $leftPlaceholders = self::resolvePlaceholderValue($leftSide)) {
- foreach ($leftPlaceholders as $leftPlaceholder) {
- $this->handlingPlaceholder = $leftSide;
- try {
- $this->merge($leftPlaceholder, $rightSide);
- } finally {
- $this->handlingPlaceholder = null;
- }
- }
- return $rightSide;
- }
- if ($rightSide !== $rightPlaceholders = self::resolvePlaceholderValue($rightSide)) {
- foreach ($rightPlaceholders as $rightPlaceholder) {
- $this->handlingPlaceholder = $rightSide;
- try {
- $this->merge($leftSide, $rightPlaceholder);
- } finally {
- $this->handlingPlaceholder = null;
- }
- }
- return $rightSide;
- }
- $this->doValidateType($leftSide);
- $this->doValidateType($rightSide);
- return $this->mergeValues($leftSide, $rightSide);
- }
- /**
- * {@inheritdoc}
- */
- final public function normalize($value)
- {
- $value = $this->preNormalize($value);
- // run custom normalization closures
- foreach ($this->normalizationClosures as $closure) {
- $value = $closure($value);
- }
- // resolve placeholder value
- if ($value !== $placeholders = self::resolvePlaceholderValue($value)) {
- foreach ($placeholders as $placeholder) {
- $this->handlingPlaceholder = $value;
- try {
- $this->normalize($placeholder);
- } finally {
- $this->handlingPlaceholder = null;
- }
- }
- return $value;
- }
- // replace value with their equivalent
- foreach ($this->equivalentValues as $data) {
- if ($data[0] === $value) {
- $value = $data[1];
- }
- }
- // validate type
- $this->doValidateType($value);
- // normalize value
- return $this->normalizeValue($value);
- }
- /**
- * Normalizes the value before any other normalization is applied.
- *
- * @param mixed $value
- *
- * @return mixed The normalized array value
- */
- protected function preNormalize($value)
- {
- return $value;
- }
- /**
- * Returns parent node for this node.
- *
- * @return NodeInterface|null
- */
- public function getParent()
- {
- return $this->parent;
- }
- /**
- * {@inheritdoc}
- */
- final public function finalize($value)
- {
- if ($value !== $placeholders = self::resolvePlaceholderValue($value)) {
- foreach ($placeholders as $placeholder) {
- $this->handlingPlaceholder = $value;
- try {
- $this->finalize($placeholder);
- } finally {
- $this->handlingPlaceholder = null;
- }
- }
- return $value;
- }
- $this->doValidateType($value);
- $value = $this->finalizeValue($value);
- // Perform validation on the final value if a closure has been set.
- // The closure is also allowed to return another value.
- foreach ($this->finalValidationClosures as $closure) {
- try {
- $value = $closure($value);
- } catch (Exception $e) {
- if ($e instanceof UnsetKeyException && null !== $this->handlingPlaceholder) {
- continue;
- }
- throw $e;
- } catch (\Exception $e) {
- throw new InvalidConfigurationException(sprintf('Invalid configuration for path "%s": ', $this->getPath()).$e->getMessage(), $e->getCode(), $e);
- }
- }
- return $value;
- }
- /**
- * Validates the type of a Node.
- *
- * @param mixed $value The value to validate
- *
- * @throws InvalidTypeException when the value is invalid
- */
- abstract protected function validateType($value);
- /**
- * Normalizes the value.
- *
- * @param mixed $value The value to normalize
- *
- * @return mixed The normalized value
- */
- abstract protected function normalizeValue($value);
- /**
- * Merges two values together.
- *
- * @param mixed $leftSide
- * @param mixed $rightSide
- *
- * @return mixed The merged value
- */
- abstract protected function mergeValues($leftSide, $rightSide);
- /**
- * Finalizes a value.
- *
- * @param mixed $value The value to finalize
- *
- * @return mixed The finalized value
- */
- abstract protected function finalizeValue($value);
- /**
- * Tests if placeholder values are allowed for this node.
- */
- protected function allowPlaceholders(): bool
- {
- return true;
- }
- /**
- * Tests if a placeholder is being handled currently.
- */
- protected function isHandlingPlaceholder(): bool
- {
- return null !== $this->handlingPlaceholder;
- }
- /**
- * Gets allowed dynamic types for this node.
- */
- protected function getValidPlaceholderTypes(): array
- {
- return [];
- }
- private static function resolvePlaceholderValue($value)
- {
- if (\is_string($value)) {
- if (isset(self::$placeholders[$value])) {
- return self::$placeholders[$value];
- }
- foreach (self::$placeholderUniquePrefixes as $placeholderUniquePrefix) {
- if (0 === strpos($value, $placeholderUniquePrefix)) {
- return [];
- }
- }
- }
- return $value;
- }
- private function doValidateType($value): void
- {
- if (null !== $this->handlingPlaceholder && !$this->allowPlaceholders()) {
- $e = new InvalidTypeException(sprintf('A dynamic value is not compatible with a "%s" node type at path "%s".', static::class, $this->getPath()));
- $e->setPath($this->getPath());
- throw $e;
- }
- if (null === $this->handlingPlaceholder || null === $value) {
- $this->validateType($value);
- return;
- }
- $knownTypes = array_keys(self::$placeholders[$this->handlingPlaceholder]);
- $validTypes = $this->getValidPlaceholderTypes();
- if ($validTypes && array_diff($knownTypes, $validTypes)) {
- $e = new InvalidTypeException(sprintf(
- 'Invalid type for path "%s". Expected %s, but got %s.',
- $this->getPath(),
- 1 === \count($validTypes) ? '"'.reset($validTypes).'"' : 'one of "'.implode('", "', $validTypes).'"',
- 1 === \count($knownTypes) ? '"'.reset($knownTypes).'"' : 'one of "'.implode('", "', $knownTypes).'"'
- ));
- if ($hint = $this->getInfo()) {
- $e->addHint($hint);
- }
- $e->setPath($this->getPath());
- throw $e;
- }
- $this->validateType($value);
- }
- }
|