Single point of entry 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: Dit artikel verwijst regelmatig naar twee eerder geschreven artikelen over autoloading en een standaard PHP-klasse voor een standalone script. Omdat we in dit artikel een grote stap zetten richting een eenvoudig maar redelijk compleet systeem loont het misschien de moeite om deze twee artikelen eerst door te lezen zodat je wat meer op dezelfde golflengte zit bij het lezen van dit artikel, te meer omdat het geheel meer is dan de som van de individuele delen. Misschien kan het ook geen kwaad om hier een stevige bak koffie bij te nuttigen :).

    Inleiding
    Wanneer je grotere applicaties gaat schrijven wordt het steeds belangrijker dat deze een goede architectuur hebben. Op een zeker moment is het namelijk niet langer wenselijk of verstandig om deze te laten bestaan uit een verzameling van standalone scripts. Een goede mindset die hier, en eigenlijk overal, gehanteerd kan worden is Don't Repeat Yourself (DRY). Indien je een (soortgelijke) bewerking twee of meerdere keren aan het uitvoeren bent middels code dan kan dit een indicatie zijn dat iets nog verder uitgekristalliseerd kan worden. Dit wil overigens niet zeggen dat je voor alles maar functies of klassen en methoden moet schrijven. Dit zou altijd een bewuste keuze moeten zijn.

    Stel dat je een simpele website hebt bestaande uit drie pagina's:
    1. een homepage met nieuwsitems
    2. een pagina met algemene informatie
    3. een contactformulier
    En stel nu dat je deze hebt opgezet door middel van drie standalone scripts (respectievelijk index.php, info.php en contact.php). De hoofdlayout (maintemplate) van de pagina's zal doorgaans in grote lijnen hetzelfde zijn. Voor de opbouw van de pagina's is mogelijk een connectie met een database nodig, dus deze verbinding zal ook in een of meer scripts totstand worden gebracht. Daarnaast heb je misschien nog andere hulpfuncties die je inline definieert of via require of include invoegt.

    Wanneer elk van de scripts standalone is zal de volledige opbouw van zo'n pagina besloten zijn in het script. Maar wat nu als je een wijziging in de layout wilt doorvoeren? Dit zal dan in drievoud moeten, omdat in elk van de scripts de hele layout (het hele HTML-document) is uitgeschreven. En wat nu als je een nieuwe pagina wilt aanmaken? Dit laatste zal overigens meestal ook een layoutwijziging inhouden omdat je je navigatie zult moeten aanpassen of uitbreiden. Dit komt dan waarschijnlijk neer op het knippen en plakken van een ander standalone script omdat er geen voorziening is voor hergebruik van onderdelen van een pagina.

    Het bovenstaande voorbeeld illustreert al redelijk goed hoeveel werk het kost indien er wijzigingen of toevoegingen plaatsvinden in een relatief simpele website bestaande uit enkele pagina's.

    Nu zou het al een redelijke stap voorwaarts zijn om de "herhalende delen" onder te brengen in aparte bestanden, deze dynamisch(er) te maken en vervolgens te includen of requiren in de scripts, maar eigenlijk ben je dan nog steeds hetzelfde aan het doen, zij het op een wat hoger en abstracter niveau. Elk script bevat nog steeds in grote lijnen dezelfde opbouw al doe je al minder werk dubbel.

    Voor een simpele site kan het bovenstaande flexibel genoeg zijn en hoeft dit niet verder geoptimaliseerd te worden. Echter, voor wat ingewikkeldere of uitgebreidere applicaties zal de bovenstaande aanpak mogelijk snel tekort schieten. Er wordt bijvoorbeeld niet altijd een HTML-document geserveerd, maar bijvoorbeeld een gegenereerde afbeelding, PDF-document of JSON-code. Of bepaalde pagina's hebben een afwijkende layout (ander maintemplate). Hoe dan ook, het staat niet op voorhand vast wat er gebeurt wanneer je een URL van een applicatie aanroept.

    Daarbij hoeft de URL ook niet per se een waarde te hebben die verwijst naar een concrete locatie - de URL kan "virtueel" zijn, met andere woorden, niet verwijzen naar een concrete directory of script, maar wel betekenis hebben binnen de applicatie.

    Omdat alleen de applicatie zelf echt kan oordelen of een URL zinnig of onzinnig is lijkt het ook een logische keuze om de applicatie dan ook zelf te laten bepalen wat deze met een URL doet.

    Binnen zo'n applicatie kan het nog steeds handig zijn dat je de beschikking hebt over de eerdergenoemde standalone scripts, dit wil je niet op voorhand uitsluiten. Denk bijvoorbeeld aan scripts die op gezette tijden worden uitgevoerd door crontabs en/of scripts waarmee je data kunt im- of exporteren. Laten we deze scripts "shellscripts" noemen: dit zijn scripts die in grote lijnen dezelfde functionaliteit hebben als de applicatie zelf -zij het een enigszins uitgeklede variant-, zonder dat hierbij complete webpagina's worden gegenereerd of weergegeven. We komen hier later op terug maar nemen voor nu aan dat er dus de behoefte kan zijn dat je binnen zo'n applicatie nog steeds rechtstreeks bestanden en directories aan kunt spreken die een specifieke rol vervullen.

    Het idee
    We zitten dus nu eigenlijk al in een (rare) spagaat: enerzijds willen we dat het mogelijk is dat je scripts rechtstreeks kunt blijven aanroepen en anderzijds willen we dat elke mogelijke andere ("virtuele") URL voorgelegd wordt aan de applicatie zodat die op zijn beurt kan bepalen wat hiermee moet gebeuren. De oplossing die we verzinnen moet ook transparant zijn: op elk moment (bij elke aanroep van een URL) moet het duidelijk zijn hoe de flow door onze applicatie (of standalone script) is. Een zo simpel mogelijke aanpak om dit te bereiken is de volgende:
    • indien de URL verwijst naar een bestaande directory of bestand: voer de code van de bijbehorende locatie uit, en anders
    • voor alle overige ("virtuele") aanroepen: verwijs de aanroep door naar één bestand wat het (voor)portaal van de applicatie vormt
    Meestal wordt voor dit (voor)portaal van de applicatie het bestand index.php gebruikt. Hierin worden doorgaans de volgende administratieve stappen uitgevoerd:
    • initialisatie van de applicatie, concreet houdt dit vaak de creatie + initialisatie van een initiële set objecten in
    • het afhandelen van het verzoek (request) en het genereren van een reactie (response)
    Het bestand index.php vormt hiermee de enige ingang in de applicatie (single point of entry). De bovenstaande stappen tezamen worden meestal het bootstrappen van de applicatie genoemd en index.php wordt in dat geval in de volksmond ook wel front controller genoemd.

    Voordat je objecten creëert en initialiseert start je meestal met het definiëren van een autoloader.

    Bij de initiële set objecten zou je bijvoorbeeld kunnen denken aan objecten voor het regelen van zaken omtrent:
    • configuratie (centrale plek voor het instellen of opvragen van configuratie-parameters)
    • database (een abstractielaag zodat er geen hardcoding van specifieke database-functies plaatsvindt)
    • sessies (een abstractielaag voor sessie-management, maar een schil om $_SESSION of een rechtstreeks gebruik hiervan kan natuurlijk ook)
    • users (een user object dat de "huidge gebruiker" representeert, compleet met privileges die deze gebruiker heeft, deze wordt elke page-access opnieuw opgebouwd; kort gezegd, user management met een soort van access control list)
    • routing (het berekenen van de "response" aan de hand van een "request"; hier kan ook worden gekeken of een gebruiker wel toegang heeft tot een bepaalde resource)
    • pagina-opbouw (afhandeling voor het opbouwen van het response-document, of dit nu HTML is of een document van een ander Content-Type)
    Voordelen
    Het doorzetten van alle "virtuele" aanroepen naar een centraal verdeelpunt heeft meerdere voordelen:
    • in plaats van meerdere ingangen (zoals bij standalone scripts) heeft je applicatie één voordeur; dit heeft weer tot gevolg dat de flow door je applicatie eenduidiger wordt en de algehele structuur van je applicatie redelijk eenvoudig blijft
    • je hoeft niet tig keer dezelfde dingen te doen (DRY), en als er iets moet veranderen in het bootstrap proces hoeft dat maar op één plaats
    • ontkoppeling van het rechtstreekse verband tussen een URL en code die daar aan gekoppeld is; je kunt aan de PHP-kant "berekenen" welke pagina/functionaliteit je wilt uitvoeren, ook heb je hiervoor niet per se allerlei aparte RewriteRules nodig, je kunt -theoretisch- volstaan met één enkele RewriteRule (of soortgelijke functionaliteit) die alles doorverwijst naar index.php
    Observaties
    Allereerst een kleine case-studie van een mogelijke URL. Deze ziet er bijvoorbeeld als volgt uit:

    HTML Source Code

    1. http://domain.com/basedir/path/to/page?arg1=val1&arg2=val2
    We onderscheiden de volgende onderdelen:
    • protocol (http://)
    • hostname (domain.com)
    • installatiepad (publieke "root") van de applicatie (/basedir)
    • pagina binnen de applicatie (/path/to/page)
      dit is het relatieve pad binnen je applicatie; dit pad bepaalt of en welke pagina of functionaliteit binnen je applicatie wordt geladen; dit is in feite ons einddoel voor dit artikel, we schrijven (onder andere) code in het index.php bestand die een schoongemaakte variant van dit relatieve pad berekent die vervolgens aan de applicatie wordt aangeboden om verder te verwerken
    • querystring (?arg1=val1&arg2=val2)
      deze bevat mogelijk verwijzingen naar subacties binnen het aangeroepen stuk functionaliteit (denk aan de actie methoden in het artikel over de standaard PHP class)
    Wanneer je functionaliteit gaat ontwikkelen die van deze systematiek gebruik maakt doe je er verstandig aan de waarden van bepaalde configuratie-parameters -mits van toepassing- af te laten hangen van de hostname. Zo zou je de gegevens voor de connectie met je database hier van af kunnen laten hangen, en zo ook een mogelijke subdirectory als startpunt van je applicatie maar ook andere (interne) paden, of debugging aan staat, of een specifieke site in onderhoud is en enkel bereikbaar is via bepaalde IP-adressen et cetera.

    Een simpel switch-statement inbouwen in je configuratie-functionaliteit die op grond van hostname verschillende waarden toekent aan dezelfde configuratie-parameters is een eenvoudig middel om je site makkelijk verplaatsbaar te maken en te houden. Op deze manier blijft index.php ook generiek: als het goed is hoef je bij het verplaatsen van de applicatie hier nooit iets in aan te passen maar verricht je uitsluitend configuratie in de configuratie-bestanden.

    Alle hyperlinks die deze site / applicatie bevat (denk aan navigatiemenu's en andere interne links) zouden dynamisch opgebouwd moeten worden en zouden onder geen beding hardcoded mogen zijn. Wanneer je deze hyperlinks overal dynamisch genereert blijven alle interne links kloppen na een verhuizing. Het enige wat je hoeft te doen is het aanmaken van een nieuwe case in je hostname-switch-statement van je configuratie waarin je alle site-afhankelijke parameters de juiste waarden geeft. Functionaliteit die de hyperlinks genereert pikken deze waarden vervolgens op en produceren juiste en volledige hyperlinks die altijd blijven werken. Hoe dit verder in zijn werk gaat (dit is in wezen onderdeel van routing binnen je site) zal op een later tijdstip in een ander artikel behandeld worden. Heel kort door de bocht introduceer je hiervoor een link-functie die een hyperlink genereert.

    Het bovenstaande relaas heeft één centraal aandachtspunt waar je bij de bouw van je applicatie continu op moet letten: voorkom hardcoding in code. Neem niet de simpelste route bij het schrijven van code zodat de applicatie een onverplaatsbare baksteen wordt. Ben je bewust van de consequenties van de beslissingen die je neemt.

    Implementatie
    Apache is een veelgebruikte webserver dus functionaliteit die deze webserver ons biedt (specifiek mod_rewrite) zullen we als voorbeeld gebruiken voor een simpele implementatie. Andere webservers hebben als het goed is eenzelfde soort functionaliteit waarmee je een soortgelijkt effect kunt bereiken. Wat we willen bereiken is dat de webserver alle requests naar niet-bestaande bestanden/directories doorstuurt naar index.php. Hierbij zullen we ook rekening moeten houden met het feit dat de applicatie zich mogelijk niet altijd in de document root bevindt.

    We bereiken dit door een een aantal condities en één RewriteRule in een .htaccess bestand op te nemen op in de root-directory van de applicatie, dit is ook de plek waar index.php zich bevindt:

    Shell-Script

    1. # Enable rewriting.
    2. RewriteEngine on
    3. # Optional: do not allow perusal of directories.
    4. Options -Indexes
    5. # Optional: explicitly enable per-directory rewrites in the .htaccess context.
    6. Options +FollowSymLinks
    7. # Required when not in the webroot. Always use a trailing slash.
    8. RewriteBase /
    9. # To be able to access existing directories and files (standalone scripts).
    10. RewriteCond %{REQUEST_FILENAME} !-d
    11. RewriteCond %{REQUEST_FILENAME} !-f
    12. # Redirect everything else to index.php.
    13. # Add QSA to ensure that querystring variables are registered as such.
    14. RewriteRule . index.php [L,QSA]
    Laat alles zien
    Deze code spreekt redelijk voor zich. Indien de applicatie zich niet in de document root bevindt dan zul je je RewriteBase hier op aan moeten passen. Staat je applicatie bijvoorbeeld in de subdirectory "test" (gezien vanaf je document root) dan pas je je RewriteBase aan naar /test/ (met zowel een leading alsook een trailing slash).

    Hiermee hebben we bereikt dat de webserver ons doorstuurt naar ofwel een standalone script, ofwel (ingeval we een "virtuele" URL van de applicatie aanroepen) naar index.php.

    Merk hierbij op dat index.php geen toevoegingen heeft met ?action=... of ?p=... of ?path=... of ?page=... of wat dan ook. Dit is niet nodig en werkt ook verwarrend omdat je hiermee in feite de querystring ($_GET) aan het manipuleren bent. Het werkt niet intuïtief als je in de (publieke) URL niet kunt zien wat er allemaal in de querystring zit - met de zojuist genoemde constructie reserveer je op voorhand ook een querystring variabele terwijl dat helemaal niet tot uiting komt in de URL. De querystring zou te allen tijde transparant moeten zijn.

    De volgende stap is het ontleden van de aangeroepen URL in de frontend controller van de applicatie (index.php). Dit kan in principe op meerdere manieren maar het komt allemaal eigenlijk op hetzelfde neer. Normaal gesproken vindt het berekenen van de aangeroepen pagina (het bepalen van het relatieve pad) binnen de applicatie plaats in een apart onderdeel (routing) maar om te illustreren hoe dit in zijn werk gaat doen wij dit hier rechtstreeks in het index.php bestand zelf.

    Daarbij, om het een en ander leesbaar te houden gieten we index.php nu in de vorm van een HTML document. Het genereren van output (het response-document) vindt normaal ook niet plaats in index.php zelf maar in de ondergelegen code van de applicatie. Het onderstaande voorbeeld dient enkel ter illustratie van het afhandelen van de doorverwijzing door de webserver en de manier waarop je het aangeroepen applicatie-pad vervolgens kunt bepalen maar is dus niet representatief voor hoe een front controller er normaal uitziet.

    Dat gezegd hebbende, kun je bijvoorbeeld op de volgende manier het relatieve applicatie-pad bepalen:

    PHP Source Code

    1. <?php
    2. // Define output type.
    3. header('Content-Type: text/html; charset=UTF-8');
    4. // Helper function for output escaping.
    5. function escape($in) {
    6. return htmlspecialchars($in, ENT_QUOTES, 'UTF-8');
    7. }
    8. // Application path relative to the document root, usually set through configuration.
    9. // For the purpose of this demo it will be determined in a generic way,
    10. // but normally this is an explicit hardcoded path in some configuration setting.
    11. // The same value should be present in the RewriteBase value of your .htaccess file with
    12. // an additional trailing slash.
    13. $applicationPath = dirname($_SERVER['SCRIPT_NAME']);
    14. // The cleaned path, relative to the application path.
    15. // This is the path we want to feed to the application.
    16. // The initial value could also contain some default value that makes sense to the application,
    17. // for example "home" or something.
    18. $path = '';
    19. // We try to ascertain whether the called URL is something that looks like a valid URL.
    20. // For this we use parse_url() and we deliberately suppress possible errors.
    21. $uriData = @parse_url($_SERVER['REQUEST_URI']);
    22. if ($uriData === false) {
    23. // parse_url() returned something unintelligible, do some error handling here.
    24. // For now we exit the script.
    25. die('[error] Invalid URL');
    26. } else {
    27. // $uriData['path'] now contains the full path.
    28. // We calculate the cleaned path by subtracting the application path from the full path.
    29. $path = substr($uriData['path'], strlen($applicationPath));
    30. // Finally, we strip leading and trailing slashes from this path.
    31. $path = trim($path, '/');
    32. }
    33. ?>
    34. <!DOCTYPE html>
    35. <html>
    36. <head>
    37. <title>Front controller</title>
    38. <style type="text/css">
    39. body { font-family: sans-serif; font-size: 10pt; margin: 25px; }
    40. </style>
    41. </head>
    42. <body>
    43. <h1>All the information you ever wanted, and more.</h1>
    44. <h2>1. REQUEST_URI</h2>
    45. <pre><?php echo escape($_SERVER['REQUEST_URI']) ?></pre>
    46. <h2>2. Application path</h2>
    47. <pre><?php echo escape($applicationPath) ?></pre>
    48. <h2>3. Full path (path part of parse_url())</h2>
    49. <pre><?php echo escape($uriData['path']) ?></pre>
    50. <h2>4. Cleaned path with stripped slashes (relative to the application path)</h2>
    51. <pre><?php echo escape($path) ?></pre>
    52. <h2>5. $_GET</h2>
    53. <p>Note that the .htaccess file did not modify the querystring at all.</p>
    54. <pre><?php echo escape(print_r($_GET, true)) ?></pre>
    55. </body>
    56. </html>
    Laat alles zien
    Nu zou je een willekeurige (niet-bestaande) URL moeten kunnen aanroepen (die binnen het root-pad van de applicatie ligt, uiteraard). Dit request wordt vervolgens doorgestuurd naar index.php met behoud van de informatie van de oorspronkelijke aanroep.

    Stel bijvoorbeeld dat je op je lokale webserver (localhost) het .htaccess en index.php bestand in de subdirectory /test/rewriterules hebt gezet. Je RewriteBase moet dan ingesteld staan op /test/rewriterules/. Als je vervolgens de volgende URL aanroept:

    HTML Source Code

    1. http://localhost/test/rewriterules/hallo/dit/is/een/test?a=1&b=2
    Dan zou dit request doorgestuurd moeten worden naar het index.php bestand.
    Vervolgens zouden de volgende onderdelen de volgende waarden moeten hebben:
    1. REQUEST_URI
      /test/rewriterules/hallo/dit/is/een/test?a=1&b=2
    2. Application path
      /test/rewriterules
    3. Full path
      /test/rewriterules/hallo/dit/is/een/test
    4. Cleaned path
      hallo/dit/is/een/test
    5. $_GET
      Array
      (
      [ a ] => 1
      [ b ] => 2
      )
    Vervolgens geef je het schoongemaakte pad (hallo/dit/is/een/test) door aan je applicatie als input. Deze bepaalt welke code er vervolgens uitgevoerd dient te worden. Als het pad niet bekend is binnen de applicatie zou je bijvoorbeeld een 404 pagina kunnen serveren of een andere foutcode kunnen genereren.

    Hoe de applicatie bepaalt of iets een geldige aanroep is... hangt van je applicatie af. Door middel van je autoloader zou je een rechtstreekse mapping kunnen maken van het schoongemaakte pad naar een bijbehorend pagina-type. Of je zou deze unieke "URL slug" (het schoongemaakte pad) op kunnen slaan in een database, samen met andere informatie welke code (PHP klasse) er geladen moet worden. Of je maakt een heel simpel switch-statement, waarmee je in feite de RewriteRule functionaliteit hebt overgeheveld van Apache naar PHP.

    Dit alles biedt interessante mogelijkheden omdat je het beheer van URLs nu echt de code ingetrokken hebt, je hoeft niet meer in een .htaccess bestand lopen te prutten met tig RewriteRules maar je zou een of meer backends kunnen schrijven waarmee je deze zelf interactief kunt beheren.

    Nota bene: Omdat dit alles interne redirects betreft gaat er geen POST data verloren als je een formulier middels POST verstuurt zoals bij een externe redirect wel het geval is, want dat is in feite een nieuw request. In de huidige opzet is de POST data het eerstvolgende request na verzending beschikbaar zoals gebruikelijk is.

    Nota bene: Een puntje wat we nog niet hebben aangestipt is het volgende: Uiteraard is het zaak dat op zijn minst het index.php en het .htaccess bestand (en wellicht ook publiek uitvoerbare shellscripts) ergens in de publieke webfolder staan, maar nergens is gesproken over waar alle overige code (dit zullen voornamelijk classes zijn) zou moeten staan. Daarbij moet je misschien ook rekening houden met het volgende: als je een applicatie-URL aanroept die overeenkomt met een bestaande directory-naam dan wordt, vanwege de aard van de regels in het .htaccess bestand, voorrang gegeven aan de directorynaam. Dit kan dus mogelijk voor botsingen zorgen in de naamgeving indien de code zich ook in de publieke webfolder of zelfs in de applicatiefolder bevindt. Daarom is het wellicht verstandig om de overige bestanden van je codebase buiten je publieke webfolder te houden.

    Als je enkel gebruik maakt van classes in combinatie met een autoloader zou je de directories waarin deze classes staan toe kunnen voegen aan het include path middels de functie set_include_path(). Vervolgens zou je deze paden op kunnen nemen in je configuratie, die mogelijk weer afhankelijk zijn van een hostname. Hiermee kun je je hele codebase vrij verplaatsen naar een door jouw gedefinieerde plek. Dit principe staat voor een groot deel beschreven in het autoloader artikel.

    Tot slot
    Dit verhaal heeft nog een staartje: we zijn dan weliswaar zover dat alle requests aan niet-bestaande pagina's worden doorgezet naar index.php, maar hiermee heb je nog niet de pagina met bijbehorende code berekend die uiteindelijk wordt uitgevoerd. Wanneer je niet al te strenge eisen stelt aan de vorm van de URLs dan zou je, zoals eerder aangegeven, kunnen volstaan met een 1:1 mapping van URL naar uit te voeren code via de eerdergenoemde autoloader. Hierbij zou je ook gebruik kunnen maken van een standaard PHP klasse voor de implementatie van verschillende pagina-typen.

    Maar als je wilt gaan voor een totale "ontkoppeling" waarbij je helemaal vrij bent in de naamgeving van je pagina's dan heeft dit wat meer voeten in de aarde en zijn er andere zaken waar je ook rekening mee zult moeten houden. Deze zullen gebundeld worden in een apart artikel specifiek over routing.

    Dan nog een korte notitie over shellscripts. Dit zijn in principe standalone scripts met eenzelfde, zij het enigszins vereenvoudigde, functionaliteit als de applicatie. Er valt iets voor te zeggen om hiervoor een aparte shellscript-bootstrap te maken die je in je shellscript include. Op deze manier zorg je ervoor dat de functionaliteit waarover een shellscript beschikt op een plek vastligt en op een plek aanpasbaar is (DRY).

    1,211x gelezen

Reacties 0