Przyjazne URLe - piszemy router

Czym są przyjazne urle? Są to adresy w formacie http://strona.pl/nowosci/php/nowa-wersja-php-wydana, a nie tak standardowe i niewygodne adresy ze zmienny GET: http://strona.pl/index.php?module=news&category=php&title=nowa-wersja-php-wydana. Pierwszą rzeczą która skłania programistów do korzystania z przyjaznych urli jest to że są one rzeczywiście przyjazne. Można bardzo prosto zapamiętać taki link, znacznie łatwiej niż standardowy link ze zmiennymi GET. Wygodnie możemy na przykład umieszczać reklamy w gazetach - http://sklep.pl/konkurs. Drugą sprawą jest to iż przyjazne urle są łatwe do indeksowania dla wyszukiwarek. Roboty indeksujące strony wyżej zindeksują nasz pierwszy przykładowy link niż ten drugi, przestarzały rupieć ;)

Podstawy

Ok, po wstępnie zapewne czytelniku jesteś żądny wiedzy. Jak uzyskać takie urle? Bardzo prosto. Cała magia zawiera się w PATH INFO.
2 krótkie linijki:

  1. < ?php
  2. $pathInfoParams = explode(’/‘, substr($_SERVER[’PATH_INFO’], 1));
  3. var_dump($pathInfoParams);
  4. ?>

Prosty skrypt który po odpaleniu, pokaże nam dump’a tablicy, którą utworzyliśmy rozbijając dane z patch info ;) Wiedząc skąd brać nasze wejście trzeba by to teraz jakoś obsłużyć.
Router – wygodny interfejs

Skoro wszystko trzymane jest w PATH INFO, wiemy również jak to przetworzyć i użyć. Jednak takie rozwiązanie nas w ogóle nie zadowala. Strukturalny kod, który nie wygodnie wdrożyć w systemie. Przydało by się napisać ładną klasę odpowiedzialną za wszystko związane z adresem. I tutaj pojawia się nazwa Router. Cóż to za cudo? Router w aplikacjach internetowych służy do analizowania URLi, oraz ich generowania.

Przedstawię teraz przykładową implementację Routera:

  1. < ?php
  2.  class NiceUrlsRouter {
  3.  
  4.    protected $input = null;
  5.    
  6.    public $controller = null;
  7.    public $action = null;
  8.    public $params = null;   
  9.    
  10.    const URL_DELIMITER = ‘/’;

Zadeklarowaliśmy 4 właściwości przechowywujące odpowiednio, wejście, kontroler, akcję oraz tablicę z parametrami. Na końcu jedna stała która przechowuje separator. Dalej piszemy niezbędne metody:

  1. /**
  2.     * Ustawienie inputu który później
  3.     * zostaje parsowany
  4.     *
  5.     * @param string $input
  6.     */
  7.    public function setInput($input){
  8.           $this -> input = $this -> prepare($input);
  9.          }
  10.         
  11.          /**
  12.          * Ustawienie parametrow
  13.          *
  14.          * @param array $params
  15.          */
  16.          private function setParams($params){          
  17.           $this -> controller = array_shift($params);
  18.           $this -> action = array_shift($params);
  19.           $this -> params = $params;     
  20.          }
  21.         
  22.         
  23.          /**
  24.          * Przygotowanie urla do parsowania.
  25.          * Czyszczenie ze zbednych rzeczy.
  26.          *
  27.          * @param string $input
  28.          * @return string
  29.          */
  30.          protected function prepare($input){      
  31.               
  32.               if(substr($input, 0, 1) == ‘/’){
  33.                 $input = substr($input, 1);
  34.               }
  35.              
  36.            if(substr($input, -1) == ‘/’){
  37.                $input = substr($input, 0, -1);
  38.            }
  39.                  
  40.           return $input;
  41.          }
  42.         
  43.          /**
  44.          * Parsowanie inputu
  45.          *
  46.          * @return bool
  47.          */
  48.          public function parse(){
  49.               
  50.                if($this -> input){
  51.                  $this -> setParams(explode(self::URL_DELIMITER, $this -> input));
  52.                  return true;
  53.                }
  54.               
  55.            return false;
  56.          }
  57.         
  58.          /**
  59.          * Tworzenie url’a
  60.          *
  61.          * @param array $params
  62.          * @return string
  63.          */
  64.          public static function url($params){
  65.            
  66.                $url = ‘http://’.$_SERVER[‘SERVER_NAME’].str_replace(‘index.php’, , $_SERVER[‘SCRIPT_NAME’]);
  67.               
  68.                if(!is_array($params)){
  69.                 $params = func_get_args();      
  70.                }         
  71.               
  72.                $params = implode(self::URL_DELIMITER, $params);
  73.               
  74.                return $url.$params;
  75.          }
  76.         
  77.  }

