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' => '/',
)); |
Kohana::init(array(
'base_url' => '/',
));
Parametr base url w moim przypadku powinien wyglądać tak
Kohana::init(array(
'base_url' => '/project/',
)); |
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,
)); |
$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));
}
} |
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
//) |
$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',
)); |
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;
}
} |
<?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"])));
} |
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')); |
/**
* 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 |
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',
)
)
); |
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;
}
} |
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ę.
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; |
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')); |
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.