Routing in een PHP applicatie

ICTscripters maakt gebruik van cookies. Door het gebruiken en browsen naar onze site gaat je automatisch akkoord met het gebruik van cookies. Klik hier voor meer informatie

  • Noot vooraf: eigenlijk was ik niet van plan dit artikel te publiceren (omdat deze eigenlijk nog steeds net compleet is), maar om nu de publicatiedatum continu vooruit te schuiven is ook zoiets. Daarom toch maar een verkorte versie die al met al toch nog redelijk lang is. Het is een vrij lang theoretisch verhaal wat voortborduurt op de vorige blogs. Mogelijk komt er ooit nog een vervolg in de vorm van een download of een concret(er)e implementatie.

    Noot vooraf: dit alles is bij elkaar, zoals gezegd, een (erg?) lang artikel geworden, maar ik denk dat het niet veel korter kan zonder dat bepaalde aspecten minder goed uit de verf zouden komen omdat over routing nu eenmaal veel te vertellen is, te meer omdat er op dit punt een heleboel zaken samenkomen in je applicatie.

    Inleiding
    Allereerst is het wellicht handig om een definitie te geven van wat ik in dit artikel versta onder routing. Routing in dit artikel omvat alle (basis)taken die verricht moeten worden ten aanzien van zowel de interne als de externe navigatie van de applicatie.

    Verantwoordelijkheden
    In het vorige artikel over het creëren van een single point of entry in een PHP applicatie waren we in principe al bezig met (het voorbereidende werk voor) routing: we nemen hier eigenlijk al een stuk van het werk van de webserver voor onze eigen rekening. We zeggen in feite "geef het request maar aan ons, wij zoeken het wel voor je uit". Dit in tegenstelling tot wat er dan doorgaans gebeurt: het serveren van een 404 Not Found pagina. Hiermee delegeer je in wezen de afhandeling van het request naar eigen code. Door het gebruik van een single point of entry kanaliseren we al het verkeer zodanig dat alle requests die (mogelijk) relevant zijn voor de applicatie worden afgeleverd bij de voordeur van deze applicatie (doorgaans index.php).

    Hiermee hebben we het aanroepen van de "uiteindelijk te serveren pagina" effectief uitgesteld. We zullen nu zelf een soort van handmatige rekensom moeten opstellen die bepaalt welke code ingeladen dient te worden. Het uitlezen van de URL en vervolgens het berekenen van de te serveren pagina (en het uitvoeren van de bijbehorende code) zijn dan ook twee (opeenvolgende) verantwoordelijkheden van de routing functionaliteit.

    Een implementatie van deze rekensom hoeft in principe helemaal niet ingewikkeld te zijn: dit kan neerkomen op een rechtstreekse mapping van een (relatieve) URL naar (de naam van) een PHP class, wat in beginsel al beschreven staat in het eerdergenoemde artikel. In combinatie met de gebruikmaking van autoloading wordt deze implementatie bijna triviaal.

    Omdat je zelf deze rekensom kunt definiëren biedt dit interessante mogelijkheden. Zo zou je in de rekensom het gebruik van een Access Control List kunnen opnemen die aan de hand van een User object bepaalt of iemand in eerste instantie toegang heeft tot een pagina. De beschikbare pagina's en bijbehorende navigatie zouden dus af kunnen hangen van de privileges van een gebruiker.

    Een applicatie heeft vaak ook een (interne) structuur en daarmee ook een (interne) navigatie. Het genereren van applicatie-URLs is dan ook een van de verantwoordelijkheden die de routing functionaliteit voor haar rekening zou moeten nemen.

    Merk hierbij op dat dit (het genereren van URLs) de tegenhanger is van de vorige taak (het uitlezen van URLs). Omdat deze twee zaken in dienst staan van elkaar zullen deze dus ook op elkaar afgestemd moeten zijn.

    Bij het genereren van de applicatie-URLs is het van groot belang dat dit op een generieke en uniforme manier gebeurt. Dit zou je bijvoorbeeld kunnen bereiken door een functie of methode te introduceren die een applicatie-link kan opbouwen aan de hand van een aantal (configuratie-)parameters. Denk bijvoorbeeld aan de volgende onderdelen:
    • het protocol (http, https etc.)
    • de hostname van de (virtuele) webserver; deze kun je bijvoorbeeld uit de $_SERVER superglobal vissen (gebruik SERVER_NAME in plaats van HTTP_HOST, alle HTTP_-directives worden verschaft door de client en zijn dus in principe minder betrouwbaar)
    • (optioneel) het relatieve pad vanaf de webroot naar de applicatie-directory
    • het applicatie-pad welke een specifiek stuk functionaliteit binnen de applicatie identificeert
    • (optioneel) extra parameters die via de querystring ($_GET) worden doorgegeven; in principe zouden deze parameters ongemoeid moeten blijven, $_GET is transparant, het is om meerdere redenen onverstandig om hier onder water extra key-value-paren aan toe te voegen; tenzij je echt een speciale reden voor hebt om hier van af te wijken -zoals later in dit verhaal zal blijken- zou de inhoud van de querystring in de URL hetzelfde moeten zijn als de inhoud van $_GET
    Als je dit alles vangt in een soort van linkfunctie, en je er vervolgens voor zorgt dat alle navigatielinks ook via deze functie gegenereerd worden dan voorkom je hiermee hardcoding van je hyperlinks in je applicatie en blijft deze vrij verplaatsbaar, zowel binnen de site (indien je de applicatie-directory in kunt stellen) als daarbuiten (je hostname verandert automatisch mee).

    Het voornaamste doel van de linkfunctie is het maken van een vertaling van een interne URL naar een externe URL. Bij het uitlezen van de URL en het uitrekenen van de in te laden code gebeurt het omgekeerde. Dit laatste zou je kunnen vergelijken met wat je normaal doet met RewriteRules (mapping van externe naar interne URLs).

    Het gebruik van een linkfunctie is handig wanneer je verschillende versies hebt van eenzelfde site die (vaak) op (fysiek) verschillende plaatsen worden gehost. Denk hierbij aan een ontwikkel-, test- en productie-omgeving. Het enige wat je hoeft te doen is een aantal configuratievariabelen in te stellen (die afhankelijk zijn van je host) en je hele applicatie-navigatie is hiermee aangepast.

    Doe je dit echter niet, en hardcode je je applicatie-links dan zul je een aantal truuks uit moeten halen. Om in dat geval lokaal een site te kunnen bekijken zou je er bijvoorbeeld voor kunnen kiezen om je browser "voor de gek te houden" door je hosts file aan te passen zodat je de site kunt bekijken onder dezelfde naam waarmee deze normaal online te bezichtigen is. Hier zitten wel een heleboel haken en ogen aan en dit is in de praktijk alles behalve handig. Voorkom dus dat je applicatie op deze manier wordt gereduceerd tot, en aanvoelt als een baksteen.

    Door het omschrijven van interne links naar externe URLs op één plek te regelen door gebruikmaking van een functie zorgt dit ook voor consistentie in de opbouw van deze URLs. Ook wanneer je deze opbouw wilt aanpassen hoef je dit ook maar op één plek te doen. Dit in tegenstelling tot een oplossing waarbij je hardcoded URLs gebruikt. Elke keer wanneer je in dat geval jouw site verhuist zul je je moeten bedienen van een kunstgreep, bijvoorbeeld door het uitvoeren van een of andere lijpe globale search/replace in code en/of content. Los van het feit dat dit foutgevoelig is is dit gewoon extra werk wat kan worden vermeden.

    Er bestaat volgens mij een meningsverschil over wat de beste schrijfwijze van deze interne links zou zijn: gebruik je relatieve verwijzingen (waarbij het pad naar de applicatie je uitgangspunt is) of absolute verwijzingen (volledige URLs opgebouwd uit de onderdelen in de bulleted list hierboven). Ik ben er nog steeds niet helemaal over uit maar mijn voorkeur gaat op dit moment voorlopig uit naar volledige URLs omdat deze compleet ondubbelzinnig zijn: een volledige URL is maar op één manier te interpreteren.

    Afhankelijk van de vrijheid die je wilt hebben bij het toewijzen van een (unieke) URL aan een stuk content / een webpagina zul je ergens een bepaalde hoeveelheid werk moeten verzetten, maar niet alles hoeft per definitie in de linkfunctie zelf te gebeuren.

    Stel bijvoorbeeld dat je helemaal vrij wilt zijn in de naamgeving van een pagina. Het enige wat je dan zult moeten afdwingen is dat de externe URLs (of in ieder geval de applicatie-paden) uniek zijn, je mag eenzelfde naam niet voor twee of meer verschillende pagina's gebruiken. Het is dan namelijk onduidelijk welke pagina je bedoelt als je deze aanroept (niet-deterministisch). Omgekeerd is het ook verstandig om dezelfde pagina niet via meerdere verschillende URLs bereikbaar te laten zijn (voorkom aliasing). Wat in dit geval handig kan zijn is de introductie van (nog) een hulpfunctie voor het genereren van (delen van) het applicatie-pad.

    Het relatieve pad vanaf de root van het domein (waaronder het applicatie-pad) is vaak opgebouwd uit verschillende stukken, soms ook wel slugs genoemd. Een slug is niets meer dan een partje van een URL. Een site is vaak gestructureerd opgezet en is opgebouwd uit verschillende niveaus. Zo heb je bijvoorbeeld een onderdeel "nieuws" met hieronder nieuwsartikelen, bijvoorbeeld "Nieuwe website gelanceerd!". Het kan dan handig zijn om het applicatie-pad deze structuur te laten volgen en functionaliteit te hebben die hiervoor zoekmachinevriendelijke URLs kan bouwen. De URL naar het nieuws-onderdeel blijft bijvoorbeeld simpelweg /nieuws. Voor de URL naar het nieuwsartikel zou je de titel kunnen gebruiken om een zoekmachinevriendelijk equivalent te genereren. Deze wordt dan via de hulpfunctie bijvoorbeeld omgezet naar de slug nieuwe-website-gelanceerd. Het applicatie-pad is opgebouwd uit de slug van de pagina(titel) zelf en alle slugs van de bovengelegen niveau's, gescheiden door een forward slash ( / ). Het complete applicatie-pad naar het nieuwsartikel wordt daarmee dus /nieuws/nieuwe-website-gelanceerd. Dit is vervolgens het unieke applicatie-pad waaronder deze pagina bereikbaar is. NB dit is uit oogpunt van archivering en het uniek zijn van een URL nu niet bepaald ideaal maar volstaat als voorbeeld.

    De meeste pagina's in een applicatie zullen dus "virtueel" zijn - via een intern mechanisme wordt bepaald welke code bij een aanroep hoort, maar deze aanroepen (URLs) wijzen dus zelden tot nooit rechtstreeks naar fysieke PHP-bestanden. In het artikel over de single point of entry wordt wel rekening gehouden met standalone scripts. Dit is ook zeker iets wat handig kan zijn en daarom zul je ervoor moeten waken dat er geen "botsingen" mogelijk zijn tussen applicatie-paden en bestaande PHP-scripts of directories. Wanneer je dus een applicatie aan het bouwen bent via deze methodiek valt er dus iets voor te zeggen om alle code van deze applicatie (afgezien van de losse scripts en de single-point-of-entry zelf (index.php)) buiten de publieke webdirectory te houden. Op deze manier sluit je uit dat dit soort botsingen kunnen plaatsvinden.

    Er zijn verschillende implementaties voor slugfuncties mogelijk zoals bijvoorbeeld op deze website beschreven staat. Je zou deze ook als uitgangspunt kunnen nemen en hiermee je eigen variant kunnen creëren, bijvoorbeeld als volgt:

    PHP Source Code

    1. <?php
    2. function generateSlug($title, $replaceChars=array('.', "'"), $separator='-') {
    3. // Save whatever locale you are using.
    4. $locale = setlocale(LC_CTYPE, 0);
    5. // Temporarily change the locale for "character classification and conversion".
    6. // LC_ALL seems a bit overkill.
    7. setlocale(LC_CTYPE, 'en_US.UTF8');
    8. // Convert the list of special characters to a char that will be trimmed/truncated to a single char later on.
    9. if (count($replaceChars)) {
    10. $title = str_replace($replaceChars, ' ', $title); // note that str_replace can be safely used in utf-8 (apparently)
    11. }
    12. // Convert all (other) special characters to an ASCII variant.
    13. $title = iconv('UTF-8', 'ASCII//TRANSLIT', $title);
    14. // Strip all other remaining special characters.
    15. $title = preg_replace('#[^a-z0-9/_|+ -]#i', '', $title);
    16. // We can now safely use strtolower because of the conversion done by iconv().
    17. // If we would want to make everything lowercase...
    18. $title = strtolower($title);
    19. // Do a (greedy) replacement on consecutive characters and truncate it to ONE character ($separator).
    20. $title = preg_replace('#[/_|+ -]+#', $separator, $title);
    21. // Remove any superfluous whitelisted non-alfanumeric characters from the start and end.
    22. $title = trim($title, '/_|+-'); // trim() removes whitespace chars automatically
    23. // Restore locale.
    24. setlocale(LC_CTYPE, $locale);
    25. // Finally, return the slug.
    26. return $title;
    27. }
    28. ?>
    Laat alles zien
    Maar het bovenstaande gaat over de creatie van het applicatie-pad en daarmee over de creatie van een externe URL, hoe combineer je dit met een linkfunctie (deze functie vertaalt immers een interne URL naar een externe URL)? Je wilt bij voorkeur niet letterlijk deze externe URL in de aanroep van je linkfunctie stoppen want dan heb je nog steeds de eerdergenoemde hardcoding. Op het moment dat je de titel/slug van het artikel wijzigt zou de URL dan niet meer kloppen.

    Een simpele mogelijke oplossing is als volgt: nummer je pagina's en refereer aan dit nummer (houd hiertoe een lijst bij, bijvoorbeeld in een database). Dit nummer is vervolgens weer gekoppeld aan de complete slug (het applicatie-pad) die bij de pagina hoort. Op deze manier zorg je er ook voor dat je interne links blijven kloppen: indien de URL verandert verandert de interne link automatisch mee omdat je hier indirect naar verwijst via een nummer. In wezen is dit dus een lookuptabel voor de applicatie-paden. Hier zul je natuurlijk wel het een en ander voor moeten programmeren (ook en temeer indien je volledige vrijheid in je naamgeving wilt hebben) om wijzigingen in slug-waarden door te voeren uiteraard, maar je hoeft hiervoor niets aan te passen in de verwijzingen zelf.

    Een voorbeeld: pagina nummer 12 bevat het eerdergenoemde nieuwsartikel. De relatieve URL (het applicatie-pad) is /nieuws/nieuwe-website-gelanceerd. De aanroep van je linkfunctie in je webpagina / WYSIWYG-tekst wordt dan bijvoorbeeld:

    PHP Source Code

    1. <a href="<?php echo linkFunctie(12) ?>">Nieuwe website gelanceerd!</a>
    Of, als je je bedient van een soort van UBB-syntax:

    HTML Source Code

    1. [node=12]Nieuwe website gelanceerd![/node]
    Veranderen nu een of meer delen van de slugs en je draagt er zorg voor dat de URLs van alle betrokken pagina's worden bijgewerkt dan blijven je interne links ook gewoon werken zonder dat je door je HTML moet baggeren met search/replace opdrachten.

    Toegegeven, hier is in zekere zin nog steeds sprake van hardcoding: het getal 12 identificeert het pagina-nummer. Indien je meerdere omgevingen hebt (bijvoorbeeld development en productie) en in beide omgevingen worden af en toe pagina's aangemaakt dan zullen deze nummers op een gegeven moment uit de pas gaan lopen. Dan is het zaak dat je met enige regelmaat, of wanneer dit voor problemen gaat zorgen, deze content terughaalt zodat de nummering weer synchroon loopt.

    Een bijkomend voordeel van deze minimale hardcoding is dat je aan dit pagina-nummer meer informatie kunt ophangen. Zo zou je deze kunnen uitbreiden met informatie over het paginatype (bijvoorbeeld een artikel) en hierbij paginatype-specifieke metadata kunnen opslaan, zoals bijvoorbeeld een artikel-id. Ook zou je hier iets kunnen doen met de eerdergenoemde Access Control List. Je zou bijvoorbeeld aan kunnen geven welke rechten je nodig hebt om de pagina te mogen inzien. Bij het opvragen van de pagina (het "uitvoeren van de rekensom") controleer je of de huidige gebruiker voldoende rechten heeft om een pagina in te mogen zien, en anders serveer je een andere pagina. Je zou dan bijvoorbeeld net kunnen doen alsof de afgeschermde pagina niet bestaat en een 404 pagina kunnen serveren.

    We zijn bijna rond, rest ons nog het "uitvoeren van de rekensom", oftewel, het handmatig afhandelen van een (GET) request. Als je echter terugrefereert aan het single point of entry artikel dan staat daar al code voor het uitlezen/extraheren van het applicatie-pad. Het enige wat je dan nog hoeft te doen is dit pad matchen met je lookup-tabel waarbij je dit keer de paden als uitgangspunt neemt in plaats van de pagina id's, en de relevante code (class van het paginatype) erbij trekt en deze inlaadt met de eerdergenoemde metadata (bijvoorbeeld een artikelnummer).

    De crux in dit hele systeem, de variant met volledig vrije naamgeving, is dus in feite een "dubbel gelinkte lijst" bestaande uit pagina-id's waar allerlei informatie (waaronder het applicatie-pad) aan opgehangen is. Het pagina-id wordt gebruikt om externe URLs te genereren via de linkfunctie en het applicatie-pad wordt gebruikt om, gegeven een externe aanroep, de bijbehorende pagina, of liever gezegd, het bijbehorende paginatype, terug te zoeken en deze te serveren. Indien je deze lijst in een database opslaat doe je er verstandig aan om de kolom waarin het applicatie-pad staat opgeslagen te indexeren voor snellere lookup. Hiermee dwing je ook op database-niveau af dat de URLs van je applicatie uniek moeten zijn.

    En wanneer je een rechtstreekse mapping gebruikt (hiermee heb je dus minder controle over hoe een URL er uitziet, maar dit is niet altijd even relevant) dan is het nog simpeler, mits de naamgeving van classes de naamgeving van de applicatie-paden volgt uiteraard. In dat geval kan de autoloader direct aan de slag met een (schoongemaakt) applicatie-pad.

    Observatie
    Alhoewel volledige vrijheid in naamgeving zijn voordelen heeft is er ook een keerzijde. Omdat niet op voorhand vaststaat hoe een URL er uitziet (deze volgt immers niet per definitie een vast patroon) zal voor elke pagina een mapping moeten worden gemaakt van externe URL naar (uiteindelijk) een intern paginatype. Je zou natuurlijk altijd alles kunnen indexeren maar je zou er ook voor kunnen kiezen om het een en ander verder on-the-fly uit te rekenen. Hierbij stel je dus in wezen opnieuw het berekenen van de te serveren pagina/content uit.

    Neem als voorbeeld een forum: dit zou kunnen resulteren in een heleboel URLs / pagina's: overzicht, categorie-pagina's, individuele topics et cetera. Wat je ook zou kunnen doen is het volgende. In plaats van het aanmaken van allerlei entries voor alle forum-gerelateerde pagina's zou je de navigatie binnen het forum kunnen delegeren naar het forum paginatype zelf. Dit zou je kunnen doen door alle (externe) URLs die starten met /forum door te sturen naar de "voordeur" van het forum paginatype op eenzelfde wijze als dat je alle requests aan de applicatie doorstuurt naar de voordeur van de applicatie. Hierbij maak je uiteraard nog steeds gebruik van de single point of entry index.php en creëer je niet ineens een nieuwe voordeur!

    Voor zo'n situatie is het dan (misschien :)) geoorloofd om een extra RewriteRule aan te maken. Het forum paginatype dient dan wel zelf verder zorg te dragen voor het ontleden van het forum-specifieke deel van de URL en het serveren van de juiste content maar hierbij kun je nog steeds handig gebruik maken van reeds aanwezige routing-functionaliteit. Zo'n RewriteRule ziet er dan bijvoorbeeld als volgt uit:

    Shell-Script

    1. # node aliases with alternative routing
    2. RewriteRule ^forum /index.php?node=11 [L]
    Ondanks het feit dat deze RewriteRule nog steeds gebruik maakt van de single point of entry van de applicatie (index.php) is het waarschijnlijk wel verstandig om terughoudend om te gaan met het definiëren van dit soort uitzonderingen. Het idee van een single point of entry is namelijk (mede) dat je bij voorkeur maar één bestand hebt die alles volledig, en direct, kan afhandelen en dat daarmee ook je .htaccess bestand (of wat je ook gebruikt) schoon blijft. Desalniettemin is er in deze opzet dus ruimte om af te wijken van de norm waarbij je in een paginatype custom routing kunt definiëren als je dit zou willen.

    Deze opzet vereist wel dat je routing functionaliteit kan schakelen tussen een modus waarbij deze een zoekmachinevriendelijke URL (slug) uitleest of met index.php werkt en middels een gereserveerd woord (in dit geval "node") rechtstreeks kan refereren aan een pagina-id. Deze laatste variant zou je kunnen zien als een niet-zoekmachinevriendelijk equivalent van de zoekmachinevriendelijke URL. Bij voorkeur gebruik je de niet-zoekmachinevriendelijke variant uitsluitend voor intern gebruik en ook zou je het gebruik van deze twee varianten strict gescheiden moeten houden. Zo is een aanroep van /forum?node=12 geen geldige URL. Er moet een duidelijke logische splitsing zijn tijdens het ontleden van het applicatie-pad waarbij naar een van de twee modi wordt geschakeld en er moet dan tevens een detectiemechanisme zijn voor ongeldige formaten. Voor deze flexibiliteit betaal je dus een prijs, de code voor het afhandelen van het request wordt hierdoor noodgedwongen complexer. De vraag die je jezelf moet stellen is of deze inspanning het waard is.

    Voor onderdelen met een afwijkende navigatie zou je deze navigatie dus lokaal in een paginatype kunnen vormgeven en on-the-fly kunnen afhandelen. Dit vereist wel dat je routing-functionaliteit hier in kan voorzien (kan schakelen tussen een modus die slugs uitleest en een modus die enkel refereert aan index.php).

    Het verder delegeren van routing in je applicatie zou je kunnen vergelijken met het aanmaken van een (of meer) extra controller(s). In een bredere zin zou je de index.php-modus ook kunnen zien als een niet-zoekmachinevriendelijke variant. Wanneer schone URLs niet echt een toevoeging zijn (denk bijvoorbeeld aan een administratief systeem dat verder niet publiek toegankelijk is) zou je alle navigatie ook enkel via node-id's kunnen laten verlopen.

    Een alternatieve aanpak waarmee ik heb lopen spelen is de volgende: beschouw het applicatie-pad /a/b/c/d. Je zou je routingfunctionaliteit (de "rekensom") dan zo kunnen opstellen dat deze zoekt naar de best passende "controller", waarbij je gebruik maakt van class_exists (class_exists maakt op zijn beurt ook gebruik van de autoloader, dus mogelijk zit hier ook nog wat ingebouwde optimalisatie). Hierbij begin je met het volledige pad -/a/b/c/d- en controleer je of dit pad bekend staat als een "controller" class, oftewel, of die class weet wat er uiteindelijk aan code ingeladen dient te worden. Bestaat de class niet dan sloop je één niveau af van het applicatie-pad. Vervolgens voer je dezelfde controle uit voor /a/b/c, en dit proces herhaal je net zolang totdat je klaar bent. Indien je alle mogelijkheden hebt uitgeput roep je de default "controller" aan. Als die het ook niet weet zou deze een 404 pagina moeten serveren. In deze opzet heb je dus geen enkele extra rewriterule nodig omdat het proces van het vinden van de code die weet wat er moet gebeuren volledig is geautomatiseerd. Maar ook hier betaal je dus weer een prijs (die mogelijk intern is geoptimaliseerd) door het, in het ergste geval, afstruinen van het volledige applicatie-pad bij elke page-access.

    (Tussentijdse) samenvatting
    In de routing van een applicatie komt een heleboel bij elkaar maar wanneer je de eerdergenoemde artikelen hebt doorgelezen dan zie je nu wellicht (beter) hoe je al deze puzzelstukjes op elkaar zou kunnen aansluiten. Er zijn natuurlijk een heleboel manieren om dit te doen, ook met moderne OOP-technieken. Deze opzet heeft echter als doel om alles zo simpel -en low level- mogelijk te houden.

    Het restant van dit artikel behandelt een eerste implementatie van een eenvoudige routing class, te beginnen met een abstracte klasse die de blauwdruk vormt voor specifieke(re) implementaties van applicatie-routing.

    Eerste implementatie (abstract)
    Allereerst maar de code, daarna de uitleg:

    PHP Source Code

    1. <?php
    2. abstract class Routing
    3. {
    4. protected $path; // the cleaned application path
    5. protected $found; // whether handleRequest() succeeded in locating the right pagetype class
    6. public function __construct($options=array()) {
    7. $this->path = false;
    8. $this->found = false;
    9. }
    10. // To be implemented in the routing scheme.
    11. abstract public function init();
    12. // To be implemented in the routing scheme.
    13. abstract public function handleRequest();
    14. // To be implemented in the routing scheme.
    15. abstract public function link();
    16. protected function initPath() {
    17. // Note that REQUEST_URI contains the original encoded URL.
    18. $urlComponents = @parse_url(urldecode($_SERVER['REQUEST_URI']));
    19. if ($urlComponents === false) {
    20. // Bad URL.
    21. throw new Exception('Routing: could not parse URL');
    22. }
    23. $relativePath = dirname($_SERVER['SCRIPT_NAME']);
    24. // Strip relative path off the entire path so we end up with the application path.
    25. $path = substr($urlComponents['path'], strlen($relativePath));
    26. $this->path = trim($path, '/');
    27. }
    28. public function pathFound() {
    29. return $this->found;
    30. }
    31. public function getPath() {
    32. return $this->path;
    33. }
    34. // Convenience function.
    35. public function redirect($link) {
    36. header('HTTP/1.1 303 See Other');
    37. header('Location: '.$link);
    38. exit;
    39. }
    40. }
    41. ?>
    Laat alles zien
    We beginnen met een abstracte class waarin we het fundament leggen voor de routing binnen de applicatie. Omdat het op dit moment nog niet vaststaat hoe deze functionaliteit precies gaat werken laten we de implementaties van sommige methoden nog even open. Deze methoden moeten dan dus nog wel ergens anders geïmplementeerd worden. De abstracte class vormt min of meer een boodschappenlijstje: wat hebben we allemaal (of minimaal) nodig om in alle routing-functionaliteit te kunnen voorzien.

    De introductie van twee klassevariabelen lijkt zinvol. De variabele $path bevat het schoongemaakte applicatie-pad. Dit is het deel van de URL wat relevant is voor de applicatie en daarmee voor de berekening die bepaalt welke code uitgevoerd dient te worden. De variabele $found geeft aan of de rekensom een zinnig eindantwoord heeft opgeleverd, met andere woorden, de waarde hiervan geeft aan of het aangeroepen applicatie-pad geldig was. Voor deze twee (protected) variabelen zijn ook twee (publieke) getters: respectievelijk getPath() en pathFound().

    De constructor is bewust simpel gehouden. Er is iets voor te zeggen om het aanmaken van een object van een klasse en het volledig intialiseren en gebruiksklaar maken van dat object op te splitsen. De optionele parameter $options biedt ruimte om later andere parameters door te geven, zoals bijvoorbeeld (referenties van) andere objecten (denk aan een User of Database object voor de berekening van een pagina-aanroep waarin ook authenticatie zit verwerkt).

    Beide variabelen krijgen de initiële waarde false. Het applicatie-pad is immers nog niet vastgesteld en ook kunnen we dus nog niet bepalen of dit een geldige aanroep was.

    Vervolgens is daar de abstracte methode init(). De implementatie van deze methode (in een klasse afgeleid van deze Routing klasse) zou alles moeten bevatten wat er voor zorgt om het object van deze klasse gebruiksklaar te maken.

    De abstracte methode handleRequest() is eigenlijk het belangrijkst. Hier wordt eindelijk, ten langen leste, berekend of het aangeroepen pad ($path) geldig is en wordt informatie geretourneerd over welke code er uitgevoerd dient te worden. Indien, en alleen dan, de aanroep geldig was wordt de variabele $found voorzien van de waarde true. Deze methode dient altijd een waarde te retourneren, ook wanneer de aanroep van $path ongeldig was. handleRequest() moet namelijk in alle gevallen terugkoppelen wat er dient te gebeuren. Dit kan dus ook het serveren van een foutmeldingspagina zijn (of als je dit zo implementeert het rechtstreeks throwen van een exception, maar het aanroepen van een niet-bestaande pagina is in principe niet "verkeerd", of liever gezegd, niet fataal of heel erg onverwacht). In de (concrete) implementatie waar we (niet hier, maar uiteindelijk) naartoe werken zal de naam van een paginatype worden geretourneerd.

    De link() methode dient een implementatie te bevatten van de linkfunctie die specifiek is voor die concrete implementatie van de Routing klasse. Zoals eerder aangegeven zou deze dus min of meer het spiegelbeeld moeten zijn van de uitlees-functionaliteit, immers, de uitvoer van deze functie is weer invoer voor de handleRequest() methode.

    De initPath() methode doet het truukje uit het single point of entry artikel nog eens dunnetjes over. Deze methode leest de URL uit en extraheert het applicatie-pad en slaat deze op in $path. Indien de URL niet uitgelezen kan worden wordt een exception gethrowd.

    Wanneer je overigens een linkfunctie maakt die zowel een zoekmachine-vriendelijke variant heeft alsmede een modus voor interne aanroepen dan zou je hier, of in de init() methode, controles kunnen uitvoeren om te bepalen in welke modus de URL uitgelezen moet worden en indien er een illegale combinatie van beide varianten is (denk aan het eerder genoemde voorbeeld /forum?node=12) zou je hier dan op kunnen reageren, bijvoorbeeld weer door het throwen van een exception.

    De (gemaks)methode redirect() is hier ook bijgeplakt omdat dit ook met "routing" te maken heeft en omdat deze toch ergens ondergebracht moet worden. So because why not :). Wanneer dit systeem verder uitgebouwd wordt en er dus ook meerdere objecten en classes ontstaan dan valt er iets voor te zeggen om deze methode te verplaatsen naar een Page- of Response-class ofzo.

    Nota bene: deze klasse voorziet (op dit moment theoretisch :)) in het afhandelen van alle verantwoordelijkheden die eerder opgesteld zijn.

    Tot slot (slot? :))
    Dit lijkt mij een goed breekpunt voor de blog, maar dit is natuurlijk wel een enorme cliffhanger. Er zijn nog een heleboel losse eindjes:
    • hoe ziet een eenvoudige concrete implementatie er uit?
    • hoe combineer je dit alles tot een werkend geheel?
    • hoe wordt e.e.a. verder uitgebouwd (User, Session, Database, ACL) en opgesplitst (Maintemplate, Page, PageType) in verschillende klassen?
    Om je toch een soort van voorbeeld te geven van waar we -ongeveer- naartoe willen, ik had al een keer eerder een download beschikbaar gesteld om je een indruk te geven van hoe dit er in zijn meest elementaire vorm uitziet.

    Mochten mensen in meer details geïnteresseerd zijn dan hoor ik dit graag, dit kan dan een aanleiding zijn om meer onderwerpen uit te diepen in de vorm van een (vervolg)blog.

    2,508x gelezen

Reacties 1