BaseNode.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  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. const DEFAULT_PATH_SEPARATOR = '.';
  24. private static $placeholderUniquePrefix;
  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 $deprecationMessage = null;
  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. * Sets 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::$placeholderUniquePrefix = $prefix;
  75. }
  76. /**
  77. * Resets all current placeholders available.
  78. *
  79. * @internal
  80. */
  81. public static function resetPlaceholders(): void
  82. {
  83. self::$placeholderUniquePrefix = null;
  84. self::$placeholders = [];
  85. }
  86. public function setAttribute($key, $value)
  87. {
  88. $this->attributes[$key] = $value;
  89. }
  90. public function getAttribute($key, $default = null)
  91. {
  92. return isset($this->attributes[$key]) ? $this->attributes[$key] : $default;
  93. }
  94. public function hasAttribute($key)
  95. {
  96. return isset($this->attributes[$key]);
  97. }
  98. public function getAttributes()
  99. {
  100. return $this->attributes;
  101. }
  102. public function setAttributes(array $attributes)
  103. {
  104. $this->attributes = $attributes;
  105. }
  106. public function removeAttribute($key)
  107. {
  108. unset($this->attributes[$key]);
  109. }
  110. /**
  111. * Sets an info message.
  112. *
  113. * @param string $info
  114. */
  115. public function setInfo($info)
  116. {
  117. $this->setAttribute('info', $info);
  118. }
  119. /**
  120. * Returns info message.
  121. *
  122. * @return string The info text
  123. */
  124. public function getInfo()
  125. {
  126. return $this->getAttribute('info');
  127. }
  128. /**
  129. * Sets the example configuration for this node.
  130. *
  131. * @param string|array $example
  132. */
  133. public function setExample($example)
  134. {
  135. $this->setAttribute('example', $example);
  136. }
  137. /**
  138. * Retrieves the example configuration for this node.
  139. *
  140. * @return string|array The example
  141. */
  142. public function getExample()
  143. {
  144. return $this->getAttribute('example');
  145. }
  146. /**
  147. * Adds an equivalent value.
  148. *
  149. * @param mixed $originalValue
  150. * @param mixed $equivalentValue
  151. */
  152. public function addEquivalentValue($originalValue, $equivalentValue)
  153. {
  154. $this->equivalentValues[] = [$originalValue, $equivalentValue];
  155. }
  156. /**
  157. * Set this node as required.
  158. *
  159. * @param bool $boolean Required node
  160. */
  161. public function setRequired($boolean)
  162. {
  163. $this->required = (bool) $boolean;
  164. }
  165. /**
  166. * Sets this node as deprecated.
  167. *
  168. * You can use %node% and %path% placeholders in your message to display,
  169. * respectively, the node name and its complete path.
  170. *
  171. * @param string|null $message Deprecated message
  172. */
  173. public function setDeprecated($message)
  174. {
  175. $this->deprecationMessage = $message;
  176. }
  177. /**
  178. * Sets if this node can be overridden.
  179. *
  180. * @param bool $allow
  181. */
  182. public function setAllowOverwrite($allow)
  183. {
  184. $this->allowOverwrite = (bool) $allow;
  185. }
  186. /**
  187. * Sets the closures used for normalization.
  188. *
  189. * @param \Closure[] $closures An array of Closures used for normalization
  190. */
  191. public function setNormalizationClosures(array $closures)
  192. {
  193. $this->normalizationClosures = $closures;
  194. }
  195. /**
  196. * Sets the closures used for final validation.
  197. *
  198. * @param \Closure[] $closures An array of Closures used for final validation
  199. */
  200. public function setFinalValidationClosures(array $closures)
  201. {
  202. $this->finalValidationClosures = $closures;
  203. }
  204. /**
  205. * {@inheritdoc}
  206. */
  207. public function isRequired()
  208. {
  209. return $this->required;
  210. }
  211. /**
  212. * Checks if this node is deprecated.
  213. *
  214. * @return bool
  215. */
  216. public function isDeprecated()
  217. {
  218. return null !== $this->deprecationMessage;
  219. }
  220. /**
  221. * Returns the deprecated message.
  222. *
  223. * @param string $node the configuration node name
  224. * @param string $path the path of the node
  225. *
  226. * @return string
  227. */
  228. public function getDeprecationMessage($node, $path)
  229. {
  230. return strtr($this->deprecationMessage, ['%node%' => $node, '%path%' => $path]);
  231. }
  232. /**
  233. * {@inheritdoc}
  234. */
  235. public function getName()
  236. {
  237. return $this->name;
  238. }
  239. /**
  240. * {@inheritdoc}
  241. */
  242. public function getPath()
  243. {
  244. if (null !== $this->parent) {
  245. return $this->parent->getPath().$this->pathSeparator.$this->name;
  246. }
  247. return $this->name;
  248. }
  249. /**
  250. * {@inheritdoc}
  251. */
  252. final public function merge($leftSide, $rightSide)
  253. {
  254. if (!$this->allowOverwrite) {
  255. 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()));
  256. }
  257. if ($leftSide !== $leftPlaceholders = self::resolvePlaceholderValue($leftSide)) {
  258. foreach ($leftPlaceholders as $leftPlaceholder) {
  259. $this->handlingPlaceholder = $leftSide;
  260. try {
  261. $this->merge($leftPlaceholder, $rightSide);
  262. } finally {
  263. $this->handlingPlaceholder = null;
  264. }
  265. }
  266. return $rightSide;
  267. }
  268. if ($rightSide !== $rightPlaceholders = self::resolvePlaceholderValue($rightSide)) {
  269. foreach ($rightPlaceholders as $rightPlaceholder) {
  270. $this->handlingPlaceholder = $rightSide;
  271. try {
  272. $this->merge($leftSide, $rightPlaceholder);
  273. } finally {
  274. $this->handlingPlaceholder = null;
  275. }
  276. }
  277. return $rightSide;
  278. }
  279. $this->doValidateType($leftSide);
  280. $this->doValidateType($rightSide);
  281. return $this->mergeValues($leftSide, $rightSide);
  282. }
  283. /**
  284. * {@inheritdoc}
  285. */
  286. final public function normalize($value)
  287. {
  288. $value = $this->preNormalize($value);
  289. // run custom normalization closures
  290. foreach ($this->normalizationClosures as $closure) {
  291. $value = $closure($value);
  292. }
  293. // resolve placeholder value
  294. if ($value !== $placeholders = self::resolvePlaceholderValue($value)) {
  295. foreach ($placeholders as $placeholder) {
  296. $this->handlingPlaceholder = $value;
  297. try {
  298. $this->normalize($placeholder);
  299. } finally {
  300. $this->handlingPlaceholder = null;
  301. }
  302. }
  303. return $value;
  304. }
  305. // replace value with their equivalent
  306. foreach ($this->equivalentValues as $data) {
  307. if ($data[0] === $value) {
  308. $value = $data[1];
  309. }
  310. }
  311. // validate type
  312. $this->doValidateType($value);
  313. // normalize value
  314. return $this->normalizeValue($value);
  315. }
  316. /**
  317. * Normalizes the value before any other normalization is applied.
  318. *
  319. * @param $value
  320. *
  321. * @return The normalized array value
  322. */
  323. protected function preNormalize($value)
  324. {
  325. return $value;
  326. }
  327. /**
  328. * Returns parent node for this node.
  329. *
  330. * @return NodeInterface|null
  331. */
  332. public function getParent()
  333. {
  334. return $this->parent;
  335. }
  336. /**
  337. * {@inheritdoc}
  338. */
  339. final public function finalize($value)
  340. {
  341. if ($value !== $placeholders = self::resolvePlaceholderValue($value)) {
  342. foreach ($placeholders as $placeholder) {
  343. $this->handlingPlaceholder = $value;
  344. try {
  345. $this->finalize($placeholder);
  346. } finally {
  347. $this->handlingPlaceholder = null;
  348. }
  349. }
  350. return $value;
  351. }
  352. $this->doValidateType($value);
  353. $value = $this->finalizeValue($value);
  354. // Perform validation on the final value if a closure has been set.
  355. // The closure is also allowed to return another value.
  356. foreach ($this->finalValidationClosures as $closure) {
  357. try {
  358. $value = $closure($value);
  359. } catch (Exception $e) {
  360. if ($e instanceof UnsetKeyException && null !== $this->handlingPlaceholder) {
  361. continue;
  362. }
  363. throw $e;
  364. } catch (\Exception $e) {
  365. throw new InvalidConfigurationException(sprintf('Invalid configuration for path "%s": %s', $this->getPath(), $e->getMessage()), $e->getCode(), $e);
  366. }
  367. }
  368. return $value;
  369. }
  370. /**
  371. * Validates the type of a Node.
  372. *
  373. * @param mixed $value The value to validate
  374. *
  375. * @throws InvalidTypeException when the value is invalid
  376. */
  377. abstract protected function validateType($value);
  378. /**
  379. * Normalizes the value.
  380. *
  381. * @param mixed $value The value to normalize
  382. *
  383. * @return mixed The normalized value
  384. */
  385. abstract protected function normalizeValue($value);
  386. /**
  387. * Merges two values together.
  388. *
  389. * @param mixed $leftSide
  390. * @param mixed $rightSide
  391. *
  392. * @return mixed The merged value
  393. */
  394. abstract protected function mergeValues($leftSide, $rightSide);
  395. /**
  396. * Finalizes a value.
  397. *
  398. * @param mixed $value The value to finalize
  399. *
  400. * @return mixed The finalized value
  401. */
  402. abstract protected function finalizeValue($value);
  403. /**
  404. * Tests if placeholder values are allowed for this node.
  405. */
  406. protected function allowPlaceholders(): bool
  407. {
  408. return true;
  409. }
  410. /**
  411. * Tests if a placeholder is being handled currently.
  412. */
  413. protected function isHandlingPlaceholder(): bool
  414. {
  415. return null !== $this->handlingPlaceholder;
  416. }
  417. /**
  418. * Gets allowed dynamic types for this node.
  419. */
  420. protected function getValidPlaceholderTypes(): array
  421. {
  422. return [];
  423. }
  424. private static function resolvePlaceholderValue($value)
  425. {
  426. if (\is_string($value)) {
  427. if (isset(self::$placeholders[$value])) {
  428. return self::$placeholders[$value];
  429. }
  430. if (self::$placeholderUniquePrefix && 0 === strpos($value, self::$placeholderUniquePrefix)) {
  431. return [];
  432. }
  433. }
  434. return $value;
  435. }
  436. private function doValidateType($value): void
  437. {
  438. if (null !== $this->handlingPlaceholder && !$this->allowPlaceholders()) {
  439. $e = new InvalidTypeException(sprintf('A dynamic value is not compatible with a "%s" node type at path "%s".', \get_class($this), $this->getPath()));
  440. $e->setPath($this->getPath());
  441. throw $e;
  442. }
  443. if (null === $this->handlingPlaceholder || null === $value) {
  444. $this->validateType($value);
  445. return;
  446. }
  447. $knownTypes = array_keys(self::$placeholders[$this->handlingPlaceholder]);
  448. $validTypes = $this->getValidPlaceholderTypes();
  449. if ($validTypes && array_diff($knownTypes, $validTypes)) {
  450. $e = new InvalidTypeException(sprintf(
  451. 'Invalid type for path "%s". Expected %s, but got %s.',
  452. $this->getPath(),
  453. 1 === \count($validTypes) ? '"'.reset($validTypes).'"' : 'one of "'.implode('", "', $validTypes).'"',
  454. 1 === \count($knownTypes) ? '"'.reset($knownTypes).'"' : 'one of "'.implode('", "', $knownTypes).'"'
  455. ));
  456. if ($hint = $this->getInfo()) {
  457. $e->addHint($hint);
  458. }
  459. $e->setPath($this->getPath());
  460. throw $e;
  461. }
  462. $this->validateType($value);
  463. }
  464. }