Michał Wachowski programista php & webdeveloper

Blog

Routing - podejście drugie

Było już o routingu. Tym razem, bez chwalenia się. Konkretny kod, konkretna funkcjonalność.

Kwestią wstępu

Jakiś czas temu pisałem o routerze. Od maja minęło sporo czasu - prezentowany wcześniej mechanizm testowany i katowany był na wiele sposobów. Modyfikowany, przebudowywany, aż przybrał całkowicie nową postać. Dziś właśnie o niej będzie.

W skrócie

Zadaniem routera jest zamiana przyjaznego linka na zestaw parametrów umożliwiających poprawne obsłużenie żądania. Czyli przykładowe /foo/bar.html ma dać identyczny efekt co "normalne" index.php?category=foo&element=bar

W rzeczywistości potrzeba ciut więcej informacji. Dobrze byłoby wiedzieć jaki kontroler za to odpowiada, język oczekiwanej odpowiedzi i pełna lista parametrów przypisanych do linka.

Teoretycznie...

Więc, do obsłużenia żądania przydatne będą:

$controller = 'FooController:single'
Identyfikator kontrolera, który zostanie przez resztę skryptu wywołany, w tym przypadku wywoływany ma być $FooController->single();.
$lang = 'pl'
Identyfikator języka, dwie litery w zupełności wystarczą.
$categorySlug = 'foo'
Slug kategorii (slug - nazwa kategorii do wstawienia w link)
$categoryID = 12
Id kategorii
$elementSlug = 'bar'
Slug elementu
$elementID = '78'
Id elementu

Muszę też określić ścieżkę (czyli adres) dla jakiej definiuję parametry.

/{categorySlug}/{elementSlug}.html/foo/bar.html
Postać, przyjemna i zrozumiała dla wpisującego adres. Ma jednak poważne ograniczenie, mianowicie - jeżeli w kategorii istnieją dwa elementy o tej samej nazwie, nie będzie wiadomo na który wskazuje. Jak w przypadku prostych stron nie jest to problemem, tak w przypadku większych serwisów sytuacja się zdarza.
/{categoryID}/{elementID}.html/12/78.html
Brak konfliktów, ale wyraźnie "unfriendly link"
/{categoryID}/{categorySlug}/{elementID}/{elementSlug}.html/12/foo/78/bar.html
Brak konfliktów, jednak to wciąż nie jest "friendly".
/{categorySlug}/{elementID}/{elementSlug}.html/foo/78/bar.html
Jest dobrze.

Więc mam definicję /{categorySlug}/{elementID}/{elementSlug}.html, która w efekcie ma dawać linki w stylu /foo/78/bar.html. Można teraz wygenerować listę definicji, od razu podstawiając wartości - wielowymiarową tablicę, gdzie kluczem jest link, a wartością tablica z parametrami przypisanymi do adresu.

Brzmi fajnie, zadziała także w momencie gdy będą w użyciu różne struktury, np. /blog/2012-01-10/blabla.html czy /oferta/kategoria/123/produkt.html.

Właśnie, brzmi fajnie, jednak w praktyce będzie to oznaczało, że dla każdego elementu będzie istnieć wpis w tablicy. Mały serwis, mało wpisów - duży serwis, dużo wpisów.

Lepiej będzie gdy router będzie przechowywał definicję, by mógł pozyskać jak najwięcej wartości z linku, czyli tak:

/foo/78/bar.html/foo/{elementID}/{elementSlug}.html
Jeden wzorzec dla wszystkich elementów z kategorii foo.
/blog/2012-01-10/blabla.html/blog/{elementDate}/{elementSlug}.html
Jeden wzorzec dla całego bloga.
/oferta/kategoria/123/produkt.html/oferta/{categorySlug}/{productID}/{productSlug}.html
Jeden wzorzec dla całej oferty.

Trzy wzorce dla bliżej nieokreślonej liczby wpisów - lepiej, czyż nie?

Praktycznie...

Cały router składa się z trzech klas

Request
Reprezentuje przychodzące żądanie. Osobiście preferuję właśnie używanie tego obiektu, zamiast $_GET itp.
RouteDefinition
Definicja ścieżki - rozpoznaje czy żądanie pasuje do ścieżki i w drugą stronę - czy przesłane do routera parametry pozwalają stworzyć link na podstawie ścieżki.
Router
Właściwy mechanizm odpowiedzialny za routing. Wykorzystuje Request i RouteDefintion

Nie będę tu opisywał każdej z klas szczegółowo, zajmę się najważniejszym elementem - czyli RouteDefinition (bo wpis znowu przydługi, a kod jest dostępny na GitHub'ie).

Więc tak - przychodzi Request, ów obiekt jest przekazywany do Router::match() gdzie następuje dopasowanie.

RouteDefinition

	$Definition = new \lib\RouteDefinition($domain, $pattern, $identifier, $arguments);
domain
subdomena, można wpisać null jeśli takowej nie ma,
pattern
wzorzec ścieżki - dokładniej o co chodzi ciut niżej,
identifier
identyfikator kontrolera w postaci lang:namespace:controller:action np.: pl:app:front:Maincontroller:action. Czemu identyfikator języka w identyfikatorze kontrolera? Bo tak się szybciej potem pisze w kodzie - tylko dlatego,
arguments
tablica argumentów/parametrów przypisanych do ścieżki ale nie tych które daje się uzyskać z wzorca - czyli może to być categoryID i categorySlug ale już nie elementID i elementSlug, w zupełności starczy categoryID, argumenty podawane są w postaci klucz - nazwa argumentu, wartość - wartość argumentu.

W momencie tworzenia definicji tworzone jest wyrażenie regularne sprawdzające czy wywoływany adres pasuje do wzorca. Sam wzorzec jest zbliżony do tych w teoretycznych rozważaniach.

Wzorzec /foo/{elementId:\d}/{elementSlug:\w}.html zostanie zamieniony na /^\/foo\/(?P<elementID<[\d]+)\/(?P<elementSlug<[\w]+)\.html?$/i. Jak widać regexp nie jest zbytnio skomplikowany.

Samo sprawdzanie czy adres pasuje do ścieżki wykonywane jest metodą RouteDefinition::matchRequest(Request $Request).

Prócz powyższej metody dostępna jest też RouteDefinition::matchIdentifier(string $identifier[, array $arguments]). Używana, gdy wykonywany jest odwrotny proces - czyli budowanie przyjaznego linka.

Tutaj sprawdzanie jest trochę bardziej skomplikowane, ponieważ do stworzenia ścieżki /foo/78/bar.html prócz identyfikatora kontrolera potrzebne są też wszystkie parametry podane podczas tworzenia instancji definicji jak i te, które zawarte będą w linku. W sumie trzy - categoryID, elementID i elementSlug

Jeśli podane zostanie mniej - nie ma możliwości utworzenia przyjaznego linka na podstawie definicji, Router musi znaleźć inną. Jeśli podane zostanie więcej parametrów - to zostaną dodane w "normalnej" postaci /foo/78/bar.html?x=1&y=2

Stąd też wynika niedoskonałość. Jeżeli są dwie ścieżki np.: /{foo:\d\w}.html i /{bar:\d}.html lub /{yada:\w}.html, dopasowana będzie pierwsza na którą trafi router. Jako że ścieżki są sortowane tylko po subdomenach (by router nie sprawdzał tych niepotrzebnych), to można to w pewnym zakresie kontrolować, czyli rejestrowanie ścieżek zacząć od najprostszych a na najbardziej złożonych kończąc.

Router

Prócz operacji na RouteDefinition, Router wykonuje jeszcze kilka innych istotnych operacji, z tym że na obiekcie Request.

Na podstawie zarejestrowanych definicji, określa czy używana jest subdomena i wprowadza odpowiednią korektę w Request

Po rozpoznaniu ścieżki, Request jest uzupełniany o informacje pozyskane z adresu (parametry z $_GET) i z dopasowanej ścieżki. Dodatkowo, na podstawie identyfikatora kontrolera ze ścieżki pozyskiwana jest nazwa kontrolera wraz z przestrzenią nazw.

Jak pisałem wyżej - cały Router, dostępny jest na GitHub'ie

Komentarze

  1. Tomasz Kowalczyk Tomasz Kowalczyk

    Wygląda to bardzo podobnie do komponentu routingu Symfony2. Skoro Twoje własne rozwiązania przypominają tego typu biblioteki, to wypada pochwalić. :)

    Odpowiedz
    1. wachowski.michal@

      Przypominać musi, w końcu S2 był inspiracją :) (choć od strony kodu routing z S2 kompletnie nie przypadł mi do gustu)
      Za pochwałę dziękować.

      Odpowiedz
  2. mtulikowski mtulikowski

    Wydaje się to dość ciekawym rozwiązaniem. Rozwiązanie jest też podobne do routingu w ZF, z tym, że u Ciebie sama definicja wygląda prościej i bardziej intuicyjnie, choć w ZF też nie jest to skomplikowane.

    Odpowiedz

Dodaj komentarz