Własny routing PHP

W dobie user-friendly URLs często stajemy przed dylematem odpowiedniego routingu. Praktycznie każdy framework ma swój mechanizm do budowania URLi, które wskazują odpowiednie części naszego serwisu.

Po co własciwie stosuje się routing, skoro stworzone skrypty php mogą być bez problemu wywoływane przez przeglądarkę? O ile w większości przypadków jest to prawdą, ponieważ zawsze możemy odwołać się do pliku w ten sposób:

o tyle dużo przyjemniej i czytelniej wygląda url w postaci:

Dodatkowo kiedy używając MVC, gdzie wszystkie akcje są metodami określonych kontrolerów, odwołanie do pliku nie zadziałałoby bezpośrednio.

Tworząc ostatnio PHP Sandbox użyłem z początku routera oferowanego przez Fat-Free, niemniej w pewnym momencie narzut tego frameworka, mianowicie używanie ini_set() w konstruktorze, spowodował, że musiałem znaleźć alternatywę. Zależało mi tylko na routerze, bez niczego więcej, postanowiłem napisać samodzielnie od podstaw Router .

Założenia

Jak ze wszystkim co chcemy napisać, należy najpierw narysować sobie główne założenia projektu, które w moim przypadku były:

  • prosty w użyciu,
  • rozpoznający metodę zapytania (GET, POST),
  • rozpoznający czy request jest asynchroniczny – dla zapytań ajaxowych,
  • możliwość wywoływania funkcji anonimowej jako akcję lub odwołanie do określonej metody w zadanej klasie,
  • nazwane parametry w urlach, które mogą być przekazane do funkcji/metody,
  • rozpoznawanie parametrów po wyrażeniu regularnym.

I tak oto powstał PhpRouter, który jeszcze od czasu delikatnie modyfikuję, aby w 100% pokrywał się z moimi zapotrzebowaniami.

To co chciałem osiągnąć to mechanizm, w którym definiowałbym w głównym pliku aplikacji reguły routingu na zasadzie:

Najbardziej zaawansowana definicja wyglądałaby w takim razie następująco:

Czyli wywołanie przez GET lub POST, posiadająca zmienne parametry i request typu XHR.

Na bazie powyższego szybko można zbudować wyrażenie regularne parsujące routing:

Route

Obiekt ten odpowiedzialny jest za przechowywanie informacji takich jak ścieżka, regex do dopasowania URLa z requestu, typ, metodę oraz informację jaką akcję ma wykonać jeżeli request na niego wskaże – wywołanie obiektu callable, albo konkretnej metody w zadanej klasie.

W założeniach wspomniałem, że chciałbym wywoływać albo funkcję anonimową, albo wywołać jakiś obiekt i na nim konkretną metodę, zatem do rozpatrzenia są dwa przypadki:

W pierwszym oczekuję urla w postaci /page/@id , gdzie id będzie wartością numeryczną (drugi parametr definiuje regexy dla poszczególnych parametrów) – jeżeli router znajdzie dopasowanie, przekaże id jako parametr do funkcji anonimowej i wywoła ją.

Drugi przypadek, bardzo podobny do pierwszego z tą różnicą, że jako @data , spodziewamy się czegokolwiek (ciąg znaków alfanumerycznych oraz „-„) i zostanie wywołana metoda showParams()  w klasie A , jednoczesnie do konstruktora przekazane zostaną parametry z ostatniej tablicy.

W takim razie konstruktor powinien wyglądać nastepująco:

Z góry zakładam 4 parametry, z czego obowiązkowe są jedynie dwa pierwsze. Niestety PHP nie pozwala na przeciążanie konstruktora jak jest to możliwe w Javie, więc trzeba robić kilka sztuczek:

  • na pierwszym miejscu zawsze spodziewam się definicji routingu, która będzie stringiem,
  • kolejny parametr może być albo opisem nazwanych parametrów (array z RegEx’ami) albo funkcja anonimowa/string opisujący metodę do wywołania,
  • ostatni parametr to array z parametrami do konstruktora dla wywoływanej metody.

Właściwie w każdym wypadku oczekujemy, że będziemy posiadać obiekt callable lub string definiujący którą metodę wywołać, co widać w warunku w konstruktorze. Nastepnie parsuję stringa z routingiem na podstawie wcześniejszego RegEx’a i ustawiam odpowiednie propercje obiektu Route :

W tej chwili obiekt Route  posiada wszystkie niezbędne informacje o sobie, aby móc w prosty sposób porównać ją z przychodzącym requestem za pomocą propercji   pattern .

RouteRequest

Kiedy mamy już zdefiniowane routingi, należy odpowiednio odczytać przychodzący request. Do tego celu utworzyłem klasę RouteRequest , która jedynie ubiera przychodzący request w obiekt, przechowując istotne na później informacje:

Router

Okay, mamy Request i mamy kolekcję Route. Musimy jednak jakoś stwierdzić o którą akcję wołamy i do tego konieczny nam jest ostatni obiekt – Router , który przy każdym requeście będzie iterował po naszej kolekcji i wywoła akcję na odpowiednim routingu (Route) lub zwróci wyjątek, że nie znaleziono takiej ścieżki.

Dopasowanie musi być zgodne w trzech wariantach:

  • metoda zgodna z requestem (GET, POST etc.),
  • typ requestu zgodny (jeżeli ajax to zezwalamy na routing dla ajaxa),
  • żądana ścieżka musi zgadzać się ze wzorcem.

Jeżeli wszystkie powyższe kryteria zostaną spełnione parsujemy parametry i wywołujemy odpowiednią akcję. Pozostaje zatem uzupełnić obiekt Route o brakujące metody   parseParams()  – odpowiedzialną za przeparsowanie nazwanych parametrow do tablicy asocjacyjnej oraz dispatch()  – odpowiedzialnej za wywołanie wymaganej metody.

Pozostaje jeszcze tylko dodać kilka brakujących getterów oraz zbudowanie obiektu kolekcji dla Route, która implementować będzie Iterator oraz Countable, aby możliwe było iterowanie kolekcji przez Router, to już zobaczyć można w gotowym kodzie w repo PhpRoutera.

Wykorzystanie

Kompletny kod wraz z testami i przykładami użycia znajduje się na GitHubie, gdzie można zgłaszać ewentualne uwagi i sugestie. Zachęcam do forkowania.

Grafika na licencji CC0, źródło.