Wzorce projektowe: Router – cz. 3

Informacje wstępne

Ten artykuł poświęcony jest omówieniu procesu przetwarzania pozyskanego wejścia na kontroler, metodę i argumenty oraz na metodzie odnajdywania właściwego pliku. Zapraszam!

Implementacja

Ostatni raz klasa miała następującą postać

class Router {

    public static $current_uri  = '';

    public static function get_uri()
    {
        if (!empty($_SERVER['PATH_INFO']))
        {
            self::$current_uri = $_SERVER['PATH_INFO'];
        }

        self::$current_uri = trim(self::$current_uri, '/ ');

        if (self::$current_uri !== '')
        {
            self::$current_uri = preg_replace('#//+#', '/', self::$current_uri);

            self::$current_uri = str_replace(' ', '_', self::$current_uri);

            self::$current_uri = preg_replace("/[^a-z0-9\\-\\_\\/\\,]/i", '', self::$current_uri);
        }
    }
}

Obecna postać klasy wygląda następująco. Poniżej kodu wytłumaczę każdą linijkę po kolei:

class Router {

    public static $current_uri  = '';
    public static $segments;
    public static $controller;
    public static $controller_path;
    public static $method    = 'index';
    public static $arguments = array();

    public static function get_uri()
    {
        if (!empty($_SERVER['PATH_INFO']))
        {
            self::$current_uri = $_SERVER['PATH_INFO'];
        }

        self::$current_uri = trim(self::$current_uri, '/ ');

        if (self::$current_uri !== '')
        {
            self::$current_uri = preg_replace('#//+#', '/', self::$current_uri);
            self::$current_uri = str_replace(' ', '_', self::$current_uri);
            self::$current_uri = preg_replace("/[^a-z0-9\\-\\_\\/\\,]/i",'',self::$current_uri);
        }
    }

    function init() {

        self::get_uri();

        self::$segments = self::$current_uri === ''
            ? array()
            : explode('/', self::$current_uri);

        $controller_path = 'controllers/';
        $method_segment  = NULL;

        foreach (self::$segments as $key => $segment)
        {
            $controller_path .= $segment;

            if ($c = $controller_path.'.php' AND is_file($c))
            {
                self::$controller = $segment;
                self::$controller_path = $c;

                $method_segment = $key + 1;

                break;
            }
            elseif (is_dir($controller_path) === FALSE)
            {
                break;
            }

            $controller_path .= '/';
        }

        if (self::$controller === NULL AND $c = $controller_path.'index.php' AND is_file($c))
        {
            self::$controller = 'index';
            self::$controller_path = $c;

            $method_segment = $key + 1;
        }

        if ($method_segment !== NULL AND isset(self::$segments[$method_segment]))
        {
            self::$method = self::$segments[$method_segment];

            if (isset(self::$segments[$method_segment + 1]))
            {
                self::$arguments = array_slice(self::$segments, $method_segment + 1);
            }
        }

        if (self::$controller === NULL)
        {
            die('error 404');
        }
    }
}

