Routing w Kohana 3.1

Podziel się z innymi!

    Z frameworkiem Kohana pracuję już kilka lata. O ile Kohana w wersji 2 była przyjazna i w miarę dobrze udokumentowana o tyle 3-cie wydanie tego frameworka jest już mniej przyjazne. Nie zrozumcie mnie źle – ogólnie zmiany w architekturze i implementacji oceniam pozytywnie, ale ogromne braki w dokumentacji i ciągłe zmiany w kodzie mają zdecydowanie negatywny wpływ na przyjemność pracy z tym oprogramowaniem.

    Zabieram się właśnie za nowy projekt. Jest niewielki więc postanowiłem wypróbować Kohanę w najnowszej „stabilnej” wersji 3.1. Utworzyłem katalog dla projektu, przekopiowałem do niego pliki frameworka, utworzyłem plik hteaccess według wzoru i odpaliłem w przeglądarce adres.

    http://localhost/project/
    

    Uruchomił mi się instalator, który poinformował mnie, że muszę nadać prawa do zapisu odpowiednim katalogom. Pozostałe wymagane warunki miałem spełnione. Nie miałem PECL HTTP i cURL ale to opcjonalne zależności bez, których wszystko, a przynajmniej fundamenty Kohany powinny ruszyć więc je zignorowałem. Zgodnie z instrukcją zmieniłem nazwę plikowi install.php i…

    Otrzymałem komunikat błędu

    HTTP_Exception_404 [ 404 ]: The requested URL project/index was not found on this server.
    

    Popularny błąd 404 – not found – wydawałoby się i wszystko jasne. Tylko, że to pierwsze uruchomienie frameworka, w który jest wstępnie skonfigurowany, posiada domyślny kontroler „welcome” a w nim domyślną akcję „index” tak więc powinienem zobaczyć przyjazny napis „hello, world!”.

    Gdyby to było moje pierwsze zetknięcie z frameworkiem Kohana to pewnie po pierwszych 5 min. dałbym sobie spokój. Nie dość, że przy pierwszym uruchomieniu dostajemy exceptiona to jeszcze tak naprawdę nic nam on nie mówi o możliwościach jego rozwiązania. W końcu kontroler jest tam gdzie trzeba, klasa ma odpowiednią nazwę i jest metoda, która zgodnie z dokumentacją powinna zostać wywołana.

    Tymczasem przyczyną jest zła konfiguracja. W pliku /application/bootstrap.php mamy taki fragment kodu

    Kohana::init(array(
        'base_url'   => '/',
    ));

    Parametr base url w moim przypadku powinien wyglądać tak

    Kohana::init(array(
        'base_url'   => '/project/',
    ));

    Proste prawda? Tylko ktoś kto dopiero poznaje tę platformę się tego nie domyśli. Przyznaję się, że sam się trochę tego naszukałem, a to dlatego, że kiedyś już rozwiązałem ten problem kawałkiem uniwersalnego kodu i zdążyłem o nim zapomnieć. Tymczasem teraz stawiając nowy projekt na świeżutkiej Kohanie prosty problemik wrócił do mnie i trafił w głowę.

    $dirname = dirname($_SERVER['SCRIPT_NAME']);
    $base_url = preg_replace('@/+$@', '', $dirname=="\\"?'':$dirname).'/'; 
     
    Kohana::init(array(
        'base_url'   => $base_url,
    ));

    Dla purystów, którzy nie lubią zbędnych operacji i nie ufają zbytnio uniwersalności tego rozwiązania (w końcu było testowane tylko na serwerach z Apache 2) proponuję nieco zmodyfikowaną wersję

    Kohana::init(array(
    	'base_url'   => '/',
    ));
     
    if (Kohana::$environment != Kohana::PRODUCTION) {
        $dirname = dirname($_SERVER['SCRIPT_NAME']);
        $base_url = preg_replace('@/+$@', '', $dirname=="\\"?'':$dirname).'/';
        if (Kohana::$base_url != $base_url) {
            throw new Kohana_Exception(sprintf('Perhaps you have a bad parameter set 
                base_url in bootstrap.php. Most likely, the correct value should be 
                "%s"', $base_url));
        }
    }

    Routing w Kohana nie jest szczytem elegancji, wygody i elastyczności. W stosunku do routingu w Django, każdy tego typu system w PHP jest prymitywną próbą naśladownictwa. Wynika to w dużej mierze z ograniczeń wyrażeń regularnych w PHP, które aż do wersji 5.2.2 (PCRE 7.0) – przynajmniej według dokumentacji – nie obsługiwały nazwanych podwzorców (named subpattern).

    $str = 'foobar: 2008';
    preg_match('/(?P<name>\w+): (?P<digit>\d+)/', $str, $matches);
    print_r($matches);
    //Array
    //(
    //    [0] => foobar: 2008
    //    [name] => foobar
    //    [1] => foobar
    //    [digit] => 2008
    //    [2] => 2008
    //)

    Ja mam na swoim kompie PHP 5.3.5 (PCRE 8.12) i dalej mi to nie działa.

    Nie mniej routing w Kohana 3.1 wzbogacił się o możliwość definiowania ścieżek z użyciem funkcji lambda lub callback – w zależności od wersji PHP oczywiście. Daje to spore możliwości, których namiastkę spróbuję teraz zaprezentować.

    Standardowe ustawienia routingu w Kohana Framework wyglądają następująco.

    Route::set('default', '(<controller>(/<action>(/<id>)))')
    	->defaults(array(
    		'controller' => 'welcome',
    		'action'     => 'index',
    	));

    Powoduje to, że do strony głównej serwisu możemy się odwołać w trojaki sposób.

    http://domena.com/
    http://domena.com/welcome/
    http://domena.com/welcome/index
    

    Jest to problem SEO tylko jeśli w projekcie budujemy różne linki odwołujące się do strony głównej. Jeśli dojdzie do tego wielojęzyczność do adresu zostanie dodany dodatkowy parametr (no chyba, że strony w poszczególnych językach trzymane są na subdomenach). Wtedy strona główna będzie występować w różnych wersjach językowych, a dodatkowo strona główna w języku domyślnym będzie w wersji z oznaczeniem języka i bez.

    http://domena.com/
    http://domena.com/pl-pl/
    

    Dobrze byłoby zrobić aby strona główna w domyślnej wersji językowej była dostępna tylko pod adresem „/”. Natomiast próba wejścia przez uri „/pl-pl/” kończyła się przekierowaniem na „/”. Po wpisaniu w adres przeglądarki adresów „/welcome/” lub „/welcome/index” powinien być zgłoszony błąd 404.

    Aby to osiągnąć stworzyłem klasę ProcessRoute w pliku /application/classes/processroute.php

    <?php defined('SYSPATH') or die('No direct script access.');
     
    class ProcessRoute {
        public static function main_page($uri) {
            $base_url = Kohana::$base_url;
            if ($uri == I18n::$lang) {
                header("Location: {$base_url}", true, 302); die();
            } else if ($uri == '') {
                if ($_SERVER["REQUEST_URI"] != $base_url) {
                    throw new HTTP_Exception_404('Unable to find a route to match the URI: :uri'
                    , array(':uri' => str_replace($base_url, '', $_SERVER["REQUEST_URI"])));
                }
                return array(
    		        'lang' => 'pl-pl',
    		        'directory' => '',
    		        'controller' => 'welcome',
    		        'action' => 'index',
    	        );
            } else if (preg_match('/^[a-z]{2,2}-[a-z]{2,2}$/', $uri)) {
                return array(
    		        'lang' => $uri,
    		        'directory' => '',
    		        'controller' => 'welcome',
    		        'action' => 'index',
    	        );
            }
            return false;
        }
    }

    Myślę, że dodatkowego wyjaśnienia wymaga jedynie fragment

    if ($_SERVER["REQUEST_URI"] != $base_url) {
        throw new HTTP_Exception_404('Unable to find a route to match the URI: :uri'
        , array(':uri' => str_replace($base_url, '', $_SERVER["REQUEST_URI"])));
    }

    Otóż w przypadku wpisania w pole adresu przeglądarki urla „http://localhost/project/index/” wartość uri przekazana do metody ProcessRoute::main_page będzie miała pustą wartość „”. Jest to ewidentny bug i dlatego musiałem zastosować to nieeleganckie obejście.

    Aby podpiąć wyżej zaprezentowaną klasę do routingu należy w bootstrapie dodać

    /**
     * Set the routes. Each route must have a minimum of a name, a URI and a set of
     * defaults for the URI.
     */
    Route::set('main', array('ProcessRoute', 'main_page'));

    Na stronie głównej budowa witryny internetowej się nie kończy. Podpięcie funkcji zwrotnej pod routing daje o wiele większe możliwości. Załóżmy, że chcę wyświetlać dane kontaktowe różne dla różnych wersji językowych.

    class Controller_Contact extends Controller {
     
        public function action_index()
        {
            $lang = Request::current()->param('lang');
            if ($lang == 'en-en') {
                $this->response->body('Contact');
            } else {
                $this->response->body('Kontakt');
            }
        }
    } // Contact

    Chciałbym aby polską wersję podpiąć pod adres „/pl-pl/kontakt”, a w wersję angielską pod „/en-en/contact”. W tym celu stworzyłem sobie plik konfiguracyjny /application/config/routes.php.

    return array
    (
    	'pl-pl' => array(
    	    'kontakt' => array(
    		    'controller' => 'contact',
    		    'action' => 'index',
    	    )
    	),
    	'en-en' => array(
    	    'contact' => array(
    		    'controller' => 'contact',
    		    'action' => 'index',
    	    )
    	)
    );

    Aby routing korzystał z tego pliku konfiguracyjnego do klasy ProcessRoute (w pliku /application/classes/processroute.php) dodałem statyczną metodę „static_pages”.

    class ProcessRoute {  
        public static function main_page($uri) {
            // ...
        }
        public static function static_pages($uri) {
            // $lang = I18n::$lang;
            if (preg_match('/^([a-z]{2,2}-[a-z]{2,2})\/(.*)/', $uri, $matches) && count($matches) == 3) {
                $lang = $matches[1];
                // if ($lang == I18n::$lang) return false;
                $uri = $matches[2];
            }
            $routes = Kohana::config("routes")->as_array();
            if (!isset($lang) || !isset($routes[$lang])) {
                return false;
            } else {
                $routes = $routes[$lang];
            }
            if (array_key_exists($uri, $routes)) {
                $route = $routes[$uri];
                $route['lang'] = $lang;
                return $route;
            } 
            return false;
        }
    }

    Proszę zwrócić uwagę na wykomentowaną linię.

    $lang = I18n::$lang;

    Chciałem mieć pewność, żeby za wyjątkiem strony głównej na wszystkich podstronach był używany parametr wersji językowej. Mogę odkomentować wspomnianą linię i wtedy dane kontaktowe pojawią się zarówno po wybraniu adresu „/pl-pl/kontakt”, jak i „/kontakt”. Jeśli do tego odkomentuję jeszcze linię

    if ($lang == I18n::$lang) return false;

    wtedy z kolei dane kontaktowe w domyślnej wersji językowej będą dostępne jedynie pod adresem „/kontakt”.

    Zapomniałbym. W bootstrapie trzeba jeszcze wywołać metodę ProcessRoute::static_pages.

    Route::set('static_pages', array('ProcessRoute', 'static_pages'));

    Jak pokazują te proste przykłady routing w Kohana 3.1 zyskał na elastyczności. Odrobina pracy i można by z tego mechanizmu wycisnąć dużo więcej. Panel administracyjny można oprzeć o tradycyjny routing oparty na kontrolerach i akcjach z kolei część publiczną adresować z wykorzystaniem plików konfiguracyjnych lub też trzymać ścieżki w bazie danych.

    Podziel się z innymi!

      6 Comments

      1. skowron-line

        Fajny artykuł, ale w odpowiedzi na 1 problem można było napisać tyko o ustawieniu VirtualHost -a no ale to jedeno z 2ch rozwiązań.

      2. spambot

        Artykuł się przydał gdybym tego nie znalazł że trzeba ścieżke zmienić w bootstrap.php to bym nawet 1 kroku nie przeszedł, już mnie tak wnerwiła ta kohana że sobie ją daruje

      3. allmaechtig

        Świetny artykuł, krótko, przystępnie i na temat – myślę że wielu ludziom przyoszczędziłeś poro czasu. Wsparcie dla Kohany dla kogoś kto pierwszy raz się styka z frameworkiem jest moim zdaniem zerowe, Symfony czy ZendfFamework są tu zdecydowanie bardziej przyjazne. Na szczęście są jeszcze społeczności 🙂

        Pozdrawiam i raz jeszcze dzięki za arta.

      4. BloodMan

        Hm. Jakby tu powiedzieć… Troche FAIL ten framework. Nie wiem dla kogo, no ale widać się sprzedaje…

      5. 666

        Отлично, про кусок универсального кода для роутинга я сам бы и не додумался

      6. Andrychów blog

        Cześć,
        początkowo umieściłem stronę na virtualhoście kohana/ ale 404 wyskakiwała cały czas,
        stworzyłem folder kohana, przeniosłem tam pliki, w bootstrap.php miałem ustaiwony base_url na /kohana/ i działa,
        dokumentacja Kohany np. vs Phalcona jest bardzo skromna

      Dodaj komentarz

      Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *