Routing pozwala nam na uzyskanie praktycznie dowolnych adresów URL w naszej aplikacji. Jeśli nie wiesz jeszcze zbyt wiele na temat routingu, to proponuję abyś najpierw zapoznał się z odpowiednim rozdziałem z naszego podręcznika. Routing w CodeIgniterze jest dosyć elastyczny. Istnieją jednak momenty, w których konieczność wpisywania na stałe reguł routingu w pliku, staje się lekko uciążliwa.
Załóżmy, że w naszej aplikacji chcemy mieć możliwość dodawania stron, które mogłyby mieć dowolną nazwę. Czyli możliwe są np. następujące adresy:
"o-nas"
"o-nas/zespol"
Zauważ, że nie chcemy mieć w adresach URL żadnych liczb, ani dodatkowych przedrostków, tylko taki adres, jaki widać. Oba powyższe adresy mają więc kierować do jednego kontrolera, w którym w zależności od adresu URL, będzie wczytywana odpowiednia zawartość. Zazwyczaj zrobilibyśmy to w następujący sposób:
$route['o-nas'] = 'pages/show/1'; $route['o-nas/zespol'] = 'pages/show/2';
Niestety jest to mało „dynamiczne” rozwiązanie, ponieważ za każdym razem kiedy chcemy dodać stonę, musimy też edytować zawartość pliku routes.php. Przyjmijmy więc, że pracujemy z następującą tabelą w bazie danych do przechowywania naszych stron:
-- ----------------------------------------------------- -- Table `pages` -- ----------------------------------------------------- CREATE TABLE `pages` ( `id` MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT, `slug` VARCHAR(50) NOT NULL, `content` TEXT, PRIMARY KEY (`id`), INDEX `slug` (`slug`) ) ENGINE = InnoDB;
Naszym pierwszym pomysłem, może być stworzenie następujących reguł w pliku routes.php:
$route['(:any)/(:any)'] = 'pages/show/$1/$2'; $route['(:any)'] = 'pages/show/$1';
Taki zapis zadziała, ale nie będzie zbyt elastyczny. Po pierwsze dla każdego kolejnego „poziomu” w adresie URL, będziemy musieli tworzyć dodatkową regułkę. Jeśli więc będziemy chcieli uzyskać adres
"o-nas/zespol/patrycja", to konieczne będzie dodanie kolejnej reguły routingu:
$route['(:any)/(:any)/(:any)'] = 'pages/show/$1/$2/$3';
Nie jest to może najwygodniejsze rozwiązanie, ale można z tym żyć. Czy da się to jednak trochę uprościć, aby nie trzeba było się martwić o kolejne poziomy zagnieżdżenia naszego adresu URL? Oczywiście. Na pierwszy rzut oka jest to dosyć niestandardowe rozwiązanie, ale sprawuje się doskonale. Chodzi o skorzystanie z reguły routingu, wykorzystywanej do przesłaniania błędów. Wystarczy, że w pliku routes.php nadamy wartość dla zmiennej w tablicy o indeksie "404_override":
// Jeśli żadna z reguł routingu nie spełnia wymagań, zostanie wywołana strona błędu. // W tym miejscu przesłaniamy standardowe wywołanie i zastępujemy je wywołaniem kontrolera Override $route['404_override'] = 'override';
Teraz tworzymy kontroler, który zajmie się obsługą naszych dynamicznych adresów.
<?php
class Override extends CI_Controller {
public function index()
{
// Pobieramy aktualny adres URL (wszystko co znajduje się po index.php)
$slug = $this->uri->uri_string();
// Pamiętaj, że dobrą praktyką jest umieszczanie zapytań do bazy danych w modelu (to poniżej, to tylko szybki przykład)
if ($data = $this->db->select('content')->where('slug', $slug)->get('pages')->row_array())
{
// Jeśli znalezionio stronę o takim samym adresie (wartość slug), to strona jest wyświetlana
$this->load->view('page_show', $data);
}
else
{
// W innym przypadku generujemy standardową stronę błędu
show_404();
}
}
}
/* End of file override.php */
/* Location: ./application/contollers/override.php */
Dzięki takiemu rozwiązaniu, nie musimy się martwić o zmienianie reguł routingu dla kolejnych „poziomów” naszego adresu URL – takie podejście jest już całkiem przyjemne. Został nam ostatni sposób i jeśli można tak to określić, najbardziej skomplikowany – zapis dynamicznych reguł routingu do pliku. Jak możemy to zrobić? Możemy zapisać do pliku nasze dynamiczne reguły routingu i wczytywać je w pliku routes.php. W najprostszej postaci może to wyglądać mniej więcej w ten sposób:
<?php
class Page_model extends CI_Model {
...
// Ta metoda będzie wywoływana zawsze po stworzeniu lub zmodyfikowaniu strony
private function _save_routes()
{
// Pobieramy identyfikator i nazwę adresu URL (slug) dla każdej strony
if ($rows = $this->db->select('id, slug')->get('pages')->result_array())
{
// Otwieramy znacznik i dodajemy standardowy nagłówek zabezpieczający przed bezpośrednim dostępem do pliku
$data[] = "<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');";
// Tworzymy tablicę z regułami routingu
foreach($rows as $row)
{
$data[] = '$route["' . $row['slug'] . '"] = "pages/show/' . $row['id'] . '";';
}
// Łączymy kolejne elementy tablicy, oddzielając je znacznikiem nowej linii
$output = implode("\n", $data);
// Ładujemy helper file
$this->load->helper('file');
// Zapisujemy dynamiczne reguły routingu w pliku
write_file(APPPATH . 'cache/dynamic_routes.php', $output);
}
}
}
/* End of file page_model.php */
/* Location: ./application/models/page_model.php */
W pliku routes.php pozostaje nam teraz jedynie wczytanie pliku z dynamicznymi regułami routingu, który wcześniej stworzyliśmy:
$route['default_controller'] = 'welcome'; $route['404_override'] = ''; // ...Pozostałe reguły routingu... // Wczytujemy nasze dynamiczne reguły routingu @include_once APPPATH . 'cache/dynamic_routes.php';
Trochę skomplikowane, w porównaniu do wcześniejszych metod, prawda? Rzeczywiście, ale takie podejście daje nam możliwość pracy z wieloma kontrolerami. Możemy więc nie tylko pracować z kontrolerem Pages, ale np. Products, jeśli on też ma mieć dowolną konstrukcję adresów URL. We wcześniejszych rozwiązaniach jest to niemożliwe (pierwszym), albo bardzo trudne (drugim) i do tego nieeleganckie.
Możecie się zastanawiać, czemu nie możemy załadować reguł routingu bezpośrednio z bazy danych, np. w pliku routes.php. Oprócz tego, że to niezbyt eleganckie rozwiązanie, to niestety tworzylibyśmy wtedy dodatkowe połączenie z bazą danych, ponieważ plik routes.php jest standardowo wczytywany o wiele wcześniej niż tworzony jest superobiekt CI.
W tym krótkim artykule poznaliśmy trzy sposoby na zbudowanie dynamicznego routingu w CodeIgniterze. Oczywiście na pewno znajdziesz jeszcze kilka innych sposobów na poradzenie sobie z tym zagadnieniem – mam nadzieję, że przedstawione przykłady będą dla Ciebie dobrym punktem wyjścia.
AKTUALIZACJA 17.05
W komentarzach pojawiły się pytania odnośnie użycia dynamicznych reguł routingu w kontekście wielu kontrolerów, dlatego postanowiłem zamieścić przykładową bibliotekę, która zobrazuje jak w przystępny sposób można do tego problemu podejść. Zwróćcie uwagę, że klasa jest naprawdę uproszczona do granic możliwości i brakuje jakiejkolwiek obsługi błędów.
Tak przy okazji, to w tym momencie można się również pokusić o implementację zdarzeń ;)
Przykładowa postać tabeli routes:
-- -----------------------------------------------------
-- Table `routes`
-- -----------------------------------------------------
CREATE TABLE `routes` (
`slug` VARCHAR(50) NOT NULL,
`url` VARCHAR(50) NOT NULL,
UNIQUE KEY `slug` (`slug`),
UNIQUE KEY `url` (`url`)
) ENGINE = InnoDB;
Przykładowa klasa Routes:
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
/**
* Biblioteka Routes
* Pozwala na generowanie pliku z dynamicznymi regułami routingu dla wielu kontrolerów
*/
class Routes {
private $ci;
public function __construct()
{
$this->ci =& get_instance();
}
/**
* Dodaje wpis routingu
*
* @param string $slug Wartość pola slug
* @param string $url Wartość "prawdziwego" adresu URL
*
* @return void
*/
public function add_route($slug, $url)
{
$this->ci->db->set(array('slug' => $slug, 'url' => $url))->insert('routes');
$this->_save_routes();
}
/**
* Aktualizuje wpis routingu
*
* @param string $slug Wartość pola slug
* @param string $url Wartość "prawdziwego" adresu URL
*
* @return void
*/
public function update_route($slug, $url)
{
$this->ci->db->set('slug', $slug)->where('url', $url)->update('routes');
$this->_save_routes();
}
/**
* Usuwa wpis routingu
*
* @param string $url Wartość "prawdziwego" adresu URL
*
* @return void
*/
public function delete_route($url)
{
$this->ci->db->where('url', $url)->delete('routes');
$this->_save_routes();
}
/**
* Generuje plik statyczny dla dynamicznego routingu
*
* @return void
*/
private function _save_routes()
{
// Pobieramy slug oraz "prawdziwą" nazwę adresu URL
if ($rows = $this->ci->db->select('slug, url')->get('routes')->result_array())
{
// Otwieramy znacznik i dodajemy standardowy nagłówek zabezpieczający przed bezpośrednim dostępem do pliku
$data[] = "<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');";
// Tworzymy tablicę z regułami routingu
foreach($rows as $row)
{
$data[] = '$route["' . $row['slug'] . '"] = "' . $row['url'] . '";';
}
// Łączymy kolejne elementy tablicy, oddzielając je znacznikiem nowej linii
$output = implode("\n", $data);
// Ładujemy helper file
$this->ci->load->helper('file');
// Zapisujemy dynamiczne reguły routingu w pliku
write_file(APPPATH . 'cache/dynamic_routes.php', $output);
}
}
}
/* End of file routes.php */
/* Location: ./application/libraries/routes.php */
Jak możemy skorzystać z tej biblioteki? To bardzo proste – przedstawię to na przykładzie metody add_route.
$this->load->library('routes');
$this->routes->add_route('o-nas', 'pages/view/1');
$this->routes->add_route('o-nas/zespol', 'pages/view/2');
$this->routes->add_route('bardzo-ciekawy-artykul', 'articles/show/25');
Trochę nie rozumiem uproszczenia korzystającego z tabeli „tables”. Przecież każdą z tych stron trzeba będzie utworzyć, tak samo jak dodając wpis w pliku routes.php. Oczywiście jest to eleganckie przechowywać to w bazie.
Witaj.
Nie do końca rozumiem czego dotyczą Twoje wątpliwości względem przechowywania stron w bazie danych… tak się po prostu robi i tyle :)
Oczywiście, zgadzam się. Tylko słowo „uproszczenie” dla tej koncepcji zwyczajnie nie pasuje.
O uproszczeniu pisałem w kontekście wykorzystania reguły routingu do przesłaniania błędów. Zamiast regułek dla każdego kolejnego „zagnieżdżenia adresu” mamy jedną regułkę „404_override”. W odniesieniu do pliku routes.php, można to moim zdaniem nazwać uproszczeniem.
Przecież i tak jeśli chcesz wyświetlić na stronie jakąkolwiek treść to musisz ją gdzieś przechowywać – najwygodniej w bazie danych. Nie widzę więc żadnego problemu w tym by do odpowiedniej tabeli dodać jedną dodatkową kolumnę ze slugiem (który de facto może być automatycznie generowany np. na podstawie tytułu wpisu etc.).
Do moich projektów zwykły slug w bazie raczej wystarcza lub coś w stylu $route[kontroler/(:any),(:num)] = „sasasasa/hahaha/$2”;
Aczkolwiek rozwiązanie nr2 bardzo mi się podoba, tylko co jeśli nam ten keszu spuchnie?
gosc says:
05/10/2013 at 10:08
A co jeśli slug musi być nie tylko w jednej tabeli tylko w 2,3,4? np. pages i news i articles ? Zrobisz tabele pod slugi, to bedzie problem, i zaczną się rozwiązanie które ostatecznie tylko ty będziesz rozumiał :) 2. to co zostanie dolaczone do routes, system bedzie szukal po ID rekordu, bedzie to szybsze niz szukanie po slug-u.
Ad. rozwiązanie z dynamic_routes
Zrobił to ktoś dla więcej niż jednej tabeli ? Nie tylko dla pages tylko np. dla pages, news i articles ?
Zaktualizowałem wpis o taki przykład, z prostą biblioteką do obsługi.
Racja prosta spraw. Dzięki.
Wprowadzam do moich projektów ;)