You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
589 lines
16 KiB
589 lines
16 KiB
<?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 (str_contains($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 |
|
*/ |
|
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 |
|
*/ |
|
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. |
|
*/ |
|
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 |
|
*/ |
|
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 |
|
*/ |
|
abstract protected function normalizeValue($value); |
|
|
|
/** |
|
* Merges two values together. |
|
* |
|
* @param mixed $leftSide |
|
* @param mixed $rightSide |
|
* |
|
* @return mixed |
|
*/ |
|
abstract protected function mergeValues($leftSide, $rightSide); |
|
|
|
/** |
|
* Finalizes a value. |
|
* |
|
* @param mixed $value The value to finalize |
|
* |
|
* @return mixed |
|
*/ |
|
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 (str_starts_with($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); |
|
} |
|
}
|
|
|