Nie mam tutaj nic skomplikowanego. Krótki opis najważnieszych metod:

  • bool parse() - parsuje zawratość właściwości $input, pierwsze 2 elementy są umieszczane odpowiednio we właściwościach $contoller i $action, reszta ląduje w $params.
  • string url(array $params) – statyczna metoda tworząca url

Wszystko gotowe, przykładowe użycie świeżo napisanej klasy jest bardzo proste.

  1. $router = new NiceUrlsRouter();
  2.  $router -> setInput($_SERVER[‘PATH_INFO’]);
  3.  
  4.  if($router -> parse()){
  5.    echo ‘Kontroler: ‘.$router -> controller.‘<br />’;
  6.    echo ‘Akcja: ‘.$router -> action.‘<br />’;
  7.    echo ‘Parametry: ‘;
  8.    print_r($router -> params);
  9.  }
  10.  
  11.  echo ‘Wygenerowany url: ‘.NiceUrlsRouter::url(array_merge(array($router -> controller, $router -> action), $router -> params)).‘<br /><br />’;

Jeszcze tylko regułki mod_rewrite, odpowiedzialne za “usówanie” index.php z adresu:

  1. RewriteEngine On
  2. RewriteCond %{REQUEST_FILENAME} !-f
  3. RewriteCond %{REQUEST_FILENAME} !-d
  4. RewriteRule (.*) index.php?$1 [L]

Po wejściu na http://localhost/news/show-one/65234 powinniśmy zobaczyć wylistowane wartości które zostały dostarczone przez router. Wszystko działa jak powinno.

Jeszcze jedną rzeczą o której trzeba pamiętć jest umieszczenie w head tagu base, podając adres strony. Dzięki temu będziemy mogli użwać ścieżek względnych w adresach naszych obrazków, plików css etc.

  1. <base href="http://localhost/" />

RewriteRouter

Z czasem może się okazać że taka prosta funkcjonalność nam nie wystarczy i będziemy chcieli posiadać różne rodzaje URL. Przykładowo:

  • http://strona.pl/news/534.html
  • http://strona.pl/artykuly/programowanie/php/przyjazne-urle-piszemy-router.html
  • http://strona.pl/forum/temat/5851.html

Zaczynają się schody, ponieważ w takim zapisie nie ma jakiejś ogólnej zasady. Każdy zbudowany jest inaczej i nie wiemy czy pierwsza pozycja to akacja, czy może parametr. A może być tak że w ogóle nie znajdziemy informacji w URL o kontrolerze bądź akcji, a są to przecież niezbędne informacje by nasz front kontroler mógł uruchamiać system. Dodatkowo na końcu dodaliśmy “.html” symulujące rozszerzenie, statycznej strony html, co ułatwia indeksowanie, ale całkowicie komplikuje naszą sytuację. Można to rozwiązać udoskonalając nasz router o wcześniej deklarowane drogi z wyrażeniami regularnymi, oraz drogami którymi mają zostać zastąpione. Zrobimy sobie dokładnie to samo co robi mod_rewrite w Apache :)

Można sobie pomyśleć że to nie jest łatwa rzecz - nic bardziej mylnego! Prostota implementacji zaskoczyła nawet mnie w momencie gdy programowałem i okazało się że rozwiązanie jest aż tak łatwe.

Zasada działania jest bardzo prosta. Otrzymujemy na wejściu /news/534. A chcemy uruchomić moduł News i akcję ShowOne czyli url powinien wyglądać następująco: /news/showone/534. Potrzebujemy więc przepisać pierwszy URL na drugi i po problemie.