Metoda „get_uri()” została zaimplementowana już w poprzednim artykule i spełnia ona swoje zadanie w sposób całkowicie wystarczający. Przypominam, iż jej zadaniem jest pobranie i zabezpieczenie adresu podanego na wejściu w adresie naszej strony.
Nowa metoda, która pojawiła się tutaj to „init()„. Spójrzmy po kolei co ona robi:

    function init() {

        self::get_uri();

Na początku metody init() uruchamiamy naszą napisaną wcześniej metodę get_uri(), dzięki temu uzyskamy potrzebne dane (dane wejścia w zmiennej $current_uri) do dalszej pracy. Lecimy dalej:

        self::$segments = self::$current_uri === ''
            ? array()
            : explode('/', self::$current_uri);

Aby móc wyszukiwać właściwy kontroler i dobrać metodę oraz argumenty musimy stworzyć tablicę segmentów. Segmenty to po prostu elementy, które na wejściu oddzielone są slashem. Przykładowo, jeśli w przeglądarce zostanie podany taki adres:

[code]http://moja.strona/katalog/kategoria/agd[/code]

To kolejne segmenty to: katalog, kategoria i agd, zatem utworzy to tablicę o takiej postaci

Array
(
    [0] => katalog
    [1] => kategoria
    [2] => agd
)

Algorytm zatem przy zapisywaniu wartości do $segments sprawdza czy $current_uri zawiera jakiś konkretny kontroler. Jeżeli nie to zwracamy pustą tablicę (bo nie ma żadnych segmentów). W przeciwnym wypadku zwracana jest tablica utworzona przez standardową funkcję PHP explode(). Dzieli ona tekst wg podanego znaku (w naszym przypadku „/”) na kolejne elementy tablicy.

        $controller_path = 'controllers/';
        $method_segment  = NULL;

Tutaj nie ma nic szczególnego, jednakże wypada wytłumaczyć do czego te zmienne będą potrzebne.

[code]$controller_path[/code]

Jest to ścieżka do poszukiwanego kontrolera. Na początku zawiera ona nazwę katalogu w którym znajdują się kontrolery jednakże w dalszej części algorytmu stopniowo będzie ta zmienna uzupełniana o dalsze katalogi i pliki które ostatecznie doprowadzą (bądź nie) do poszukiwanego kontrolera

[code]$method_segment[/code]

Ta zmienna będzie pamiętała numer segmentu wg, którego algorytm będzie uznawał, że jest to metoda do wywołania. Innymi słowy. Jeśli w ścieżce:

[code]http://moja.strona/katalog/kategoria/agd[/code]

kontroler zostanie odnaleziony pod nazwą „katalog.php” i otrzyma on numer segmentu „0” to metoda otrzyma numer segmentu „1” (pod indeksem nr „1” jest segment o nazwie kategoria”) . Jeśli natomiast okaże się, że „katalog” i „kategoria” to tylko katalogi (w sensie ścieżki) a właściwy kontroler nazywa się „agd.php” to otrzyma on numer segmentu „2” a więc metoda otrzyma numer „3”. Pod indeksem „3” nie ma żadnego segmentu więc system uzna, że należy uruchomić metodę domyślną. Być może w tej chwili brzmi to trochę niezrozumiale ale to wszystko wytłumaczę w dalszej części algorytmu i wszystko stanie się jasne. Lećmy dalej:

        foreach (self::$segments as $key => $segment)
        {
            $controller_path .= $segment;

            if ($c = $controller_path.'.php' AND is_file($c))
            {
                self::$controller = $segment;
                self::$controller_path = $c;

                $method_segment = $key + 1;

                break;
            }
            elseif (is_dir($controller_path) === FALSE)
            {
                break;
            }

            $controller_path .= '/';
        }

Mamy trochę większy „kąsek” 🙂 Pamiętacie jak do zmennej „$segments” zapisywaliśmy pustą tablicę bądź wyniki działania funkcji „explode”? Teraz zostanie uruchomiona pętla iterująca po tych „segmentach” Jeśli tablica była pusta to pętla się nie wykona (zostanie wykonana procedura domyślna znajdująca za tą pętlą). Jeśli jednak zostały pobrane jakieś segmenty to pętla się wykona i oto co ona robi:

  1. Pierwszym krokiem pętli jest „doklejenie” do ścieżki w zmiennej „$controller_path” pierwszego segmentu. W naszym przykładzie pierwszy segment nazywa się „katalog”. Zatem w pierwszym kroku zmienna $controller_path przyjmie wartość „/controllers/katalog”
  2. W wyrażeniu warunkowym zapisujemy wartość $controller_path do zmiennej $c wraz z rozszerzeniem pliku „.php” i sprawdzamy za pomocą funkcji „is_file()” czy jest taki plik. Jeśli jest to znaczy, że znaleźliśmy kontroler. Do pola „$controller” zapisujemy nazwę segmentu (to będzie nazwa kontrolera, który należy uruchomić). Zapisujemy także tą ścieżkę do pola $controller_path oraz ustawiamy do zmiennej $method_segment numer obecnego indeksu zwiększony o 1 (bo metoda jest zawsze kolejnym segmentem po kontrolerze – tłumaczyłem to wcześniej) i przerywamy pętle.
  3. Jeżeli jednak to nie był plik to sprawdzamy w warunku alternatywnym czy przypadkiem nie jest to może jakiś katalog. Jeśli nie jest to przerywamy całą pętle. Oznacza to, że nie możemy iść dalej bo nie ma ani takiego katalogu ani pliku. Jeśli natomiast jest taki katalog to znaczy, że możemy przejść do kolejnego kroku w pętli. Algorytm najpierw doda „slash” a następnie w kolejnym kroku (jeśli jakiś pozostał) doda kolejny segment i punkty 1,2,3 zostaną powtórzone.

Cały ten cykl będzie trwał do momentu aż cała tablica segmentów zostanie przejrzana lub gdy nie znaleziono ani pliku ani katalogu. OK, tak czy siak pętla się kończy a mamy jeszcze trochę algorytmu dalej:

        if (self::$controller === NULL AND $c = $controller_path.'index.php' AND is_file($c))
        {
            self::$controller = 'index';
            self::$controller_path = $c;

            $method_segment = $key + 1;
        }

Ten warunek zostanie spełniony jeśli zawiedzie poprzednia pętla bądź gdy nie została w ogóle uruchomiona. W uproszczeniu: gdy nie uda się wybrać żadnego kontrolera. Jeśli taka sytuacja wystąpi sprawdzamy czy istnieje na dysku kontroler, który nazywa się „index”. Przyjąłem w algorytmie, że wszystko co ma być domyślne (kontroler, metoda, szablon) nazywało się index. Jeśli znajdziemy taki plik to wykonuje się właściwie ten sam algorytm co w pętli tylko, że dla określonego już pliku. Zauważ, że szukamy w obecnej wartości zmiennej $controler_path. Zatem jeśli w cała pętla przeszła do końca i nadal nie znaleziono kontrolera – oznacza to, że wszystkie elementy tej ścieżki to były tylko katalogi. Zatem w naszym przykładzie wartość zmiennej $controller_path na tym etapie będzie równa:

[code]/controllers/katalog/kategoria/agd/[/code]

Zatem system będzie szukał pliku

[code]/controllers/katalog/kategoria/agd/index.php[/code]

Jest to na tyle bezpieczne rozwiązanie, że nie musimy zmuszać się do wymyślania i podawania zawsze jakiejś nazwy kontrolera bądź metody. Jeśli mają one być bardzo specyficzne to możemy im nadać nazwię „index” i nie trzeba ich będzie podawać w adresie – co przełoży się na większą czytelność.

Ostatni element to wybór metody do uruchomienia oraz pobranie atrybutów:

        if ($method_segment !== NULL AND isset(self::$segments[$method_segment]))
        {
            self::$method = self::$segments[$method_segment];

            if (isset(self::$segments[$method_segment + 1]))
            {
                self::$arguments = array_slice(self::$segments, $method_segment + 1);
            }
        }

Warunek uruchomi się gdy numer segmentu metody został ustalony i gdy znajduje się on w tablicy segmentów. Jeśli tak to zapisujemy nazwę tego segmentu jako nazwa metody (pole $method). Dodatkowo sprawdzamy czy tablica kończy się na wielkości określonej przez numer segmentu metody. Jeśli tablica jest większa tzn, że mamy tam jakieś dodatkowe argumenty. Wycinamy z tablicy segmentów fragment od numeru segmentu metody. Pozostałe elementy to tablica argumentów.

Mamy jeszcze jeden mały fragment kodu, który zostanie później rozwinięty

        if (self::$controller === NULL)
        {
            die('error 404');
        }

Tu chyba nie ma nic tajemniczego. Jeśli nasz $controller nie został znaleziony to „uśmiercamy” nasz skrypt informacją z kodem 404.

Przykładowe wywołanie kodu:

Router::init();
echo '<table>';
echo '<tr><td>URI:</td><td>'.Router::$current_uri.'</td></tr>';
echo '<tr><td>SCIEZKA:</td><td>'.Router::$controller_path.'</td></tr>';
echo '<tr><td>KONTROLER:</td><td>'.Router::$controller.'</td></tr>';
echo '<tr><td>FUNKCJA:</td><td>'.Router::$method.'</td></tr>';
echo '<tr><td>PARAMETRY:</td><td>'.implode(', ',Router::$arguments).'</td></tr>';
echo '</table>';

Podsumowanie

Myślę, że przedstawione przeze mnie rozwiązanie jest w miarę czytelne i zrozumiałe. Z moich testów wynika, że algorytm działa bardzo szybko i sprawnie jednocześnie zachowując bardzo elastyczny system organizowania sobie kontrolerów w zależności od upodobań programisty. System pozwala na właściwie nieograniczony stopień zagnieżdżeń zachowując dowolność w systemie tworzenia i organizowania plików. Być może jest jeszcze parę rzeczy, które można w tym algorytmie poprawić. Z chęcią czekam na Wasze propozycje i opinie. Na pewno wezmę je pod uwagę.

Serdecznie polecam pobawić się tym algorytmem i zobaczyć jakie to proste i użyteczne. Mam nadzieję, że to rozwiązanie Wam się podoba.

W następnym artykule spróbuję użyć Router do uruchomienia metod kontrolera

Poprzednia część: Router – cz. 2