Overview

Packages

  • PHP
  • Router

Classes

  • Route
  • Router
  • RouterForIncludes
  • SimpleRoute
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * Rozšířená verze. Hlavní změny:
  4:  * - zjednodušené generování URL, které se liší jen hodnotou jediného parametru
  5:  * - umožněny nepovinné části routy
  6:  * - možnost vynechání části URL před zpracováním, doplnění prefixu do výstupní adresy
  7:  * - automatické přesměrování z nepoužívaných URL na nové
  8:  * - předá řízení libovolnému callbacku (funkce nebo třída+metoda; nadefinuje se jen přibližně, hodnoty se doplní z URL)
  9:  * 
 10:  * @author Jakub Kulhan (původní verze), http://bukaj.netuje.cz/blog/jednoduchy-routing-v-php
 11:  * @author Viktorie Halasů (rozšíření), http://projekty.vize.name/router/
 12:  * @version 1.3
 13:  * @package Router
 14:  */
 15: 
 16: 
 17: class Router implements ArrayAccess     {
 18:     
 19: 
 20:     /** @var string Pro přesměrování, do tohoto parametru se má zkopírovat hodnota ze staré URL. */
 21:     const COPY_OLD_VALUE = '@@copy';
 22:         
 23:     /** @var string Určuje, že tato část výstupní adresy se bude dynamicky dosazovat (polotovar). */
 24:     const PREPARE = "\032";
 25:     
 26:     /** @var string Název klíče v poli $_GET, do kterého se v .htaccess ukládá URL (nepovinné).   */
 27:     protected $urlRouteKey = 'route';
 28: 
 29:     /** @var string Základ URL. */
 30:     protected $baseUrl = '';
 31:     
 32:     /** @var string Část vstupní URL, která má být odstraněna před hledáním vhodné routy. */
 33:     protected $ignoredUrlPart = '';
 34:     
 35:     /** @var string Řetězec, který se má přidat před každou výstupní URL. */
 36:     protected $outputUrlPrefix = '';
 37:     
 38:     /** @var string Aktuální URL (to, co bylo předané metodě parseURL). */ 
 39:     protected $currentUrl = '';
 40: 
 41:     /** @var array Parametry z rozebrané URL. */
 42:     protected $params = array();
 43:     
 44:     /** @var array Výchozí hodnoty parametrů, pokud bude URL prázdná. */
 45:     protected $defaultParams = array();
 46:     
 47:     
 48:     /** @var array Parsované routy. */
 49:     protected $routes = array();
 50:     
 51:     /** @var array Připravené šablony URL. */
 52:     protected $urlTemplates = array();
 53:         
 54:     /** @var string Maska pro jméno kontroleru / metody / funkce, kterou bude volat metoda delegate. */
 55:     protected $callbackMask = '';
 56:     
 57:     /** @var string Přeložené jméno callbacku. */
 58:     protected $callback = '';
 59:     
 60:     /** @var string Callback pro případ chyby (neexistující metoda/třída/funkce). */
 61:     protected $errorCallback = '';
 62:     
 63:     
 64:     /**
 65:      * Konstruktor
 66:      * @param array $defaults Výchozí parametry pro celý router.
 67:      */
 68:     public function __construct(array $defaults = array())  {
 69:         $this->setBaseUrl();
 70:         $this->defaultParams = $defaults;
 71:     }
 72:     
 73:     
 74:     /**
 75:      * Parsuje a přidá další routu.
 76:      * @param string $route 
 77:      * @param array $defaults Pole výchozích hodnot.
 78:      * @param int $flags Modifikátory routy (bitmask). FIXED | CI | REDIR
 79:      * @param array $redir Pokud se přesměrovávají nepoužívané URL na nové (flag REDIR), musí obsahovat buď pole hodnot
 80:      *      pro vytvoření nové URL, anebo callback, který toto pole vrací. 
 81:      * @see Route::__construct()
 82:      * @return Router (fluent interface)
 83:      * 
 84:      * @throws InvalidArgumentException Pokud je nastavený flag REDIR, ale chybí údaj, kam se má přesměrovávat.
 85:      */
 86:     final public function addRoute($route, array $defaults = array(), $flags=0, $redir = null)  {
 87:         $class = $flags & Route::SIMPLE ? 'SimpleRoute' : 'Route';
 88:         $this->routes[] = new $class($route, $defaults, $flags, $redir);
 89:         return $this;
 90:     }
 91:     
 92: 
 93:     /**
 94:      * Připraví URL jako pojmenovanou šablonu (= jedna hodnota se bude dynamicky doplňovat, ostatní zůstanou stejné).
 95:      * @param string $name Jméno šablony
 96:      * @param array $params Parametry pro vytvoření URL. 
 97:      *      Variabilní parametr musí mít jako hodnotu konstantu Router::PREPARE. Pokud se pro tuto část routy používá vlastní regulár, je potřeba ke konstantě připojit i řetězec, který tomuto reguláru odpovídá.
 98:      * @param mixed $query_string Query string (volitelně). Buď hotový řetězec, nebo pole [název]=>hodnota.
 99:      * @param string $fragment Kotva.
100:      * @return Router (fluent interface)
101:      * 
102:      * @throws InvalidArgumentException Pokud požadované jméno už existuje nebo zadané parametry neodpovídají žádné 
103:      *      routě nebo zadané jméno není řetězec.
104:      * @throws UnexpectedValueException Pokud nebyla určená variabilní část.
105:      */
106:     final public function prepareUrlTemplate($tplName, array $params, $queryString = '', $fragment = '')    {
107:         
108:         if ($this->templateExists($tplName))    {
109:             throw new InvalidArgumentException("Router: Pojmenovaná šablona URL '$tplName' již existuje!");
110:         } elseif (!is_string($tplName)) {
111:             throw new InvalidArgumentException("Router: Jméno pro šablonu URL je špatného typu (očekáván řetězec).");
112:         }
113:         
114:         $queryString = $this->createQueryString($queryString);
115:         
116:         /* Najde parametry, které se budou dynamicky doplňovat. */
117:         $variables = array();
118:         foreach ($params as $name => $value)    {
119:             /* Pokud nebyla zadaná hodnota pro regulár této části, použije se výchozí */
120:             if (strpos($value, self::PREPARE) !== false)    {
121:                 $value = str_replace(self::PREPARE, '', $value);
122:                 $params[$name] = empty($value) ? Route::DEFAULT_VALUE : $value;
123:                 $variables[$name] = self::PREPARE;
124:             }
125:         }
126:         
127:         /* Pokud nebyly zadané proměnné části ani v parametrech, ani v query string, varuje uživatele. */
128:         if (empty($variables) && strpos($queryString, self::PREPARE) === false) {
129:             trigger_error("Varování routeru: Šablona URL '$tplName' neobsahuje proměnnou část.", E_USER_WARNING);
130:         }
131:         
132:         /* Najde vhodnou routu a vytvoří šablonu */
133:         foreach ($this->routes as $route)   {
134:             if ($route->hasFlag(Route::FIXED))  { 
135:                 continue; 
136:             }
137:             
138:             if ($route->createUrlFromParams($params))   {
139:                 if (!empty($variables)) {
140:                     $route->addVariableUrlParts($variables);
141:                 }
142:                 
143:                 $url = $this->outputUrlPrefix . $route->getLastCreatedUrl();
144:                 if (!empty($queryString))   {
145:                     $url = $this->insertParamsToUrl($url, $queryString);
146:                 }
147:                 if (!empty($fragment) || $fragment === 0)   {
148:                     $url .= '#' . $fragment;
149:                 }
150:                 $this->urlTemplates[$tplName] = $url;
151:                 
152:                 return $this;
153:             }
154:         }
155:         throw new InvalidArgumentException("Router: Zadané parametry neodpovídají žádné routě, URL nelze vytvořit.");       
156:     }
157:     
158:     
159:     /**
160:      * Vytvoří hotovou URL podle pojmenované šablony
161:      * @param string $tplName Jméno šablony
162:      * @param string $value Řetězec, který se má doplnit
163:      * @return string URL
164:      * 
165:      * @throws InvalidArgumentException Pokud neexistuje polotovar s tímto jménem, nebo zadaná hodnota není řetězec/číslo.
166:      */
167:     final public function urlFromTemplate($tplName, $value) {
168:         if (!is_scalar($value) || (is_object($value) && !method_exists($value, '__toString')))  {
169:             throw new InvalidArgumentException("Router: Zadaná hodnota je špatného typu (očekáván řetězec nebo objekt s metodou __toString).");
170:         } elseif (!$this->templateExists($tplName)) {
171:             throw new InvalidArgumentException("Router: Pojmenovaná URL '$tplName' neexistuje!");
172:         }
173:         
174:         return str_replace(self::PREPARE, (string) $value, $this->urlTemplates[$tplName]);
175:     }
176:     
177:     
178:     /**
179:      * Podle zadaných parametrů složí URL.
180:      * @param array $params Parametry. Musí být uvedeny i výchozí hodnoty (2.param addRoute), nepovinné části routy lze vynechat.
181:      * @param string|array $queryString Buď hotový řetězec, nebo pole [název]=>hodnota.
182:      * @param string $fragment Kotva
183:      * @return string
184:      */
185:     final public function url(array $params, $queryString = '', $fragment = '') {
186: 
187:         foreach ($this->routes as $route)   {
188:             if ($route->createUrlFromParams($params))   {
189:                 $url = $this->outputUrlPrefix . $route->getLastCreatedUrl();
190:                 
191:                 $queryString = $this->createQueryString($queryString);
192:                 if (!empty($queryString))   {
193:                     $url = $this->insertParamsToUrl($url, $queryString);
194:                 }
195:                 if (!empty($fragment) || $fragment === 0)   {
196:                     $url .= '#' . $fragment;
197:                 }
198:                 return $url;
199:             }
200:         }
201:         trigger_error("Router: Zadané parametry neodpovídají žádné routě, URL nelze vytvořit.", E_USER_WARNING);
202:         return '';
203:     }
204:     
205:     
206:     /**
207:      * Zpracuje dodatečné parametry do URL (query string), neescapuje, otazník nepřidává.
208:      *      Vyhodí parametr, ve kterém je routa z mod_rewrite.
209:      * @param string|array $params Může být pole parametrů i hotový query string.
210:      * @return string
211:      */
212:     final protected function createQueryString($params)     {
213:         if (!empty($params) && is_string($params))  {
214:             parse_str(ltrim($params, '?&'), $parsed);
215:             unset($parsed[$this->urlRouteKey]);
216:             return (!empty($parsed) ? urldecode(http_build_query($parsed, '', '&')) : '');
217:         }
218:         elseif (is_array($params))  {
219:             unset($params[$this->urlRouteKey]);
220:             return (!empty($params) ? urldecode(http_build_query($params, '', '&')) : '');
221:         }
222:         else
223:             return '';
224:     }
225:     
226:     /**
227:      * Vloží do URL další parametry.
228:      * @param string $url
229:      * @param string $queryString
230:      * @return string 
231:      */
232:     final protected function insertParamsToUrl($url, $queryString)  {
233:         $queryString = (strpos($url, '?') === false ? '?' : '&') . $queryString;
234:         if (($pos = strpos($url, '#')) === false)   {
235:             return $url . $queryString;
236:         } else {
237:             return substr_replace($url, $queryString, $pos, 0);
238:         }
239:     }
240: 
241:     
242:     /**
243:      * Rozebere URL na parametry podle první vyhovující routy a vrátí výsledek. Pokud zadaná URL neodpovídá žádné routě, vyvolá varování a nastaví výchozí hodnoty.
244:      * @param string $url
245:      * @return array 
246:      * 
247:      * @throws UnexpectedValueException Pokud je tato routa přesměrovávaná a cíl je špatně zadaný.
248:      */
249:     final public function parseUrl($url)    {
250:         
251:         $url = rtrim($url, '/\\');
252:         if (!empty($this->ignoredUrlPart))  {
253:             $url = str_replace($this->ignoredUrlPart, '', $url);
254:         }
255:         $this->currentUrl = $url;
256:         
257:         /* Nezadaná cesta nebo nejvyšší adresář webu - použijí se výchozí hodnoty (viz konstruktor). */
258:         if (empty($this->currentUrl))   {
259:             $this->setDefaultParams();
260:             
261:         } else {
262:             foreach ($this->routes as $route)   {
263:                 if ($route->matchUrl($this->currentUrl))    {
264:                     $this->params = $route->getParsedUrl();
265: 
266:                     if ($route->isRedirected()) {
267:                         $this->redirectRoute($route);
268:                     } else {
269:                         break;
270:                     }
271:                 }
272:             }
273:             
274:             if (empty($this->params))   {
275:                 trigger_error("Router: Zadaná URL neodpovídá žádné routě.", E_USER_WARNING);
276:                 $this->setDefaultParams();
277:             }
278:         }
279:         
280:         $this->resolveCallbackName();
281:         return $this->params;
282:     }
283:     
284:     
285:     /**
286:      * Nastaví výchozí požadavek (podle hodnoty callbacku).
287:      */
288:     final protected function setDefaultParams() {
289:         $this->params = $this->defaultParams;
290:     }
291:     
292:     
293:     /**
294:      * Nastaví masku pro callback. Tam, kde se má doplnit parametr z URL, napište jeho jméno v ostrých závorkách.
295:      * Lze zadat jak funkci: "<func>", tak třídu + metodu: "<controller>Controller:<action>". Více hodnot oddělte čárkou.
296:      * @param string $mask
297:      * @return Router (fluent interface)
298:      * 
299:      * @throws InvalidArgumentException Pokud vstup není řetězec.
300:      */
301:     final public function setCallbackMask($mask)    {
302:         if (!is_string($mask))  {
303:             throw new InvalidArgumentException("Router::setCallbackMask() - Vstup je špatného typu (očekáván řetězec).");
304:         }
305:         $this->callbackMask = str_replace(' ', '', $mask);
306:         
307:         /* Pokud byla už dřív volaná parseUrl(), doplň hodnoty pro callback */
308:         if (!empty($this->params))  {
309:             $this->resolveCallbackName();
310:         }
311:         return $this;       
312:     }
313:     
314:     
315:     /**
316:      * Nastaví callback, který se má použít v případě chyby (neexistující třídy/metody apod.) Syntax stejná jako pro setCallbackMask, ale bez doplňovaných hodnot.
317:      * @param string $callback
318:      * @return Router (fluent interface)
319:      * 
320:      * @throws InvalidArgumentException Pokud vstup není řetězec.
321:      */
322:     final public function setErrorCallback($callback)   {
323:         if (!is_string($callback))  {
324:             throw new InvalidArgumentException("Router::setErrorCallback() - Vstup je špatného typu (očekáván řetězec).");
325:         }
326:         $this->errorCallback = str_replace(' ', '', $callback);
327:         return $this;       
328:     }
329:     
330:     
331:     /**
332:      * Vytvoří callback pro metodu delegate (z parametrů z URL podle zadané masky).
333:      * 
334:      * @throws RuntimeException Pokud jsou v callbacku požadované parametry, které v routě chybí.
335:      */
336:     final protected function resolveCallbackName()  {
337:         $tmp = $this->callbackMask;
338:         if (preg_match_all('~<([^>]+)>~', $tmp, $m, PREG_PATTERN_ORDER))    {
339:             foreach ($m[1] as $name)    {
340:                 if (!isset($this->params[$name]))   {
341:                     throw new RuntimeException("Router: V callbacku je požadovaný parametr '$name', ale v parametrech z URL chybí.");
342:                 }
343:                 $tmp = str_replace("<$name>", $this->params[$name], $tmp);
344:             }
345:         }
346:         $this->callback = $tmp;
347:     }
348:     
349:     
350:     /**
351:      * Vytvoří spustitelné callbacky z řetězce. Pokud se jméno třídy opakuje, použije jen jedinou instanci.
352:      * @param string $str
353:      * @return array
354:      * 
355:      * @throws BadFunctionCallException Pokud callback neexistuje.
356:      */
357:     protected function callbackFromString($str) {
358:         
359:         $cb = array();
360:         $instances = array();
361:         foreach (explode(',', $str) as $part)   {
362:             $tmp = explode(':', $part);
363:             
364:             /* Volá se třída + metoda  */
365:             if (isset($tmp[1])) {
366:                 list($class, $method) = $tmp;
367:                 if (!class_exists($class))  {
368:                     throw new BadFunctionCallException("Router: Třída '$class' neexistuje.");
369:                 }
370:                 if (!isset($instances[$class])) {
371:                     $instances[$class] = new $class;
372:                 }
373:                 if (!method_exists($instances[$class], $method))    {
374:                     throw new BadFunctionCallException("Router: Metoda '$class::$method' neexistuje.");
375:                 }
376:                 $cb[] = array($instances[$class], $method);
377:             
378:             /* Volá se funkce */
379:             } else {
380:                 $func = $tmp[0];
381:                 if (!function_exists($func))    {
382:                     throw new BadFunctionCallException("Router: Funkce '$func' neexistuje.");
383:                 }
384:                 /* Funkce dostane všechny parametry z rozebrané URL. */
385:                 $cb[] = $func;
386:             }
387:         }
388:         
389:         return $cb;
390:     }
391:     
392:     
393:     /**
394:      * Předá řízení. Nastavené metody/funkce dostanou parametry z rozebrané URL.
395:      * 
396:      * @throws BadFunctionCallException Pokud callback neexistuje.
397:      */
398:     public function delegate()  {
399:         
400:         try {
401:             foreach ($this->callbackFromString($this->callback) as $cb) {
402:                 call_user_func($cb, $this->params);
403:             }
404:         } catch (BadFunctionCallException $e)   {
405:             if (!empty($this->errorCallback))   {
406:                 foreach ($this->callbackFromString($this->errorCallback) as $cb)    {
407:                     call_user_func($cb, $this->params);
408:                 }
409:             } else {
410:                 throw $e;
411:             }
412:         }
413:     }
414:     
415:     
416:     /**
417:      * Přesměruje na URL podle nastavení zadané routy.
418:      * @param Route $route
419:      * 
420:      * @throws UnexpectedValueException Pokud něco selže během nastavení cílové URL.
421:      */
422:     private function redirectRoute(Route $route)    {
423:         try {
424:             $redirParams = $route->getRedirParams();
425:             /* Zkopírování požadovaných hodnot ze současné URL */
426:             foreach ($redirParams as $name => $value)   {
427:                 if ($value === self::COPY_OLD_VALUE)    {
428:                     $redirParams[$name] = !empty($this->params[$name]) ? $this->params[$name] : '';
429:                 }
430:             }
431:             
432:             $newUrl = $this->url($redirParams);
433:             $this->redirect($newUrl);
434:             die();
435:             
436:         } catch (Exception $e)  {
437:             throw new UnexpectedValueException("Nelze přesměrovat routu. Popis chyby: ".$e->getMessage());
438:         }
439:     }
440:     
441:     
442:     /**
443:      * Přesměruje na zadanou URL. 
444:      * @param string $newUrl
445:      * @param int $code HTTP kód odpovědi, výchozí hodnota odpovídá stavu "Moved Permanently".
446:      */
447:     protected function redirect($newUrl, $code = 301)   {
448:         if (headers_sent()) {
449:             @ header("refresh:1;url=$newUrl");
450:         } else {
451:             @ header("Location: ".$newUrl, true, $code);
452:         }
453:         exit;
454:     }
455:     
456: 
457:     /**
458:      * Zjistí, jestli už existuje šablona URL s tímto jménem.
459:      * @param string $tplName Název
460:      * @return bool
461:      */
462:     final public function templateExists($tplName)  {
463:         return isset($this->urlTemplates[$tplName]);
464:     }
465:     
466:         
467:     /**
468:      * Zjistí základní URL aplikace (bez koncového lomítka)
469:      */
470:     protected function setBaseUrl() {
471:         $dirName = dirname($_SERVER['PHP_SELF']);
472:         $dirName = rtrim($dirName, '/\\');
473:         $this->baseUrl = "http://$_SERVER[SERVER_NAME]" . $dirName;
474:         $this->baseUrl = str_replace($this->ignoredUrlPart, '', $this->baseUrl);
475:     }
476: 
477:     
478:     /**
479:      * Nastaví část, která se má ve všech vstupních URL ignorovat.
480:      * @param string $part 
481:      * @return Router (fluent interface)
482:      * 
483:      * @throws InvalidArgumentException Pokud vstup není řetězec.
484:      */
485:     final public function setIgnoredUrlPart($part)  {
486:         if (!is_string($part))  {
487:             throw new InvalidArgumentException("Router: Zadaná část URL, která se má vynechat, je špatného typu (očekáván řetězec).");
488:         }
489:         $this->ignoredUrlPart = $part;
490:         $this->setBaseUrl();
491:         return $this;
492:     }
493:     
494:     
495:     /**
496:      * Nastaví prefix pro všechny výstupní URL. 
497:      * @param string $prefix
498:      * @return Router (fluent interface)
499:      * 
500:      * @throws InvalidArgumentException Pokud vstup není řetězec.
501:      */
502:     final public function setOutputUrlPrefix($prefix)   {
503:         if (!is_string($prefix))    {
504:             throw new InvalidArgumentException("Router: Prefix pro výstupní URL je špatného typu (očekáván řetězec).");
505:         }
506:         $this->outputUrlPrefix = $prefix;
507:         return $this;
508:     }
509:     
510:     
511:     /**
512:      * Nastaví název parametru $_GET, do kterého se v .htaccess ukládá požadovaná URL. Výchozí hodnota je "route".
513:      * @param string $param
514:      * @return Router (fluent interface)
515:      * 
516:      * @throws InvalidArgumentException Pokud vstup není řetězec.  
517:      */
518:     final public function setRouteKey($param)   {
519:         if (!is_string($param)) {
520:             throw new InvalidArgumentException("Router::setRouteKey() - Zadaná hodnota je špatného typu (očekáván řetězec).");
521:         }
522:         $this->urlRouteKey = $param;
523:         return $this;
524:     }
525:     
526:     
527:     /**
528:      * Vrací základní adresu aplikace (bez koncového lomítka)
529:      * @return string 
530:      */
531:     final public function getBaseUrl()  {
532:         return $this->baseUrl;
533:     }
534:     
535:     
536:     /**
537:      * Vrací URL aktuálního požadavku (bez koncového lomítka)
538:      * @return string 
539:      */
540:     final public function getCurrentUrl()   {
541:         return $this->currentUrl;
542:     }
543:     
544: 
545:     /**
546:      * Vrátí rozebranou URL.
547:      * @return array
548:      */
549:     final public function getParams()   {
550:         return $this->params;
551:     }
552:     
553:     
554:     /**
555:      * Getter
556:      * @param string $propertyName
557:      * @return mixed
558:      */
559:     public function __get($propertyName)    {
560:         $func = 'get'.ucfirst($propertyName);
561:         if (method_exists($this, $func))    {
562:             return $this->$func();
563:         } else {
564:             trigger_error("Router: Vlastnost '$propertyName' neexistuje, nebo není přístupná.", E_USER_NOTICE);
565:         }
566:     }
567:     
568:     
569:     /**
570:      * Setter není povolen.
571:      * @param string $propertyName
572:      * @param string $propertyValue
573:      */
574:     final public function __set($propertyName, $propertyValue) {
575:         trigger_error("Router: Vlastnost '$propertyName' neexistuje, nebo není veřejně přístupná.", E_USER_WARNING);
576:     }
577: 
578:         /* array access: --------------------  */
579:     
580:     /**
581:      * @see ArrayAccess::offsetSet()
582:      */
583:     public function offsetSet($offset, $value)  {
584:         if ($value instanceof Route)    {
585:             $this->routes[] = $value;
586:         } else {
587:             throw new InvalidArgumentException("Zadaná hodnota musí být instance třídy Route.");
588:         }
589:     }
590:     
591:     
592:     /**
593:      * @see ArrayAccess::offsetExists()
594:      */
595:     public function offsetExists($offset)   {
596:         return isset($this->routes[$offset]);
597:     }
598:     
599:     
600:     /**
601:      * Odstranění rout není povoleno.
602:      * @see ArrayAccess::offsetUnset() 
603:      */
604:     public function offsetUnset($offset)    {
605:         throw new LogicException("Odstraňování rout není povoleno.");
606:     }
607:     
608:     
609:     /**
610:      * @see ArrayAccess::offsetGet()
611:      */
612:     public function offsetGet($offset)  {
613:         return isset($this->routes[$offset]) ? $this->routes[$offset] : null;
614:     }
615: }
616: 
PHP Router ver.1.3, r02 API documentation generated by ApiGen 2.6.1