1: <?php
2:
3: /**
4: * Představuje jednu routu.
5: * Zadané reguláry pro jednotlivé části routy implicitně rozlišují velikost písmen.
6: *
7: * @author Jakub Kulhan (původní verze routeru), http://bukaj.netuje.cz/blog/jednoduchy-routing-v-php
8: * @author Viktorie Halasu (rozšíření), http://projekty.vize.name/router/
9: * @property-read array $redirParams Pole parametrů pro vytvoření URL, na kterou se tato routa přesměrovává.
10: * @property-read array $parsedUrl Pole parametrů z URL, která byla rozebrána podle této routy.
11: * @property-read string $lastCreatedUrl Poslední vytvořená URL.
12: * @package Router
13: * @version 1.3
14: */
15:
16: class Route {
17:
18: /** @var int Modifikátor. Určuje, že z této routy se nebude dělat polotovar. */
19: const FIXED = 1;
20:
21: /** @var int Modifikátor. Určuje, že tato routa nerozlišuje velká a malá písmena. */
22: const CI = 2;
23:
24: /** @var int Modifikátor. Požadavek odpovídající této routě se má ihned přesměrovat na novou URL. */
25: const REDIR = 4;
26:
27: /** @var int Modifikátor. Zadaná routa je pro URL z parametrů (a má se zpracovat třídou SimpleRoute). */
28: const SIMPLE = 8;
29:
30: /** @var string Výchozí regulár pro jednotlivé části routy (písmena bez diakritiky, číslice, podtržítko, tečka, pomlčka). */
31: const DEFAULT_REGEX = '[A-Za-z0-9_.-]+';
32:
33: /** @var string Hodnota, kterou zachytí výchozí regulár. */
34: const DEFAULT_VALUE = 'a';
35:
36: /** @var string Textová podoba této routy tak, jak byla zadaná. */
37: protected $source = '';
38:
39: /** @var bool Má se tato routa přesměrovávat? */
40: protected $isRedirected = false;
41:
42: /** @var int Modifikátory použité pro tuto routu (bitmask). */
43: protected $flags = 0;
44:
45: /** @var array Parametry z rozebrané routy. */
46: protected $params;
47:
48: /** @var array URL rozebraná podle této routy. */
49: protected $parsedUrl = array();
50:
51: /** @var string Regulár pro tuto routu. */
52: protected $regex;
53:
54:
55: /** @var array Přesměrování (pole parametrů pro vytvoření URL.) */
56: protected $redirParams;
57:
58: /** @var callback Callback pro přesměrování. */
59: protected $redirCallback;
60:
61:
62: /** @var string Formátovací řetězec pro URL. */
63: protected $urlFormat;
64:
65: /** @var array Parametry, ze kterých byla naposled vytvořena URL. */
66: protected $lastUrlParams;
67:
68: /** @var string Poslední vytvořená URL. */
69: protected $lastCreatedUrl = '';
70:
71:
72: /**
73: * Parsuje zadání routy.
74: * @param string $route
75: * @param array $defaults Pole výchozích hodnot.
76: * @param int $flags Modifikátory routy (bitmask). FIXED | CI | REDIR
77: * @param mixed $redir Pokud se přesměrovávají nepoužívané URL na nové (flag REDIR), musí obsahovat buď pole hodnot
78: * pro vytvoření nové URL, anebo callback, který toto pole vrací.
79: * Callback funkce dostane jediný parametr - parsovanou routu (ze starého URL). Pokud je použité pole
80: * a některý prvek má hodnotu Router::COPY_OLD_VALUE, zkopíruje se do něj hodnota ze stejnojmenného prvku
81: * ve staré URL (např. "controller").
82: */
83: public function __construct($route, array $defaults = array(), $flags = 0, $redir = null) {
84: $this->source = $route;
85: if (is_int($flags)) {
86: $this->flags = $flags;
87: }
88: $this->params = array(
89: 'order' => array(), /* pořadí parametrů v routě (podle toho se složí výstupní URL): název => pořadí */
90: 'fromRoute' => array(), /* jen parametry z routy s doplněnými výchozími hodnotami: název => [value => null|hodnota, optional => true|false] */
91: 'allValues' => array(), /* parametry z routy i výchozí par. */
92: );
93: $this->urlFormat = '';
94: $this->regex = '~^';
95:
96: $orderIndex = 0;
97: foreach ($this->parse($this->source) as $i => $part) {
98: $optional = false;
99: if ($i % 2 === 0) {
100: $this->urlFormat .= $part;
101: $this->regex .= preg_quote($part, '~');
102:
103: } else {
104: list($name, $partRegex) = explode("\037", $part);
105: /* escapování oddělovače, zpětné lomítko je potřeba escapovat dvakrát */
106: $partRegex = preg_replace('#(?<!\\\\)~#', '\\~', $partRegex);
107: $this->urlFormat .= '%s';
108: /* Je tato část routy nepovinná? */
109: if ($name{0} === '?') {
110: $optional = true;
111: if (substr($this->regex, -1) === '/') {
112: $this->regex .= '?';
113: }
114: $name = substr($name, 1);
115: }
116:
117: /* Pokud není definovaný regulár pro tuto část routy, použije se výchozí */
118: $this->regex .= ($optional ? '(?:' : '') . '(?<' . $name . '>' .
119: (!empty($partRegex) ? $partRegex : self::DEFAULT_REGEX) .
120: ')' . ($optional ? ')?' : '');
121:
122: /* Uloží se jednotlivé části routy a jejich vlastnosti. */
123: $this->params['order'][$name] = $orderIndex;
124: $this->params['fromRoute'][$name] = array(
125: 'value' => null,
126: 'optional' => $optional,
127: );
128: $this->params['allValues'][$name] = null;
129: }
130: $orderIndex++;
131: }
132:
133: /* PCRE modif.: Unicode vždy, case-insensitive volitelně */
134: $this->regex .= '$~u' . ($this->hasFlag(self::CI) ? 'i' : '');
135:
136: /* Přidají se výchozí hodnoty nebo parametry, pokud byly zadány. */
137: if (!empty($defaults)) {
138: foreach ($defaults as $key => $val) {
139: if (!isset($this->params['fromRoute'][$key])) {
140: $this->params['fromRoute'][$key]['value'] = $val;
141: $this->params['fromRoute'][$key]['optional'] = false;
142: }
143: $this->params['allValues'][$key] = $val;
144: }
145: }
146:
147: if ($this->hasFlag(self::REDIR)) {
148: $this->resolveRedirection($redir);
149: }
150: }
151:
152:
153: /**
154: * Pokusí se rozebrat zadanou URL podle této routy.
155: * @param string $url
156: * @return bool
157: */
158: public function matchUrl($url) {
159: if (preg_match($this->regex, $url, $urlParams)) {
160: $this->parsedUrl = $this->kmerge($this->params['allValues'], $urlParams);
161: return true;
162: }
163: $this->parsedUrl = array();
164: return false;
165: }
166:
167:
168: /**
169: * Podle této routy zkusí ze zadaných parametrů vytvořit URL. Parametry mohou být v libovolném pořadí, nezávisle na pořadí v routě.
170: * @param array $params Parametry. Musí být uvedeny i výchozí hodnoty (2.param konstruktoru). Nepovinné části routy nebo výchozí parametry pro celý router (1.param Router::__construct) lze vynechat.
171: * @return bool
172: */
173: public function createUrlFromParams(array $params) {
174:
175: $vsparams = array();
176:
177: foreach ($this->params['fromRoute'] as $parName => $parProps) {
178:
179: /* Pokud je v routě víc povinných částí, než bylo zadáno parametrů, máme špatnou routu. */
180: if (empty($params) && !$parProps['optional']) {
181: return false;
182: }
183:
184: if (isset($params[$parName])) {
185: /* Výchozí parametry musí mít stejnou hodnotu. */
186: if ($parProps['value'] !== null && $params[$parName] != $parProps['value']) {
187: return false;
188: }
189: /* Pokud parametr patří do routy (= není výchozí), použije se. */
190: if (isset($this->params['order'][$parName])) {
191: $vsparams[$this->params['order'][$parName]] = $params[$parName];
192: }
193: unset($params[$parName]);
194: }
195: /* Parametr nebyl zadaný, patří do routy, ale je nepovinný: použije se prázdná hodnota. */
196: elseif ($parProps['optional'] === false) {
197: $vsparams[$this->params['order'][$parName]] = '';
198: }
199: }
200:
201: /* Pokud zbyly parametry, které nejsou v routě, máme špatnou routu. */
202: if (!empty($params)) {
203: $this->lastUrlParams = array();
204: $this->lastCreatedUrl = '';
205: return false;
206: }
207:
208: ksort($vsparams);
209:
210: /* Pro vsprintf: počet zástupných znaků v this->urlFormat a vložených hodnot se musí shodovat. */
211: $diff = count($this->params['order']) - count($vsparams);
212: if ($diff > 0) {
213: $vsparams = array_merge($vsparams, array_fill(0, $diff, ''));
214: }
215:
216: /* Ověření hotové URL proti reguláru této routy. */
217: $url = rtrim(vsprintf($this->urlFormat, $vsparams), '/');
218: if (preg_match($this->regex, $url)) {
219: $this->lastUrlParams = $vsparams;
220: $this->lastCreatedUrl = $url;
221: return true;
222: } else {
223: $this->lastUrlParams = array();
224: $this->lastCreatedUrl = '';
225: return false;
226: }
227: }
228:
229:
230: /**
231: * Změní hodnotu některých částí u poslední vytvořené URL a vrátí novou URL (pro šablony URL).
232: * @param array $variables Proměnné části.
233: *
234: * @throws LogicException
235: */
236: public function addVariableUrlParts(array $variables) {
237: if (empty($this->lastUrlParams)) {
238: throw new LogicException("Nelze nastavit proměnné parametry routy. Nejdřív se musí vytvořit URL podle routy.");
239: }
240: foreach ($variables as $name => $value) {
241: if (isset($this->lastUrlParams[$this->params['order'][$name]])) {
242: $this->lastUrlParams[$this->params['order'][$name]] = $value;
243: }
244: }
245: $this->lastCreatedUrl = rtrim(vsprintf($this->urlFormat, $this->lastUrlParams), '/');
246: }
247:
248:
249: /**
250: * Zjistí, jestli má routa nastaven zadaný modifikátor
251: * @param int $flag
252: * @return bool
253: */
254: final public function hasFlag($flag) {
255: return (is_int($flag) && $this->flags & $flag);
256: }
257:
258:
259: /**
260: * Vrací pole parametrů pro vytvoření URL (u přesměrovávané routy)
261: * @return array
262: */
263: final public function getRedirParams() {
264: if (empty($this->redirParams)) {
265: $this->redirParams = $this->invokeRedirCallback();
266: }
267: return $this->redirParams;
268: }
269:
270:
271: /**
272: * Vrací rozebranou URL.
273: * @return array
274: */
275: final public function getParsedUrl() {
276: return $this->parsedUrl;
277: }
278:
279:
280: /**
281: * Vrací naposled vytvořenou URL.
282: * @return string
283: */
284: final public function getLastCreatedUrl() {
285: return $this->lastCreatedUrl;
286: }
287:
288:
289: /**
290: * Zjistí, jestli se tato routa má přesměrovávat.
291: * @return bool
292: */
293: final public function isRedirected() {
294: return $this->isRedirected;
295: }
296:
297:
298: /**
299: * Parsuje zadanou routu.
300: * @param string $route
301: * @return array
302: */
303: protected function parse($route) {
304: return explode("\036", trim(
305: preg_replace(
306: '~:(\??[A-Za-z0-9_]+)(?:<(.+?)>)?~',
307: "\036\$1\037\$2\036",
308: $route
309: ), "\036")
310: );
311: }
312:
313:
314: /**
315: * Sloučí dvě asoc. pole. Podobné jako array_merge, ale nepřepisuje hodnotu v prvním poli prázdným řetězcem ani NULL.
316: * @param array $arr1
317: * @param array $arr2
318: * @return array
319: */
320: final protected function kmerge($arr1, $arr2) {
321: foreach ($arr2 as $key => $val) {
322: if (!isset($arr1[$key])) {
323: $arr1[$key] = $val;
324: } elseif ($val !== '' && $val !== null) {
325: $arr1[$key] = $val;
326: }
327: }
328: return $arr1;
329: }
330:
331:
332: /**
333: * Nastaví přesměrování
334: * @param mixed $redir (Pole nebo callback).
335: *
336: * @throws InvalidArgumentException Pokud chybí cíl, nebo je špatného dat.typu
337: */
338: protected function resolveRedirection($redir = null) {
339: if (empty($redir)) {
340: throw new InvalidArgumentException("Routa se má přesměrovávat, ale chybí cíl (4. parametr).");
341: }
342: if (is_array($redir) && !is_int(key($redir))) {
343: $this->redirParams = $redir;
344: } else {
345: /* ověření pomocí is_callable() se pro úsporu prostředků provádí až v invokeRedirCallback() */
346: $this->redirCallback = $redir;
347: }
348: $this->isRedirected = true;
349: }
350:
351:
352: /**
353: * Volá callback pro přesměrovávanou routu a vrací jeho výsledek.
354: * @return array
355: *
356: * @throws RuntimeException Pokud je callback neplatný
357: * @throws LogicException
358: * @throws UnexpectedValueException Pokud callback nevrací pole.
359: */
360: protected function invokeRedirCallback() {
361: if (!is_callable($this->redirCallback)) {
362: throw new RuntimeException("Routa se má přesměrovávat, ale zadaný callback je neplatný.");
363: } elseif (empty($this->parsedUrl)) {
364: throw new LogicException("Přesměrování routy callbackem nelze volat dříve, než byla parsovaná URL.");
365: }
366: $redirParams = call_user_func($this->redirCallback, $this->parsedUrl);
367: if (!is_array($redirParams)) {
368: throw new UnexpectedValueException("Callback pro přesměrování routy musí vracet pole.");
369: }
370: return $redirParams;
371: }
372:
373:
374: /**
375: * Vrací textové zadání této routy.
376: * @return string
377: */
378: public function toSource() {
379: return $this->source;
380: }
381: }
382: