Routing - podejście drugie
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
Wygląda to bardzo podobnie do komponentu routingu Symfony2. Skoro Twoje własne rozwiązania przypominają tego typu biblioteki, to wypada pochwalić. :)
OdpowiedzPrzypominać musi, w końcu S2 był inspiracją :) (choć od strony kodu routing z S2 kompletnie nie przypadł mi do gustu)
OdpowiedzZa pochwałę dziękować.
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