PageRenderTime 44ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/libs/Nette/Application/Routers/Route.php

https://github.com/Vrtak-CZ/ORM-benchmark
PHP | 750 lines | 473 code | 156 blank | 121 comment | 93 complexity | 5fd9724a6a7b4a38a488ab14a76f7a0d MD5 | raw file
  1. <?php
  2. /**
  3. * Nette Framework
  4. *
  5. * @copyright Copyright (c) 2004, 2010 David Grudl
  6. * @license http://nette.org/license Nette license
  7. * @link http://nette.org
  8. * @category Nette
  9. * @package Nette\Application
  10. */
  11. namespace Nette\Application;
  12. use Nette,
  13. Nette\String;
  14. /**
  15. * The bidirectional route is responsible for mapping
  16. * HTTP request to a PresenterRoute object for dispatch and vice-versa.
  17. *
  18. * @copyright Copyright (c) 2004, 2010 David Grudl
  19. * @package Nette\Application
  20. */
  21. class Route extends Nette\Object implements IRouter
  22. {
  23. const PRESENTER_KEY = 'presenter';
  24. const MODULE_KEY = 'module';
  25. /** flag */
  26. const CASE_SENSITIVE = 256;
  27. /**#@+ @ignore internal uri type */
  28. const HOST = 1;
  29. const PATH = 2;
  30. const RELATIVE = 3;
  31. /**#@-*/
  32. /**#@+ key used in {@link Route::$styles} or metadata {@link Route::__construct} */
  33. const VALUE = 'value';
  34. const PATTERN = 'pattern';
  35. const FILTER_IN = 'filterIn';
  36. const FILTER_OUT = 'filterOut';
  37. const FILTER_TABLE = 'filterTable';
  38. /**#@-*/
  39. /**#@+ @ignore internal fixity types - how to handle default value? {@link Route::$metadata} */
  40. const OPTIONAL = 0;
  41. const PATH_OPTIONAL = 1;
  42. const CONSTANT = 2;
  43. /**#@-*/
  44. /** @var bool */
  45. public static $defaultFlags = 0;
  46. /** @var array */
  47. public static $styles = array(
  48. '#' => array( // default style for path parameters
  49. self::PATTERN => '[^/]+',
  50. self::FILTER_IN => 'rawurldecode',
  51. self::FILTER_OUT => 'rawurlencode',
  52. ),
  53. '?#' => array( // default style for query parameters
  54. ),
  55. 'module' => array(
  56. self::PATTERN => '[a-z][a-z0-9.-]*',
  57. self::FILTER_IN => array(__CLASS__, 'path2presenter'),
  58. self::FILTER_OUT => array(__CLASS__, 'presenter2path'),
  59. ),
  60. 'presenter' => array(
  61. self::PATTERN => '[a-z][a-z0-9.-]*',
  62. self::FILTER_IN => array(__CLASS__, 'path2presenter'),
  63. self::FILTER_OUT => array(__CLASS__, 'presenter2path'),
  64. ),
  65. 'action' => array(
  66. self::PATTERN => '[a-z][a-z0-9-]*',
  67. self::FILTER_IN => array(__CLASS__, 'path2action'),
  68. self::FILTER_OUT => array(__CLASS__, 'action2path'),
  69. ),
  70. '?module' => array(
  71. ),
  72. '?presenter' => array(
  73. ),
  74. '?action' => array(
  75. ),
  76. );
  77. /** @var string */
  78. private $mask;
  79. /** @var array */
  80. private $sequence;
  81. /** @var string regular expression pattern */
  82. private $re;
  83. /** @var array of [value & fixity, filterIn, filterOut] */
  84. private $metadata = array();
  85. /** @var array */
  86. private $xlat;
  87. /** @var int HOST, PATH, RELATIVE */
  88. private $type;
  89. /** @var int */
  90. private $flags;
  91. /**
  92. * @param string URL mask, e.g. '<presenter>/<action>/<id \d{1,3}>'
  93. * @param array default values or metadata
  94. * @param int flags
  95. */
  96. public function __construct($mask, array $metadata = array(), $flags = 0)
  97. {
  98. $this->flags = $flags | self::$defaultFlags;
  99. $this->setMask($mask, $metadata);
  100. }
  101. /**
  102. * Maps HTTP request to a PresenterRequest object.
  103. * @param Nette\Web\IHttpRequest
  104. * @return PresenterRequest|NULL
  105. */
  106. public function match(Nette\Web\IHttpRequest $httpRequest)
  107. {
  108. // combine with precedence: mask (params in URL-path), fixity, query, (post,) defaults
  109. // 1) URL MASK
  110. $uri = $httpRequest->getUri();
  111. if ($this->type === self::HOST) {
  112. $path = '//' . $uri->getHost() . $uri->getPath();
  113. } elseif ($this->type === self::RELATIVE) {
  114. $basePath = $uri->getBasePath();
  115. if (strncmp($uri->getPath(), $basePath, strlen($basePath)) !== 0) {
  116. return NULL;
  117. }
  118. $path = (string) substr($uri->getPath(), strlen($basePath));
  119. } else {
  120. $path = $uri->getPath();
  121. }
  122. if ($path !== '') {
  123. $path = rtrim($path, '/') . '/';
  124. }
  125. if (!$matches = String::match($path, $this->re)) {
  126. // stop, not matched
  127. return NULL;
  128. }
  129. // deletes numeric keys, restore '-' chars
  130. $params = array();
  131. foreach ($matches as $k => $v) {
  132. if (is_string($k) && $v !== '') {
  133. $params[str_replace('___', '-', $k)] = $v; // trick
  134. }
  135. }
  136. // 2) CONSTANT FIXITY
  137. foreach ($this->metadata as $name => $meta) {
  138. if (isset($params[$name])) {
  139. //$params[$name] = $this->flags & self::CASE_SENSITIVE === 0 ? strtolower($params[$name]) : */$params[$name]; // strtolower damages UTF-8
  140. } elseif (isset($meta['fixity']) && $meta['fixity'] !== self::OPTIONAL) {
  141. $params[$name] = NULL; // cannot be overwriten in 3) and detected by isset() in 4)
  142. }
  143. }
  144. // 3) QUERY
  145. if ($this->xlat) {
  146. $params += self::renameKeys($httpRequest->getQuery(), array_flip($this->xlat));
  147. } else {
  148. $params += $httpRequest->getQuery();
  149. }
  150. // 4) APPLY FILTERS & FIXITY
  151. foreach ($this->metadata as $name => $meta) {
  152. if (isset($params[$name])) {
  153. if (!is_scalar($params[$name])) {
  154. } elseif (isset($meta[self::FILTER_TABLE][$params[$name]])) { // applyies filterTable only to scalar parameters
  155. $params[$name] = $meta[self::FILTER_TABLE][$params[$name]];
  156. } elseif (isset($meta[self::FILTER_IN])) { // applyies filterIn only to scalar parameters
  157. $params[$name] = call_user_func($meta[self::FILTER_IN], (string) $params[$name]);
  158. if ($params[$name] === NULL && !isset($meta['fixity'])) {
  159. return NULL; // rejected by filter
  160. }
  161. }
  162. } elseif (isset($meta['fixity'])) {
  163. $params[$name] = $meta[self::VALUE];
  164. }
  165. }
  166. // 5) BUILD PresenterRequest
  167. if (!isset($params[self::PRESENTER_KEY])) {
  168. throw new \InvalidStateException('Missing presenter in route definition.');
  169. }
  170. if (isset($this->metadata[self::MODULE_KEY])) {
  171. if (!isset($params[self::MODULE_KEY])) {
  172. throw new \InvalidStateException('Missing module in route definition.');
  173. }
  174. $presenter = $params[self::MODULE_KEY] . ':' . $params[self::PRESENTER_KEY];
  175. unset($params[self::MODULE_KEY], $params[self::PRESENTER_KEY]);
  176. } else {
  177. $presenter = $params[self::PRESENTER_KEY];
  178. unset($params[self::PRESENTER_KEY]);
  179. }
  180. return new PresenterRequest(
  181. $presenter,
  182. $httpRequest->getMethod(),
  183. $params,
  184. $httpRequest->getPost(),
  185. $httpRequest->getFiles(),
  186. array(PresenterRequest::SECURED => $httpRequest->isSecured())
  187. );
  188. }
  189. /**
  190. * Constructs absolute URL from PresenterRequest object.
  191. * @param Nette\Web\IHttpRequest
  192. * @param PresenterRequest
  193. * @return string|NULL
  194. */
  195. public function constructUrl(PresenterRequest $appRequest, Nette\Web\IHttpRequest $httpRequest)
  196. {
  197. if ($this->flags & self::ONE_WAY) {
  198. return NULL;
  199. }
  200. $params = $appRequest->getParams();
  201. $metadata = $this->metadata;
  202. $presenter = $appRequest->getPresenterName();
  203. $params[self::PRESENTER_KEY] = $presenter;
  204. if (isset($metadata[self::MODULE_KEY])) { // try split into module and [submodule:]presenter parts
  205. $module = $metadata[self::MODULE_KEY];
  206. if (isset($module['fixity']) && strncasecmp($presenter, $module[self::VALUE] . ':', strlen($module[self::VALUE]) + 1) === 0) {
  207. $a = strlen($module[self::VALUE]);
  208. } else {
  209. $a = strrpos($presenter, ':');
  210. }
  211. if ($a === FALSE) {
  212. $params[self::MODULE_KEY] = '';
  213. } else {
  214. $params[self::MODULE_KEY] = substr($presenter, 0, $a);
  215. $params[self::PRESENTER_KEY] = substr($presenter, $a + 1);
  216. }
  217. }
  218. foreach ($metadata as $name => $meta) {
  219. if (!isset($params[$name])) continue; // retains NULL values
  220. if (isset($meta['fixity'])) {
  221. if (is_scalar($params[$name]) && strcasecmp($params[$name], $meta[self::VALUE]) === 0) {
  222. // remove default values; NULL values are retain
  223. unset($params[$name]);
  224. continue;
  225. } elseif ($meta['fixity'] === self::CONSTANT) {
  226. return NULL; // missing or wrong parameter '$name'
  227. }
  228. }
  229. if (!is_scalar($params[$name])) {
  230. } elseif (isset($meta['filterTable2'][$params[$name]])) {
  231. $params[$name] = $meta['filterTable2'][$params[$name]];
  232. } elseif (isset($meta[self::FILTER_OUT])) {
  233. $params[$name] = call_user_func($meta[self::FILTER_OUT], $params[$name]);
  234. }
  235. if (isset($meta[self::PATTERN]) && !preg_match($meta[self::PATTERN], rawurldecode($params[$name]))) {
  236. return NULL; // pattern not match
  237. }
  238. }
  239. // compositing path
  240. $sequence = $this->sequence;
  241. $brackets = array();
  242. $required = 0;
  243. $uri = '';
  244. $i = count($sequence) - 1;
  245. do {
  246. $uri = $sequence[$i] . $uri;
  247. if ($i === 0) break;
  248. $i--;
  249. $name = $sequence[$i]; $i--; // parameter name
  250. if ($name === ']') { // opening optional part
  251. $brackets[] = $uri;
  252. } elseif ($name[0] === '[') { // closing optional part
  253. $tmp = array_pop($brackets);
  254. if ($required < count($brackets) + 1) { // is this level optional?
  255. if ($name !== '[!') { // and not "required"-optional
  256. $uri = $tmp;
  257. }
  258. } else {
  259. $required = count($brackets);
  260. }
  261. } elseif ($name[0] === '?') { // "foo" parameter
  262. continue;
  263. } elseif (isset($params[$name]) && $params[$name] != '') { // intentionally ==
  264. $required = count($brackets); // make this level required
  265. $uri = $params[$name] . $uri;
  266. unset($params[$name]);
  267. } elseif (isset($metadata[$name]['fixity'])) { // has default value?
  268. $uri = $metadata[$name]['defOut'] . $uri;
  269. } else {
  270. return NULL; // missing parameter '$name'
  271. }
  272. } while (TRUE);
  273. // build query string
  274. if ($this->xlat) {
  275. $params = self::renameKeys($params, $this->xlat);
  276. }
  277. $sep = ini_get('arg_separator.input');
  278. $query = http_build_query($params, '', $sep ? $sep[0] : '&');
  279. if ($query != '') $uri .= '?' . $query; // intentionally ==
  280. // absolutize path
  281. if ($this->type === self::RELATIVE) {
  282. $uri = '//' . $httpRequest->getUri()->getAuthority() . $httpRequest->getUri()->getBasePath() . $uri;
  283. } elseif ($this->type === self::PATH) {
  284. $uri = '//' . $httpRequest->getUri()->getAuthority() . $uri;
  285. }
  286. if (strpos($uri, '//', 2) !== FALSE) {
  287. return NULL; // TODO: implement counterpart in match() ?
  288. }
  289. $uri = ($this->flags & self::SECURED ? 'https:' : 'http:') . $uri;
  290. return $uri;
  291. }
  292. /**
  293. * Parse mask and array of default values; initializes object.
  294. * @param string
  295. * @param array
  296. * @return void
  297. */
  298. private function setMask($mask, array $metadata)
  299. {
  300. $this->mask = $mask;
  301. // detect '//host/path' vs. '/abs. path' vs. 'relative path'
  302. if (substr($mask, 0, 2) === '//') {
  303. $this->type = self::HOST;
  304. } elseif (substr($mask, 0, 1) === '/') {
  305. $this->type = self::PATH;
  306. } else {
  307. $this->type = self::RELATIVE;
  308. }
  309. foreach ($metadata as $name => $meta) {
  310. if (!is_array($meta)) {
  311. $metadata[$name] = array(self::VALUE => $meta, 'fixity' => self::CONSTANT);
  312. } elseif (array_key_exists(self::VALUE, $meta)) {
  313. $metadata[$name]['fixity'] = self::CONSTANT;
  314. }
  315. }
  316. // PARSE MASK
  317. $parts = String::split($mask, '/<([^># ]+) *([^>#]*)(#?[^>\[\]]*)>|(\[!?|\]|\s*\?.*)/'); // <parameter-name [pattern] [#class]> or [ or ] or ?...
  318. $this->xlat = array();
  319. $i = count($parts) - 1;
  320. // PARSE QUERY PART OF MASK
  321. if (isset($parts[$i - 1]) && substr(ltrim($parts[$i - 1]), 0, 1) === '?') {
  322. $matches = String::matchAll($parts[$i - 1], '/(?:([a-zA-Z0-9_.-]+)=)?<([^># ]+) *([^>#]*)(#?[^>]*)>/'); // name=<parameter-name [pattern][#class]>
  323. foreach ($matches as $match) {
  324. list(, $param, $name, $pattern, $class) = $match; // $pattern is not used
  325. if ($class !== '') {
  326. if (!isset(self::$styles[$class])) {
  327. throw new \InvalidStateException("Parameter '$name' has '$class' flag, but Route::\$styles['$class'] is not set.");
  328. }
  329. $meta = self::$styles[$class];
  330. } elseif (isset(self::$styles['?' . $name])) {
  331. $meta = self::$styles['?' . $name];
  332. } else {
  333. $meta = self::$styles['?#'];
  334. }
  335. if (isset($metadata[$name])) {
  336. $meta = $metadata[$name] + $meta;
  337. }
  338. if (array_key_exists(self::VALUE, $meta)) {
  339. $meta['fixity'] = self::OPTIONAL;
  340. }
  341. unset($meta['pattern']);
  342. $meta['filterTable2'] = empty($meta[self::FILTER_TABLE]) ? NULL : array_flip($meta[self::FILTER_TABLE]);
  343. $metadata[$name] = $meta;
  344. if ($param !== '') {
  345. $this->xlat[$name] = $param;
  346. }
  347. }
  348. $i -= 5;
  349. }
  350. $brackets = 0; // optional level
  351. $re = '';
  352. $sequence = array();
  353. $autoOptional = array(0, 0); // strlen($re), count($sequence)
  354. do {
  355. array_unshift($sequence, $parts[$i]);
  356. $re = preg_quote($parts[$i], '#') . $re;
  357. if ($i === 0) break;
  358. $i--;
  359. $part = $parts[$i]; // [ or ]
  360. if ($part === '[' || $part === ']' || $part === '[!') {
  361. $brackets += $part[0] === '[' ? -1 : 1;
  362. if ($brackets < 0) {
  363. throw new \InvalidArgumentException("Unexpected '$part' in mask '$mask'.");
  364. }
  365. array_unshift($sequence, $part);
  366. $re = ($part[0] === '[' ? '(?:' : ')?') . $re;
  367. $i -= 4;
  368. continue;
  369. }
  370. $class = $parts[$i]; $i--; // validation class
  371. $pattern = trim($parts[$i]); $i--; // validation condition (as regexp)
  372. $name = $parts[$i]; $i--; // parameter name
  373. array_unshift($sequence, $name);
  374. if ($name[0] === '?') { // "foo" parameter
  375. $re = '(?:' . preg_quote(substr($name, 1), '#') . '|' . $pattern . ')' . $re;
  376. $sequence[1] = substr($name, 1) . $sequence[1];
  377. continue;
  378. }
  379. // check name (limitation by regexp)
  380. if (preg_match('#[^a-z0-9_-]#i', $name)) {
  381. throw new \InvalidArgumentException("Parameter name must be alphanumeric string due to limitations of PCRE, '$name' given.");
  382. }
  383. // pattern, condition & metadata
  384. if ($class !== '') {
  385. if (!isset(self::$styles[$class])) {
  386. throw new \InvalidStateException("Parameter '$name' has '$class' flag, but Route::\$styles['$class'] is not set.");
  387. }
  388. $meta = self::$styles[$class];
  389. } elseif (isset(self::$styles[$name])) {
  390. $meta = self::$styles[$name];
  391. } else {
  392. $meta = self::$styles['#'];
  393. }
  394. if (isset($metadata[$name])) {
  395. $meta = $metadata[$name] + $meta;
  396. }
  397. if ($pattern == '' && isset($meta[self::PATTERN])) {
  398. $pattern = $meta[self::PATTERN];
  399. }
  400. $meta['filterTable2'] = empty($meta[self::FILTER_TABLE]) ? NULL : array_flip($meta[self::FILTER_TABLE]);
  401. if (array_key_exists(self::VALUE, $meta)) {
  402. if (isset($meta['filterTable2'][$meta[self::VALUE]])) {
  403. $meta['defOut'] = $meta['filterTable2'][$meta[self::VALUE]];
  404. } elseif (isset($meta[self::FILTER_OUT])) {
  405. $meta['defOut'] = call_user_func($meta[self::FILTER_OUT], $meta[self::VALUE]);
  406. } else {
  407. $meta['defOut'] = $meta[self::VALUE];
  408. }
  409. }
  410. $meta[self::PATTERN] = "#(?:$pattern)$#A" . ($this->flags & self::CASE_SENSITIVE ? '' : 'iu');
  411. // include in expression
  412. $re = '(?P<' . str_replace('-', '___', $name) . '>' . $pattern . ')' . $re; // str_replace is dirty trick to enable '-' in parameter name
  413. if ($brackets) { // is in brackets?
  414. if (!isset($meta[self::VALUE])) {
  415. $meta[self::VALUE] = $meta['defOut'] = NULL;
  416. }
  417. $meta['fixity'] = self::PATH_OPTIONAL;
  418. } elseif (isset($meta['fixity'])) { // auto-optional
  419. $re = '(?:' . substr_replace($re, ')?', strlen($re) - $autoOptional[0], 0);
  420. array_splice($sequence, count($sequence) - $autoOptional[1], 0, array(']', ''));
  421. array_unshift($sequence, '[', '');
  422. $meta['fixity'] = self::PATH_OPTIONAL;
  423. } else {
  424. $autoOptional = array(strlen($re), count($sequence));
  425. }
  426. $metadata[$name] = $meta;
  427. } while (TRUE);
  428. if ($brackets) {
  429. throw new \InvalidArgumentException("Missing closing ']' in mask '$mask'.");
  430. }
  431. $this->re = '#' . $re . '/?$#A' . ($this->flags & self::CASE_SENSITIVE ? '' : 'iu');
  432. $this->metadata = $metadata;
  433. $this->sequence = $sequence;
  434. }
  435. /**
  436. * Returns mask.
  437. * @return string
  438. */
  439. public function getMask()
  440. {
  441. return $this->mask;
  442. }
  443. /**
  444. * Returns default values.
  445. * @return array
  446. */
  447. public function getDefaults()
  448. {
  449. $defaults = array();
  450. foreach ($this->metadata as $name => $meta) {
  451. if (isset($meta['fixity'])) {
  452. $defaults[$name] = $meta[self::VALUE];
  453. }
  454. }
  455. return $defaults;
  456. }
  457. /********************* Utilities ****************d*g**/
  458. /**
  459. * Proprietary cache aim.
  460. * @return string|FALSE
  461. */
  462. public function getTargetPresenter()
  463. {
  464. if ($this->flags & self::ONE_WAY) {
  465. return FALSE;
  466. }
  467. $m = $this->metadata;
  468. $module = '';
  469. if (isset($m[self::MODULE_KEY])) {
  470. if (isset($m[self::MODULE_KEY]['fixity']) && $m[self::MODULE_KEY]['fixity'] === self::CONSTANT) {
  471. $module = $m[self::MODULE_KEY][self::VALUE] . ':';
  472. } else {
  473. return NULL;
  474. }
  475. }
  476. if (isset($m[self::PRESENTER_KEY]['fixity']) && $m[self::PRESENTER_KEY]['fixity'] === self::CONSTANT) {
  477. return $module . $m[self::PRESENTER_KEY][self::VALUE];
  478. }
  479. return NULL;
  480. }
  481. /**
  482. * Rename keys in array.
  483. * @param array
  484. * @param array
  485. * @return array
  486. */
  487. private static function renameKeys($arr, $xlat)
  488. {
  489. if (empty($xlat)) return $arr;
  490. $res = array();
  491. $occupied = array_flip($xlat);
  492. foreach ($arr as $k => $v) {
  493. if (isset($xlat[$k])) {
  494. $res[$xlat[$k]] = $v;
  495. } elseif (!isset($occupied[$k])) {
  496. $res[$k] = $v;
  497. }
  498. }
  499. return $res;
  500. }
  501. /********************* Inflectors ****************d*g**/
  502. /**
  503. * camelCaseAction name -> dash-separated.
  504. * @param string
  505. * @return string
  506. */
  507. private static function action2path($s)
  508. {
  509. $s = preg_replace('#(.)(?=[A-Z])#', '$1-', $s);
  510. $s = strtolower($s);
  511. $s = rawurlencode($s);
  512. return $s;
  513. }
  514. /**
  515. * dash-separated -> camelCaseAction name.
  516. * @param string
  517. * @return string
  518. */
  519. private static function path2action($s)
  520. {
  521. $s = strtolower($s);
  522. $s = preg_replace('#-(?=[a-z])#', ' ', $s);
  523. $s = substr(ucwords('x' . $s), 1);
  524. //$s = lcfirst(ucwords($s));
  525. $s = str_replace(' ', '', $s);
  526. return $s;
  527. }
  528. /**
  529. * PascalCase:Presenter name -> dash-and-dot-separated.
  530. * @param string
  531. * @return string
  532. */
  533. private static function presenter2path($s)
  534. {
  535. $s = strtr($s, ':', '.');
  536. $s = preg_replace('#([^.])(?=[A-Z])#', '$1-', $s);
  537. $s = strtolower($s);
  538. $s = rawurlencode($s);
  539. return $s;
  540. }
  541. /**
  542. * dash-and-dot-separated -> PascalCase:Presenter name.
  543. * @param string
  544. * @return string
  545. */
  546. private static function path2presenter($s)
  547. {
  548. $s = strtolower($s);
  549. $s = preg_replace('#([.-])(?=[a-z])#', '$1 ', $s);
  550. $s = ucwords($s);
  551. $s = str_replace('. ', ':', $s);
  552. $s = str_replace('- ', '', $s);
  553. return $s;
  554. }
  555. /********************* Route::$styles manipulator ****************d*g**/
  556. /**
  557. * Creates new style.
  558. * @param string style name (#style, urlParameter, ?queryParameter)
  559. * @param string optional parent style name
  560. * @param void
  561. */
  562. public static function addStyle($style, $parent = '#')
  563. {
  564. if (isset(self::$styles[$style])) {
  565. throw new \InvalidArgumentException("Style '$style' already exists.");
  566. }
  567. if ($parent !== NULL) {
  568. if (!isset(self::$styles[$parent])) {
  569. throw new \InvalidArgumentException("Parent style '$parent' doesn't exist.");
  570. }
  571. self::$styles[$style] = self::$styles[$parent];
  572. } else {
  573. self::$styles[$style] = array();
  574. }
  575. }
  576. /**
  577. * Changes style property value.
  578. * @param string style name (#style, urlParameter, ?queryParameter)
  579. * @param string property name (Route::PATTERN, Route::FILTER_IN, Route::FILTER_OUT, Route::FILTER_TABLE)
  580. * @param mixed property value
  581. * @param void
  582. */
  583. public static function setStyleProperty($style, $key, $value)
  584. {
  585. if (!isset(self::$styles[$style])) {
  586. throw new \InvalidArgumentException("Style '$style' doesn't exist.");
  587. }
  588. self::$styles[$style][$key] = $value;
  589. }
  590. }