Framework idealny? Routing

Pierwszy z cyklu już bardziej konkretnych przemyśleń i wizji dotyczących poszczególnych komponentów „idealnego frameworka”. Tym razem chciałbym omówić moją wizję routingu gdyż jakby od niego wszystko się zaczyna.

Routing w praktyce

Routing – to dzięki temu rozwiązaniu gdy użytkownik wywołuje konkretne żądanie (adres i typ) router przekazuje polecenie do odpowiedniej części systemu. To on rozpoznaje i wykonuje pracę listonosza – dostaje konkretny adres i przekazuje tam dany list. We wczesnych rozwiązaniach routing istniał w bardzo uproszczonej formie. Requesty w postaci:

?action=kontakt

oznaczał tylko tyle, że jest gdzieś wielki „switch” albo zestaw „else if” w których w zależności od wartości parametru „action” wykonywany był konkretny blok kodu. Oczywiście później zostało to usprawnione o bardziej zamknięte funkcje, dodano inne parametry akcji. W zasadzie można by nazwać takie rozwiązanie bardzo prostym routingiem.

We współczesnych rozwiązaniach możemy spotkać routing w bardziej zaawansowanej formie, gdyż wiele frameworków stara się implementować wzorzec MVC – chociaż osobiście nie znam żadnego, który by w pełni ten wzorzec implementował. MVC narzuca istnienie komponentów takich jak Model, Widok i Kontroler. Za kontroler przyjęta została klasa PHP która posiada różnorakie metody (akcje). W związku z tym aby nieco ułatwić dostęp do wykonywania poszczególnych akcji utworzono podstawowy wzór:

/controller/action

gdzie controller jest nazwą klasy kontrolera a action nazwą metody, która się w tej klasie ma uruchomić. Rozwiązanie proste i przyjemne. Podobne rozwiązanie funkcjonuje np w Zend Framework albo w Ruby on Rails. To rozwiązanie jest dobre do momentu gdy chcemy jednak zrobić coś bardziej skomplikowanego, np gdy:

  • Specjalista SEO stwierdził, że adres ma jednak wyglądać w postaci /nazwa_uzytkownika albo /kontroler,akcja.html
  • Trzeba podzielić funkcjonalności na bardziej logiczne części (np moduły)
  • Konkretne adresy maja funkcjonować w ramach konkretnej domeny (subdomeny) w wielomodułowym lub wieloaplikacyjnym systemie

Jak sobie poradził z tym np Zend Framework? Ano jak to ZF, czyli „Zbuduj sobie sam”. Efekt jest taki, że robiąc bardzo prosty adres, potrzebuję kilku linijek (przykład dla pliku INI)

routes.forum_topic_add.route = "nowy-temat"
routes.forum_topic_add.type = "Zend_Controller_Router_Route"
routes.forum_topic_add.defaults.controller = topic
routes.forum_topic_add.defaults.action = new

Whaaaat? Chciałem tylko podać adres i wskazać go na konkretną akcję do wykonania. O co tu chodzi? Poza tym to jest przecież jeszcze prosty adres. Nie ma tu żadnych wyrażeń regularnych. Gdybym chciał tak zrobić musiałbym dostawić jeszcze kilka dodatkowych linijek na każdy parametr. Dodatkowo, żeby nie było za prosto musiałbym mieć router Zend_Controller_Router_Route na Zend_Controller_Router_Route_Regex – bo tamten już wyrażeń nie rozumie. Jeszcze, żeby było ciekawie poszczególne adresy można łączyć w łańcuchy a te łańcuchy w kolejne łańcuchy już z routerami i innym typie – np Static (tak jest jeszcze jeden typ). Tylko po co? Czy naprawdę programista musi mieć aż tak dokładną kontrolę nad tym?

Wydaje mi się, że programista powinien po prostu pomyśleć sobie adres i wskazać którą metodę/funkcję ma on uruchomić. Wyobrażam to sobie tak:

nowy-temat = topic.new

I co tu potrzeba więcej? Adres taki i taki, ma kierować na taką i taką funkcję. Po co mi takie dokładne pisanie gdzie jaki kontroler a jaka akcja a jaki to typ routingu. Tym powinien już się przejmować framework a nie programista.

