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 <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 $name, ?NodeInterface $parent = null, string $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 $node, string $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 $node, string $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. 1 === \count($validTypes) ? '"'.reset($validTypes).'"' : 'one of "'.implode('", "', $validTypes).'"',
  495. 1 === \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. }