RewriteRouter jest klasą dziedziczącą po NiceUrlsRouter który napisaliśmy przed chwilą. Dodajemy właściwość przetrzymującą reguły, metody do zarządzania regułami, oraz nadpisujemy metodę parse która porównuje URL wejściowy i jeżeli zgadza się on to podmienia go. Oto cała klasa:

  1. class RewriteRouter extends NiceUrlsRouter {
  2.    
  3.      public $rules = array();
  4.      
  5.       /**
  6.        * Dodanie reguły
  7.        *
  8.        * @param string $name
  9.        * @param string $pattern
  10.        * @param string $url
  11.        */
  12.       public function addRule($name, $pattern, $url){
  13.        $this -> rules[$name] = array(‘pattern’ => $pattern, ‘url’ => $url);
  14.       }
  15.      
  16.        /**
  17.         * Ustawienie tablicy z regułami
  18.         *
  19.         * @param array $rules
  20.         */     
  21.        public function setRules($rules){
  22.            $this -> rules = $rules;
  23.        }
  24.      
  25.        /**
  26.         * Sprawdzanie wzoru z regułki.
  27.         * Jeżeli się zgadza podmiana urla i
  28.         * wykonanie NiceUrlsRouter::parse();
  29.         *
  30.         * @return bool
  31.         */
  32.        public function parse(){
  33.          
  34.            foreach($this -> rules as $name => $rule){           
  35.              $pattern = "#^".$this -> prepare($rule[‘pattern’])."$#";
  36.              if(preg_match($pattern, $this -> input)){
  37.                  $this -> setInput(preg_replace($pattern, $rule[‘url’], $this -> input));
  38.                  return parent::parse();
  39.                  break;
  40.              }
  41.            }
  42.                  
  43.            return parent::parse();
  44.        }
  45.      
  46.  }

Używanie tego routera różni się jedynie tym że przed parsowaniem powinniśmy zadeklarować regułki:

  1. $rewriteRouter = new RewriteRouter();
  2.  $rewriteRouter -> setInput($_SERVER[‘PATH_INFO’]);
  3.  
  4.  $rewriteRouter -> addRule(‘news’, ‘/news/(\d+)’, ‘/news/showOne/$1′);
  5.  $rewriteRouter -> addRule(‘pages’, ‘/strona/([a-z1-9\-\/]+)’, ‘/pages/showByPath/$1′);
  6.  
  7.  if($rewriteRouter -> parse()){
  8.    echo ‘Kontroler: ‘.$rewriteRouter -> controller.‘<br />’;
  9.    echo ‘Akcja: ‘.$rewriteRouter -> action.‘<br />’;
  10.    echo ‘Parametry: ‘;
  11.    print_r($rewriteRouter -> params);
  12.  }

Nasz router przy parsowaniu wykonuje teraz jedną czynność więcej. Sprawdza czy input zgadza się z wyrażeniem regularnym zawartym w regule. Jeżeli tak, to podmienia do z url zadeklarowanym do podmiany i ten url daje do parsowania metodzie rodzica - NiceUrlsRouter::parse().

Podsumowanie

Stworzone w artykule routery są bardzo proste. Służą one raczej jako przykładowa implementacja. Można do nich dodać bardzo dużo innych funkcjonalności. Jako przykład może posłużyć świetny router frameworka Agavi. W porównaniu do naszych 2 routerów routing w Agavi jest o wiele bardziej złożony i posiada większe możliwości. Całość składa się z kilkunastu plików, a drogi definiuje się w oddzielnym pliku xml. Można tam zagnieżdżać drogi, ustawić callback, opcje odpowiedzialne za i18n ;)