No dobra dobra, ale przecież to jest taki prosty przykład. A co z parametrami? W zendzie można użyć kontrolera Route albo Static. Np tak:

routes.archive.route = "archive/:year/*"
routes.archive.defaults.controller = archive
routes.archive.defaults.action = show
routes.archive.defaults.year = 2000
routes.archive.reqs.year = "\d+"

Powyższe rozwiązanie mówi tyle, że: jest wzór archive/:year. Ma ono odpalić kontroler archive i metodę show. A parametr :year ma być liczbą a gdy a domyślnie ma przyjąć wartość 2000. W celach optymalizacji używa domyślnego routera, który uznaje za parametry wyrażenia po znaku „:” i separuje je po slashu. Tylko, że jest dodatkowa walidacja na to aby parametr year był liczbą. Moim zdaniem zupełnie niepotrzebna komplikacja. Przykład poniżej jest nieco inne podejście:

routes.archive.type = "Zend_Controller_Router_Route_Regex"
routes.archive.route = "archive/(\d+)"
routes.archive.defaults.controller = "archive"
routes.archive.defaults.action = "show"
routes.archive.map.1 = "year"

Wzór routingu już jest zapisany bezpośrednio w adresie ale z kolei teraz trzeba w osobnym miejscu ponazywać poszczególne parametry. Znowu jakimś nowym cudacznym sposobem, który wymaga uruchomienia jakiegoś specjalnie napisanego parsera, gdzie jedne robi tak, a inny tak. Moim zdaniem idealny framework powinien oferować kompleksowe. jednolite rozwiązanie problemu. Programiści Zend Framework chyba sami nie znają za bardzo możliwości języka w którym piszą, bo jak inaczej wytłumaczyć fakt nie skorzystania z podstawowej umiejętności wyrażeń regularnych jaką jest named subpattern. Widziałem multum przykładów z różnymi konfiguracjami routingu ale nigdy nie widziałem tego prostego zastosowania. Ze strony PHP

<?php preg_match('/(?<name>\w+):(?<digit>\d+)/', $str, $matches); ?>

dzięki temu funkcja do wyrażeń regularnych automatycznie podstawia nam klucze sparsowanych parametrów jednocześnie dbając o poprawną walidację związaną z podanym typem wyrażenia. Sam nie jestem orłem jeśli chodzi o wyrażenia regularne ale to jest na tyle proste, że każdy to zrozumie. Dzięki temu można budować dowolnie skomplikowane adresy do routera. i bardzo proste i bardzo skomplikowane zachowując całkowitą spójność w sposobie ich tworzenia.

forum/(?<title>\w+),(?<id>\d+).html = forum.show
forum/(?<title>\w+),(?<id>\d+),new-topic.html = forum.new_topic
user/(?<name>\w+)/profile.html = user.profile
kontakt.html = article.contact
show_teaser = entry.show_teaser
program,(?<channel>\d+),(?<date>\d{2,4}-\d{2}-\d{2}).html = tv.schedule

itd itd. Wystarczy tylko wskazywać te konkretne adresy na konkretne akcje. Dla mnie takie rozwiązanie jest o wiele bardziej sensowne, spójne i czytelne niż to które proponuje Zend Framework. To rozwiązanie i pomysł zaczerpnięte jest z Django – frameworka w jezyku Python.

No dobrze, ale ktoś powie: „W ten sposób wszystkie adresy z założenia przeszukuje się wyrażeniami regularnymi a w ZF można wskazać co jest proste a co nie, dzięki temu działa to szybciej.” Ja odpowiem tak: Co zadziała szybciej? Utworzenie 70 instancji klas typu routera i przetworzenie w nich adresu + sparsowanie parametrów i walidacji (każdy router jeszcze robi to inaczej) czy przeszukanie 70 wyrażeń regularnych? Poza tym funkcje do wyrażeń regularnych są dosyć dobrze zoptymalizowane i nie robią niepotrzebnej roboty z parsowaniem wyrażeń jeśli ich tam faktycznie nie ma.

Sposób realizacji routingu ma także odwzorowanie w nieco innym podejściu do wywoływania akcji ale to już w kolejnym poście traktującym o Akcji.