You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

581 lines
14 KiB

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