590 lines
16 KiB
PHP
590 lines
16 KiB
PHP
|
<?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);
|
||
|
}
|
||
|
}
|