BaseNode.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  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 (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 $node, string $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 $node, string $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 (0 === 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. 1 === \count($validTypes) ? '"'.reset($validTypes).'"' : 'one of "'.implode('", "', $validTypes).'"',
  497. 1 === \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. }