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\Routing; 13 14/** 15 * A Route describes a route and its parameters. 16 * 17 * @author Fabien Potencier <fabien@symfony.com> 18 * @author Tobias Schultze <http://tobion.de> 19 */ 20class Route implements \Serializable 21{ 22 private $path = '/'; 23 private $host = ''; 24 private $schemes = []; 25 private $methods = []; 26 private $defaults = []; 27 private $requirements = []; 28 private $options = []; 29 private $condition = ''; 30 31 /** 32 * @var CompiledRoute|null 33 */ 34 private $compiled; 35 36 /** 37 * Constructor. 38 * 39 * Available options: 40 * 41 * * compiler_class: A class name able to compile this route instance (RouteCompiler by default) 42 * * utf8: Whether UTF-8 matching is enforced ot not 43 * 44 * @param string $path The path pattern to match 45 * @param array $defaults An array of default parameter values 46 * @param array $requirements An array of requirements for parameters (regexes) 47 * @param array $options An array of options 48 * @param string|null $host The host pattern to match 49 * @param string|string[] $schemes A required URI scheme or an array of restricted schemes 50 * @param string|string[] $methods A required HTTP method or an array of restricted methods 51 * @param string|null $condition A condition that should evaluate to true for the route to match 52 */ 53 public function __construct(string $path, array $defaults = [], array $requirements = [], array $options = [], ?string $host = '', $schemes = [], $methods = [], ?string $condition = '') 54 { 55 $this->setPath($path); 56 $this->addDefaults($defaults); 57 $this->addRequirements($requirements); 58 $this->setOptions($options); 59 $this->setHost($host); 60 $this->setSchemes($schemes); 61 $this->setMethods($methods); 62 $this->setCondition($condition); 63 } 64 65 public function __serialize(): array 66 { 67 return [ 68 'path' => $this->path, 69 'host' => $this->host, 70 'defaults' => $this->defaults, 71 'requirements' => $this->requirements, 72 'options' => $this->options, 73 'schemes' => $this->schemes, 74 'methods' => $this->methods, 75 'condition' => $this->condition, 76 'compiled' => $this->compiled, 77 ]; 78 } 79 80 /** 81 * @internal 82 */ 83 final public function serialize(): string 84 { 85 return serialize($this->__serialize()); 86 } 87 88 public function __unserialize(array $data): void 89 { 90 $this->path = $data['path']; 91 $this->host = $data['host']; 92 $this->defaults = $data['defaults']; 93 $this->requirements = $data['requirements']; 94 $this->options = $data['options']; 95 $this->schemes = $data['schemes']; 96 $this->methods = $data['methods']; 97 98 if (isset($data['condition'])) { 99 $this->condition = $data['condition']; 100 } 101 if (isset($data['compiled'])) { 102 $this->compiled = $data['compiled']; 103 } 104 } 105 106 /** 107 * @internal 108 */ 109 final public function unserialize($serialized) 110 { 111 $this->__unserialize(unserialize($serialized)); 112 } 113 114 /** 115 * @return string 116 */ 117 public function getPath() 118 { 119 return $this->path; 120 } 121 122 /** 123 * @return $this 124 */ 125 public function setPath(string $pattern) 126 { 127 $pattern = $this->extractInlineDefaultsAndRequirements($pattern); 128 129 // A pattern must start with a slash and must not have multiple slashes at the beginning because the 130 // generated path for this route would be confused with a network path, e.g. '//domain.com/path'. 131 $this->path = '/'.ltrim(trim($pattern), '/'); 132 $this->compiled = null; 133 134 return $this; 135 } 136 137 /** 138 * @return string 139 */ 140 public function getHost() 141 { 142 return $this->host; 143 } 144 145 /** 146 * @return $this 147 */ 148 public function setHost(?string $pattern) 149 { 150 $this->host = $this->extractInlineDefaultsAndRequirements((string) $pattern); 151 $this->compiled = null; 152 153 return $this; 154 } 155 156 /** 157 * Returns the lowercased schemes this route is restricted to. 158 * So an empty array means that any scheme is allowed. 159 * 160 * @return string[] 161 */ 162 public function getSchemes() 163 { 164 return $this->schemes; 165 } 166 167 /** 168 * Sets the schemes (e.g. 'https') this route is restricted to. 169 * So an empty array means that any scheme is allowed. 170 * 171 * @param string|string[] $schemes The scheme or an array of schemes 172 * 173 * @return $this 174 */ 175 public function setSchemes($schemes) 176 { 177 $this->schemes = array_map('strtolower', (array) $schemes); 178 $this->compiled = null; 179 180 return $this; 181 } 182 183 /** 184 * Checks if a scheme requirement has been set. 185 * 186 * @return bool 187 */ 188 public function hasScheme(string $scheme) 189 { 190 return \in_array(strtolower($scheme), $this->schemes, true); 191 } 192 193 /** 194 * Returns the uppercased HTTP methods this route is restricted to. 195 * So an empty array means that any method is allowed. 196 * 197 * @return string[] 198 */ 199 public function getMethods() 200 { 201 return $this->methods; 202 } 203 204 /** 205 * Sets the HTTP methods (e.g. 'POST') this route is restricted to. 206 * So an empty array means that any method is allowed. 207 * 208 * @param string|string[] $methods The method or an array of methods 209 * 210 * @return $this 211 */ 212 public function setMethods($methods) 213 { 214 $this->methods = array_map('strtoupper', (array) $methods); 215 $this->compiled = null; 216 217 return $this; 218 } 219 220 /** 221 * @return array 222 */ 223 public function getOptions() 224 { 225 return $this->options; 226 } 227 228 /** 229 * @return $this 230 */ 231 public function setOptions(array $options) 232 { 233 $this->options = [ 234 'compiler_class' => 'Symfony\\Component\\Routing\\RouteCompiler', 235 ]; 236 237 return $this->addOptions($options); 238 } 239 240 /** 241 * @return $this 242 */ 243 public function addOptions(array $options) 244 { 245 foreach ($options as $name => $option) { 246 $this->options[$name] = $option; 247 } 248 $this->compiled = null; 249 250 return $this; 251 } 252 253 /** 254 * Sets an option value. 255 * 256 * @param mixed $value The option value 257 * 258 * @return $this 259 */ 260 public function setOption(string $name, $value) 261 { 262 $this->options[$name] = $value; 263 $this->compiled = null; 264 265 return $this; 266 } 267 268 /** 269 * Returns the option value or null when not found. 270 * 271 * @return mixed 272 */ 273 public function getOption(string $name) 274 { 275 return $this->options[$name] ?? null; 276 } 277 278 /** 279 * @return bool 280 */ 281 public function hasOption(string $name) 282 { 283 return \array_key_exists($name, $this->options); 284 } 285 286 /** 287 * @return array 288 */ 289 public function getDefaults() 290 { 291 return $this->defaults; 292 } 293 294 /** 295 * @return $this 296 */ 297 public function setDefaults(array $defaults) 298 { 299 $this->defaults = []; 300 301 return $this->addDefaults($defaults); 302 } 303 304 /** 305 * @return $this 306 */ 307 public function addDefaults(array $defaults) 308 { 309 if (isset($defaults['_locale']) && $this->isLocalized()) { 310 unset($defaults['_locale']); 311 } 312 313 foreach ($defaults as $name => $default) { 314 $this->defaults[$name] = $default; 315 } 316 $this->compiled = null; 317 318 return $this; 319 } 320 321 /** 322 * @return mixed 323 */ 324 public function getDefault(string $name) 325 { 326 return $this->defaults[$name] ?? null; 327 } 328 329 /** 330 * @return bool 331 */ 332 public function hasDefault(string $name) 333 { 334 return \array_key_exists($name, $this->defaults); 335 } 336 337 /** 338 * Sets a default value. 339 * 340 * @param mixed $default The default value 341 * 342 * @return $this 343 */ 344 public function setDefault(string $name, $default) 345 { 346 if ('_locale' === $name && $this->isLocalized()) { 347 return $this; 348 } 349 350 $this->defaults[$name] = $default; 351 $this->compiled = null; 352 353 return $this; 354 } 355 356 /** 357 * @return array 358 */ 359 public function getRequirements() 360 { 361 return $this->requirements; 362 } 363 364 /** 365 * @return $this 366 */ 367 public function setRequirements(array $requirements) 368 { 369 $this->requirements = []; 370 371 return $this->addRequirements($requirements); 372 } 373 374 /** 375 * @return $this 376 */ 377 public function addRequirements(array $requirements) 378 { 379 if (isset($requirements['_locale']) && $this->isLocalized()) { 380 unset($requirements['_locale']); 381 } 382 383 foreach ($requirements as $key => $regex) { 384 $this->requirements[$key] = $this->sanitizeRequirement($key, $regex); 385 } 386 $this->compiled = null; 387 388 return $this; 389 } 390 391 /** 392 * @return string|null 393 */ 394 public function getRequirement(string $key) 395 { 396 return $this->requirements[$key] ?? null; 397 } 398 399 /** 400 * @return bool 401 */ 402 public function hasRequirement(string $key) 403 { 404 return \array_key_exists($key, $this->requirements); 405 } 406 407 /** 408 * @return $this 409 */ 410 public function setRequirement(string $key, string $regex) 411 { 412 if ('_locale' === $key && $this->isLocalized()) { 413 return $this; 414 } 415 416 $this->requirements[$key] = $this->sanitizeRequirement($key, $regex); 417 $this->compiled = null; 418 419 return $this; 420 } 421 422 /** 423 * @return string 424 */ 425 public function getCondition() 426 { 427 return $this->condition; 428 } 429 430 /** 431 * @return $this 432 */ 433 public function setCondition(?string $condition) 434 { 435 $this->condition = (string) $condition; 436 $this->compiled = null; 437 438 return $this; 439 } 440 441 /** 442 * Compiles the route. 443 * 444 * @return CompiledRoute 445 * 446 * @throws \LogicException If the Route cannot be compiled because the 447 * path or host pattern is invalid 448 * 449 * @see RouteCompiler which is responsible for the compilation process 450 */ 451 public function compile() 452 { 453 if (null !== $this->compiled) { 454 return $this->compiled; 455 } 456 457 $class = $this->getOption('compiler_class'); 458 459 return $this->compiled = $class::compile($this); 460 } 461 462 private function extractInlineDefaultsAndRequirements(string $pattern): string 463 { 464 if (false === strpbrk($pattern, '?<')) { 465 return $pattern; 466 } 467 468 return preg_replace_callback('#\{(!?)(\w++)(<.*?>)?(\?[^\}]*+)?\}#', function ($m) { 469 if (isset($m[4][0])) { 470 $this->setDefault($m[2], '?' !== $m[4] ? substr($m[4], 1) : null); 471 } 472 if (isset($m[3][0])) { 473 $this->setRequirement($m[2], substr($m[3], 1, -1)); 474 } 475 476 return '{'.$m[1].$m[2].'}'; 477 }, $pattern); 478 } 479 480 private function sanitizeRequirement(string $key, string $regex) 481 { 482 if ('' !== $regex) { 483 if ('^' === $regex[0]) { 484 $regex = substr($regex, 1); 485 } elseif (0 === strpos($regex, '\\A')) { 486 $regex = substr($regex, 2); 487 } 488 } 489 490 if (str_ends_with($regex, '$')) { 491 $regex = substr($regex, 0, -1); 492 } elseif (\strlen($regex) - 2 === strpos($regex, '\\z')) { 493 $regex = substr($regex, 0, -2); 494 } 495 496 if ('' === $regex) { 497 throw new \InvalidArgumentException(sprintf('Routing requirement for "%s" cannot be empty.', $key)); 498 } 499 500 return $regex; 501 } 502 503 private function isLocalized(): bool 504 { 505 return isset($this->defaults['_locale']) && isset($this->defaults['_canonical_route']) && ($this->requirements['_locale'] ?? null) === preg_quote($this->defaults['_locale']); 506 } 507} 508