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.