Komentarze

  1. Ja_Szczur (#) 10.27.2007 13:16

    ot, ciekawostka:
    zamiast

    if(substr($input, 0, 1) == ‘/’){
    $input = substr($input, 1);
    }
    if(substr($input, -1) == ‘/’){
    $input = substr($input, 0, -1);
    }

    proponuję używać po prostu trim($input, NiceUrlsRouter::URL_DELIMITER);

  2. greensky (#) 10.27.2007 21:38

    wszystko fajnie - ale jedna rzecz chyba nie tak - ? w mod rewrite - uniemożliwia on wykorzystanie metody get w php np w formularzach. co prawda w wypadku użycia maskowania prawdziwych adresów get przyda się raaczej rzadko - ale czasem sie jednak przyda (wyszukiwarka?)

  3. Strzałek (#) 10.31.2007 19:58

    Ja_Szczur: trim usuwa spacje, a w prepare usuwam z końca slashe. Po co? Ano po to żeby robiąc później explode nie miał pustych wpisów w tablicy.

    greensky: nie pamiętam już kiedy ostanio używałem GET’a. A wyszukiwarka? Żaden problem - index.php/szukaj/jak+napisac+router+w+php/.

    Pozdrawiam.

  4. miś uszatek (#) 11.01.2007 00:00

    Strzałek
    1. Proponuje zacząć od nauki języka polskiego:

    Po wejściu na http://localhost/news/show-one/65234 i powinniśmy wylistowane wartości które zostały dostarczone przez router

    2. nauki gramatyki - usówa

    3. Zaglądania do manuala - trim usówa spacje, a w prepare usuwam z końca slashe -> trim — Strip whitespace (or other characters) from the beginning and end of a string
    a przykład użycia podał Ci Ja_Szczur
    a tu masz link jak nie umiesz znaleźć - http://pl.php.net/manual/pl/function.trim.php

  5. Strzałek (#) 11.01.2007 16:12

    Ano przyznaję Ci rację. Zawsze żyłem w świadomości że trim usuwa tylko spacje ;)

  6. marcin (#) 12.05.2007 00:36

    do “miś uszatek”: od kiedy błąd typu “usówa” jest błędem gramatycznym? Odsyłam Cię do podstawówki… Po wiedzę.

  7. wojak (#) 05.04.2008 13:30

    a to moje zakręcone rozwiązanie :)

    function parsePath ( $url,$pat,$def )
    {
    $arr = array_fill ( 0, ( ( sizeof ( $url ) >sizeof ( $pat ) ) ?sizeof ( $url ) :sizeof ( $pat ) ) ,” ) ;
    $result3 = array_combine ( ( ( array ) $pat + array_keys ( $arr ) ) , ( array ) $url + $arr ) ;
    foreach ( $result3 as $key = > $value ) if ( $value = = NULL ) {
    unset ( $result3[$key] ) ;
    }
    $size = sizeof ( $result3 ) -1;
    foreach ( $result3 as $k = >$v ) {
    if ( is_int ( key ( $result3 ) ) ) {
    if ( key ( $result3 ) ‘pl’ ) ;

    print_r(parsePath($url,$pat,$def));

  8. wojak (#) 05.04.2008 13:31

    przepraszam nie skopiowalo się jak należy

    function parsePath ( $url,$pat,$def )
    {
    $arr = array_fill ( 0, ( ( sizeof ( $url ) >sizeof ( $pat ) ) ?sizeof ( $url ) :sizeof ( $pat ) ) ,” ) ;
    $result3 = array_combine ( ( ( array ) $pat + array_keys ( $arr ) ) , ( array ) $url + $arr ) ;
    foreach ( $result3 as $key = > $value ) if ( $value = = NULL ) {
    unset ( $result3[$key] ) ;
    }
    $size = sizeof ( $result3 ) -1;
    foreach ( $result3 as $k = >$v ) {
    if ( is_int ( key ( $result3 ) ) ) {
    if ( key ( $result3 ) ‘pl’ ) ;

    parsePath($url,$pat,$def);

  9. wojak (#) 05.04.2008 13:46

    Polecam zabezpieczyć komentarze. Dużo elementów poprostu wcięło. Tu jest mój zakręcony parser:

    http://baael.freeweb7.com/

    przepraszam za zamieszanie.

  10. Tomasz Chmielewski (#) 05.05.2008 20:53

    Warto dodac dodatkowe regulki mod_rewrite w przypadku, gdy nasza strona www korzystala z adresow typu example.com/blah=12&costam=15&gdziestam=17, a teraz korzystamy z “ladnych” example.com/costam/gdzestam - w ten sposob uzytkownik zostanie automatycznie przekierowany na “nowa” wersje strony (gdy trafi do nas np. z wyszukiwarki lub z linku na innej stronie).

Dodaj komentarz

wymagane
wymagane (nie będzie publikowane)