1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\Config\Definition; 13 14use Symfony\Component\Config\Definition\Exception\Exception; 15use Symfony\Component\Config\Definition\Exception\ForbiddenOverwriteException; 16use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; 17use Symfony\Component\Config\Definition\Exception\InvalidTypeException; 18use Symfony\Component\Config\Definition\Exception\UnsetKeyException; 19 20/** 21 * The base node class. 22 * 23 * @author Johannes M. Schmitt <schmittjoh@gmail.com> 24 */ 25abstract class BaseNode implements NodeInterface 26{ 27 const DEFAULT_PATH_SEPARATOR = '.'; 28 29 private static $placeholderUniquePrefix; 30 private static $placeholders = []; 31 32 protected $name; 33 protected $parent; 34 protected $normalizationClosures = []; 35 protected $finalValidationClosures = []; 36 protected $allowOverwrite = true; 37 protected $required = false; 38 protected $deprecationMessage = null; 39 protected $equivalentValues = []; 40 protected $attributes = []; 41 protected $pathSeparator; 42 43 private $handlingPlaceholder; 44 45 /** 46 * @throws \InvalidArgumentException if the name contains a period 47 */ 48 public function __construct(?string $name, NodeInterface $parent = null, string $pathSeparator = self::DEFAULT_PATH_SEPARATOR) 49 { 50 if (false !== strpos($name = (string) $name, $pathSeparator)) { 51 throw new \InvalidArgumentException('The name must not contain ".'.$pathSeparator.'".'); 52 } 53 54 $this->name = $name; 55 $this->parent = $parent; 56 $this->pathSeparator = $pathSeparator; 57 } 58 59 /** 60 * Register possible (dummy) values for a dynamic placeholder value. 61 * 62 * Matching configuration values will be processed with a provided value, one by one. After a provided value is 63 * successfully processed the configuration value is returned as is, thus preserving the placeholder. 64 * 65 * @internal 66 */ 67 public static function setPlaceholder(string $placeholder, array $values): void 68 { 69 if (!$values) { 70 throw new \InvalidArgumentException('At least one value must be provided.'); 71 } 72 73 self::$placeholders[$placeholder] = $values; 74 } 75 76 /** 77 * Sets a common prefix for dynamic placeholder values. 78 * 79 * Matching configuration values will be skipped from being processed and are returned as is, thus preserving the 80 * placeholder. An exact match provided by {@see setPlaceholder()} might take precedence. 81 * 82 * @internal 83 */ 84 public static function setPlaceholderUniquePrefix(string $prefix): void 85 { 86 self::$placeholderUniquePrefix = $prefix; 87 } 88 89 /** 90 * Resets all current placeholders available. 91 * 92 * @internal 93 */ 94 public static function resetPlaceholders(): void 95 { 96 self::$placeholderUniquePrefix = null; 97 self::$placeholders = []; 98 } 99 100 public function setAttribute(string $key, $value) 101 { 102 $this->attributes[$key] = $value; 103 } 104 105 /** 106 * @return mixed 107 */ 108 public function getAttribute(string $key, $default = null) 109 { 110 return isset($this->attributes[$key]) ? $this->attributes[$key] : $default; 111 } 112 113 /** 114 * @return bool 115 */ 116 public function hasAttribute(string $key) 117 { 118 return isset($this->attributes[$key]); 119 } 120 121 /** 122 * @return array 123 */ 124 public function getAttributes() 125 { 126 return $this->attributes; 127 } 128 129 public function setAttributes(array $attributes) 130 { 131 $this->attributes = $attributes; 132 } 133 134 public function removeAttribute(string $key) 135 { 136 unset($this->attributes[$key]); 137 } 138 139 /** 140 * Sets an info message. 141 */ 142 public function setInfo(string $info) 143 { 144 $this->setAttribute('info', $info); 145 } 146 147 /** 148 * Returns info message. 149 * 150 * @return string|null The info text 151 */ 152 public function getInfo() 153 { 154 return $this->getAttribute('info'); 155 } 156 157 /** 158 * Sets the example configuration for this node. 159 * 160 * @param string|array $example 161 */ 162 public function setExample($example) 163 { 164 $this->setAttribute('example', $example); 165 } 166 167 /** 168 * Retrieves the example configuration for this node. 169 * 170 * @return string|array|null The example 171 */ 172 public function getExample() 173 { 174 return $this->getAttribute('example'); 175 } 176 177 /** 178 * Adds an equivalent value. 179 * 180 * @param mixed $originalValue 181 * @param mixed $equivalentValue 182 */ 183 public function addEquivalentValue($originalValue, $equivalentValue) 184 { 185 $this->equivalentValues[] = [$originalValue, $equivalentValue]; 186 } 187 188 /** 189 * Set this node as required. 190 * 191 * @param bool $boolean Required node 192 */ 193 public function setRequired(bool $boolean) 194 { 195 $this->required = $boolean; 196 } 197 198 /** 199 * Sets this node as deprecated. 200 * 201 * You can use %node% and %path% placeholders in your message to display, 202 * respectively, the node name and its complete path. 203 */ 204 public function setDeprecated(?string $message) 205 { 206 $this->deprecationMessage = $message; 207 } 208 209 /** 210 * Sets if this node can be overridden. 211 */ 212 public function setAllowOverwrite(bool $allow) 213 { 214 $this->allowOverwrite = $allow; 215 } 216 217 /** 218 * Sets the closures used for normalization. 219 * 220 * @param \Closure[] $closures An array of Closures used for normalization 221 */ 222 public function setNormalizationClosures(array $closures) 223 { 224 $this->normalizationClosures = $closures; 225 } 226 227 /** 228 * Sets the closures used for final validation. 229 * 230 * @param \Closure[] $closures An array of Closures used for final validation 231 */ 232 public function setFinalValidationClosures(array $closures) 233 { 234 $this->finalValidationClosures = $closures; 235 } 236 237 /** 238 * {@inheritdoc} 239 */ 240 public function isRequired() 241 { 242 return $this->required; 243 } 244 245 /** 246 * Checks if this node is deprecated. 247 * 248 * @return bool 249 */ 250 public function isDeprecated() 251 { 252 return null !== $this->deprecationMessage; 253 } 254 255 /** 256 * Returns the deprecated message. 257 * 258 * @param string $node the configuration node name 259 * @param string $path the path of the node 260 * 261 * @return string 262 */ 263 public function getDeprecationMessage(string $node, string $path) 264 { 265 return strtr($this->deprecationMessage, ['%node%' => $node, '%path%' => $path]); 266 } 267 268 /** 269 * {@inheritdoc} 270 */ 271 public function getName() 272 { 273 return $this->name; 274 } 275 276 /** 277 * {@inheritdoc} 278 */ 279 public function getPath() 280 { 281 if (null !== $this->parent) { 282 return $this->parent->getPath().$this->pathSeparator.$this->name; 283 } 284 285 return $this->name; 286 } 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 297 if ($leftSide !== $leftPlaceholders = self::resolvePlaceholderValue($leftSide)) { 298 foreach ($leftPlaceholders as $leftPlaceholder) { 299 $this->handlingPlaceholder = $leftSide; 300 try { 301 $this->merge($leftPlaceholder, $rightSide); 302 } finally { 303 $this->handlingPlaceholder = null; 304 } 305 } 306 307 return $rightSide; 308 } 309 310 if ($rightSide !== $rightPlaceholders = self::resolvePlaceholderValue($rightSide)) { 311 foreach ($rightPlaceholders as $rightPlaceholder) { 312 $this->handlingPlaceholder = $rightSide; 313 try { 314 $this->merge($leftSide, $rightPlaceholder); 315 } finally { 316 $this->handlingPlaceholder = null; 317 } 318 } 319 320 return $rightSide; 321 } 322 323 $this->doValidateType($leftSide); 324 $this->doValidateType($rightSide); 325 326 return $this->mergeValues($leftSide, $rightSide); 327 } 328 329 /** 330 * {@inheritdoc} 331 */ 332 final public function normalize($value) 333 { 334 $value = $this->preNormalize($value); 335 336 // run custom normalization closures 337 foreach ($this->normalizationClosures as $closure) { 338 $value = $closure($value); 339 } 340 341 // resolve placeholder value 342 if ($value !== $placeholders = self::resolvePlaceholderValue($value)) { 343 foreach ($placeholders as $placeholder) { 344 $this->handlingPlaceholder = $value; 345 try { 346 $this->normalize($placeholder); 347 } finally { 348 $this->handlingPlaceholder = null; 349 } 350 } 351 352 return $value; 353 } 354 355 // replace value with their equivalent 356 foreach ($this->equivalentValues as $data) { 357 if ($data[0] === $value) { 358 $value = $data[1]; 359 } 360 } 361 362 // validate type 363 $this->doValidateType($value); 364 365 // normalize value 366 return $this->normalizeValue($value); 367 } 368 369 /** 370 * Normalizes the value before any other normalization is applied. 371 * 372 * @param mixed $value 373 * 374 * @return mixed The normalized array value 375 */ 376 protected function preNormalize($value) 377 { 378 return $value; 379 } 380 381 /** 382 * Returns parent node for this node. 383 * 384 * @return NodeInterface|null 385 */ 386 public function getParent() 387 { 388 return $this->parent; 389 } 390 391 /** 392 * {@inheritdoc} 393 */ 394 final public function finalize($value) 395 { 396 if ($value !== $placeholders = self::resolvePlaceholderValue($value)) { 397 foreach ($placeholders as $placeholder) { 398 $this->handlingPlaceholder = $value; 399 try { 400 $this->finalize($placeholder); 401 } finally { 402 $this->handlingPlaceholder = null; 403 } 404 } 405 406 return $value; 407 } 408 409 $this->doValidateType($value); 410 411 $value = $this->finalizeValue($value); 412 413 // Perform validation on the final value if a closure has been set. 414 // The closure is also allowed to return another value. 415 foreach ($this->finalValidationClosures as $closure) { 416 try { 417 $value = $closure($value); 418 } catch (Exception $e) { 419 if ($e instanceof UnsetKeyException && null !== $this->handlingPlaceholder) { 420 continue; 421 } 422 423 throw $e; 424 } catch (\Exception $e) { 425 throw new InvalidConfigurationException(sprintf('Invalid configuration for path "%s": %s.', $this->getPath(), $e->getMessage()), $e->getCode(), $e); 426 } 427 } 428 429 return $value; 430 } 431 432 /** 433 * Validates the type of a Node. 434 * 435 * @param mixed $value The value to validate 436 * 437 * @throws InvalidTypeException when the value is invalid 438 */ 439 abstract protected function validateType($value); 440 441 /** 442 * Normalizes the value. 443 * 444 * @param mixed $value The value to normalize 445 * 446 * @return mixed The normalized value 447 */ 448 abstract protected function normalizeValue($value); 449 450 /** 451 * Merges two values together. 452 * 453 * @param mixed $leftSide 454 * @param mixed $rightSide 455 * 456 * @return mixed The merged value 457 */ 458 abstract protected function mergeValues($leftSide, $rightSide); 459 460 /** 461 * Finalizes a value. 462 * 463 * @param mixed $value The value to finalize 464 * 465 * @return mixed The finalized value 466 */ 467 abstract protected function finalizeValue($value); 468 469 /** 470 * Tests if placeholder values are allowed for this node. 471 */ 472 protected function allowPlaceholders(): bool 473 { 474 return true; 475 } 476 477 /** 478 * Tests if a placeholder is being handled currently. 479 */ 480 protected function isHandlingPlaceholder(): bool 481 { 482 return null !== $this->handlingPlaceholder; 483 } 484 485 /** 486 * Gets allowed dynamic types for this node. 487 */ 488 protected function getValidPlaceholderTypes(): array 489 { 490 return []; 491 } 492 493 private static function resolvePlaceholderValue($value) 494 { 495 if (\is_string($value)) { 496 if (isset(self::$placeholders[$value])) { 497 return self::$placeholders[$value]; 498 } 499 500 if (self::$placeholderUniquePrefix && 0 === strpos($value, self::$placeholderUniquePrefix)) { 501 return []; 502 } 503 } 504 505 return $value; 506 } 507 508 private function doValidateType($value): void 509 { 510 if (null !== $this->handlingPlaceholder && !$this->allowPlaceholders()) { 511 $e = new InvalidTypeException(sprintf('A dynamic value is not compatible with a "%s" node type at path "%s".', static::class, $this->getPath())); 512 $e->setPath($this->getPath()); 513 514 throw $e; 515 } 516 517 if (null === $this->handlingPlaceholder || null === $value) { 518 $this->validateType($value); 519 520 return; 521 } 522 523 $knownTypes = array_keys(self::$placeholders[$this->handlingPlaceholder]); 524 $validTypes = $this->getValidPlaceholderTypes(); 525 526 if ($validTypes && array_diff($knownTypes, $validTypes)) { 527 $e = new InvalidTypeException(sprintf( 528 'Invalid type for path "%s". Expected %s, but got %s.', 529 $this->getPath(), 530 1 === \count($validTypes) ? '"'.reset($validTypes).'"' : 'one of "'.implode('", "', $validTypes).'"', 531 1 === \count($knownTypes) ? '"'.reset($knownTypes).'"' : 'one of "'.implode('", "', $knownTypes).'"' 532 )); 533 if ($hint = $this->getInfo()) { 534 $e->addHint($hint); 535 } 536 $e->setPath($this->getPath()); 537 538 throw $e; 539 } 540 541 $this->validateType($value); 542 } 543} 544