Dynamiczny routing w CodeIgniter

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');

9 komentarzy do “Dynamiczny routing w CodeIgniter”

  1. 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.

    Odpowiedz
    • 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 :)

      Odpowiedz
      • Oczywiście, zgadzam się. Tylko słowo „uproszczenie” dla tej koncepcji zwyczajnie nie pasuje.

        Odpowiedz
        • 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.

          Odpowiedz
    • 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.).

      Odpowiedz
      • 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.

        Odpowiedz
  2. 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 ?

    Odpowiedz

Dodaj komentarz

Ta strona używa Akismet do redukcji spamu. Dowiedz się, w jaki sposób przetwarzane są dane Twoich komentarzy.