Standaard PHP class voor standalone script

This site uses cookies. By continuing to browse this site, you are agreeing to our Cookie Policy.

  • Noot vooraf: bij het schrijven van dit artikel wordt verondersteld dat alle webpagina's en data gebruik maken van UTF-8 als character encoding.

    Inleiding
    Je zult je vast afvragen wat je je hierbij moet voorstellen. Wanneer je op regelmatige basis PHP code schrijft ben je hier ongetwijfeld wel eens tegenaan gelopen: je hebt de wens om een stuk code te schrijven die een bepaald voorgeschreven doel heeft: een importscript, een contactformulier of een ander script wat dient als basis voor een later uit te bouwen systeem (nieuwssysteempje, blog etc.).

    Kenmerkend aan dit soort scripts is dat ze, naast dat ze op de achtergrond mogelijk veel werk verzetten, (vaak maar niet altijd) output produceren die op het scherm wordt weergegeven, zij het debug-informatie die extra inzicht geeft in de toestand van een database of een tekst met UBB-codes die wordt omgezet naar een net artikel met gelimiteerde HTML-functionaliteit.

    Hoe dan ook, de output heeft een zeker formaat of Content-Type. Hierbij is het eigenlijk altijd belangrijk dat de regels die gelden bij dat formaat worden gehonoreerd, al doe je dit mogelijk niet altijd of bewust. Zo zal de output vaak een (versimpelde) variant van HTML zijn maar lang niet altijd wordt deze output (uiteindelijk) in een kloppend formaat -in de vorm van een volledig en kloppend HTML document- gegoten. PHP zal er maling aan hebben dat HTML-snippets kop noch staart hebben, de browser rendert de dingen meestal toch wel correct en de programmeur is zich mogelijk slechts ten dele bewust van wat er allemaal misgaat of kan gaan.

    Vaak doe je op een wat abstracter niveau toch elke keer hetzelfde. Omdat je meestal binnen de HTML-context werkt en je daarmee werkt volgens het "request-response" schaakspel van HTTP ben je vaak bezig met één specifieke (sub)actie binnen het afgebakende stuk functionaliteit met het eerdergenoemde specifieke doel. Dit alles even los van een meer "event-driven" aanpak die tegenwoordig meer en meer zijn intrede heeft gemaakt door de introductie van JavaScript frameworks en het gebruik van AJAX - de rode lijn door webapplicaties is meestal nog steeds de uitvoer van één (hoofd)actie per page-access. Al deze interactieve extra's zijn leuk, maar het is nog steeds extra (franje).

    Om het eerder genoemde voorbeeld van contactformulier verder te verkennen is een mogelijke opdeling in acties de volgende:
    • weergave van het contactformulier, mogelijk met terugkoppeling naar aanleiding van eerder ingevoerde, maar onvolledige of foute, informatie
    • verwerking van het formulier; dit omvat meestal validatie van de invoer, het opslaan van gegevens in de database en/of het verzenden van een of meer e-mailberichten en een doorverwijzing (terug naar het formulier indien er fouten waren, door naar een bedankpagina na afloop van de verwerking); deze stap (verwerking) in een aparte actie onderbrengen verdient de voorkeur zodat je gebruik kunt maken van het POST REDIRECT GET design pattern
    • weergave van een bedankpagina, mogelijk met verdere instructies en doorverwijzingen naar andere pagina's
    Verdeel en heers
    Door het opdelen van deze "mini applicatie" in acties en het schetsen van hoe deze acties zich tot elkaar verhouden (onder welke omstandigheden is actie B vanuit actie A bereikbaar) is de "flow" binnen deze code ook meteen gedefineerd.

    De implementatie van dit principe (het opdelen in acties) laat echter nogal eens te wensen over... Vaak wordt gekozen voor een procedurele aanpak met een vrij complex en genest if-elseif-elseif-else statement waarin de voorwaarden worden geschetst waaronder dat stukje code ten uitvoer gebracht zou moeten worden. Hierin worden meestal ook de superglobals $_GET en $_POST (veelvuldig) gecombineerd.

    Zoals je wellicht kunt voorstellen (of uit ervaring weet :)) verzandt dit al snel in een onhanteerbare kluwen code (a.k.a. spaghetticode) die steeds moeilijker goed interpreteerbaar wordt te meer als hier over tijd ook nog allerlei wijzigingen en toevoegingen in aangebracht worden.

    Daar komt meestal nog een andere complicatie bij: het script zelf is doorgaans onderdeel van een groter geheel - deze wordt geinclude in andere code waarin de standaard layout van de website staat gedefinieerd... en vaak ook direct wordt weergegeven. Vervolgens kom je in de knoei met de "verwerk acties" die zelf geen output produceren maar altijd na afloop meteen doorverwijzen via header() redirects. Los van het produceren van output (van de layout van de site) die niet wordt gebruikt (het renderen hiervan is een verspilling van resources) wordt ook vaak output buffering als lapmiddel gebruikt om "headers already sent" meldingen af te vangen die als gevolg van deze opzet anders geproduceerd zouden worden. Deze hele aanpak borduurt dus eigenlijk continu voort op een verkeerde aanpak/insteek.

    Een procedurele aanpak is dus mogelijk in dit geval niet de handigste. Ervaring met een object georiënteerde aanpak zou vandaag de dag ook een van de wapens in je arsenaal moeten zijn. Te meer omdat dit bepaalde taken (sterk) vereenvoudigt.

    Een object georiënteerde aanpak biedt ons (waarschijnlijk) betere middelen om deze acties te compartimenteren. Zoals we later in dit artikel zullen zien komt dit min of meer neer op één-actie-per-methode in de te bouwen klasse. Het voordeel van (het gebruik van) deze methode(n) is dat elke actie echt in afzondering aanspreekbaar is en daarmee ook echt in afzondering behandeld kan worden. Het is volledig ondubbelzinnig welke methode op enig moment wordt aangeroepen en is niet meer afhankelijk van een of ander If Statement From Hell. Dit maakt je code beter leesbaar, onderhoudbaar en uitbreidbaar. Ook geeft dit volledige (of in ieder geval betere) controle over de output die geproduceerd wordt zodat output buffering geen noodzaak meer is maar een keuze.

    Eerste implementatie
    Allereerst: hoe noemen we deze klasse? De klasse vormt in wezen een sjabloon voor een webpagina met een specifiek doel, dus een voor de hand liggende naam is wellicht Pagetype.

    Het idee is dus om het principe één-actie-per-methode te vangen in een klasse. De code in deze klasse zou dus op een of andere manier moeten kunnen bepalen welke actie ten uitvoer moet worden gebracht. Een manier om dit te doen is door deze actie in de URL op te nemen, specifiek, hier een querystring-variabele voor te introduceren, bij voorkeur met een vaste naam. In deze eerste implementatie kiezen we voor de naam "action". Je zou er ook voor kunnen kiezen om deze naam dynamisch instelbaar te laten zijn, bijvoorbeeld door deze naam mee te geven als parameter aan de __construct() methode maar voor nu houden we het bij de statische naam "action".

    Dan zullen we iets moeten besluiten omtrent naamgeving. Om de "actie methoden" (methoden die uitgevoerd kunnen worden via de URL) te onderscheiden van andere methoden voorzien we de naam deze methoden van het voorvoegsel "action". Al deze actie methoden hebben de visibilty protected zodat deze niet rechtstreeks van buitenaf benaderbaar zijn.

    Omdat deze klasse een sjabloon vormt en geen directe of volledige implementatie van een paginatype omvat valt er iets voor te zeggen om hier een abstracte klasse van te maken. Hierbij stellen we de eis dat klassen die hiervan zijn afgeleid ten minste één default actie methode implementeren: actionDefault.

    Bij de creatie van een instantie van een Pagetype object (de aanroep van de __construct() methode) wordt gecontroleerd of $_GET['action'] bestaat en of de waarde van deze querystring parameter overeenkomt met een actie methode in de klasse. In deze parameter laten we trouwens het voorvoegsel "action" weg uit de waarde, want dit zou nogal dubbel op zijn omdat de naam van de parameter al "action" heet. Mocht de methode niet bestaan dan vallen we terug op de default actie methode actionDefault. De geldige (berekende) actie methode naam (wederom zonder het voorvoegsel "action") wordt opgeslagen in de klasse variabele $(this->)action. Dit omdat we in een andere methode de actie methode daadwerkelijk gaan uitvoeren. Als alternatief op het terugvallen op een default actie zou je er ook voor kunnen kiezen om een exception te throwen indien er niet direct een aanwezige actie methode wordt gevonden maar voor nu gebruiken we een wat vrijere aanpak.

    Een van de indirecte doelen van deze klasse is het (op verzoek) produceren van complete en kloppende HTML documenten. Het toevoegen van methoden die een standaard HTML document header en footer weergeven lijkt dus een logische keuze. Dit is gevangen in (respectievelijk) de methoden __header() en __footer(). Er is bewust niet ingesprongen in de HTML code van de header methode omdat dit mogelijk voor problemen zorgt (spaties of regelovergangen voor het begin van een HTML document dienen te worden vermeden).

    Dan is er nog een (enkele) publieke methode nodig om de actie methode daadwerkelijk uit te voeren. Hiervoor hebben we de publieke methode execute(). Zoals eerder aangegeven: een actie methode produceert niet per definitie output (omdat dit in sommige gevallen voor problemen zorgt). We hebben dus ook een soort van aan/uit knop nodig die bepaalt of de header + mogelijke content + footer afgedrukt moet worden. Hiervoor introduceren we de boolse klasse variabele $(this->)renderAsHTML. Als alternatief zou je deze variabele ook "showOutput" kunnen noemen, maar deze laatste naam dekt niet helemaal de lading zoals we later zullen zien.

    Tot slot kan het handig zijn om op het laatste moment nog zaken in te stellen (zoals het instellen van de waarde van showOutput), dit kun je doen met de optionele methode init(). Het is niet verplicht om deze in afgeleide klassen te implementeren.

    Zodoende komen we tot de eerste implementatie van deze abstracte klasse:

    PHP Source Code

    1. <?php
    2. abstract class Pagetype
    3. {
    4. protected $action; // contains calculated action
    5. protected $renderAsHTML; // indicates whether to render the page as HTML
    6. // Possible addition: pass parameter to indicate the name of the action variable.
    7. public function __construct() {
    8. // Note that method_exists() does not care about case.
    9. if (isset($_GET['action']) && method_exists($this, 'action'.$_GET['action'])) {
    10. $this->action = $_GET['action'];
    11. } else {
    12. // Alternative: throw exception.
    13. $this->action = 'default';
    14. }
    15. $this->renderAsHTML = true;
    16. }
    17. // Optional method, not required to implement this in derived class.
    18. protected function init() {}
    19. // To be implemented in classes derived from Pagetype.
    20. abstract protected function actionDefault();
    21. protected function __header() {
    22. // Standard HTML header.
    23. header('Content-Type: text/html; charset=UTF-8');
    24. ?><!DOCTYPE html>
    25. <html>
    26. <head>
    27. <meta charset="UTF-8">
    28. <title></title>
    29. </head>
    30. <body><?php
    31. }
    32. protected function __footer() {
    33. // Standard HTML footer.
    34. ?></body></html><?php
    35. }
    36. public function execute() {
    37. $this->init(); // last minute initialisation
    38. $actionMethod = 'action'.$this->action;
    39. if ($this->renderAsHTML) {
    40. // Display a complete HTML document (header - content - footer).
    41. $this->__header();
    42. $this->$actionMethod(); // execute the calculated action method
    43. $this->__footer();
    44. } else {
    45. // Just execute the action method.
    46. $this->$actionMethod();
    47. }
    48. }
    49. }
    50. ?>
    Display All
    Deze klasse doet van zichzelf nog niets, we zullen ook concrete implementaties moeten maken die afgeleid zijn van deze klasse. Hiertoe een super simpel voorbeeld klasse:

    PHP Source Code

    1. <?php
    2. class MyPagetype extends Pagetype
    3. {
    4. protected function init() {
    5. // Turn off rendering as HTML document in case of the following actions:
    6. if (in_array($this->action, array(
    7. 'noHTML',
    8. ))) {
    9. $this->renderAsHTML = false;
    10. }
    11. }
    12. // We are required to implement this abstract method.
    13. // Called without an action or with ?action=default
    14. protected function actionDefault() {
    15. ?><h1>Hello World</h1>
    16. <p>Hi there.</p><?php
    17. }
    18. // Called with ?action=test
    19. protected function actionTest() {
    20. ?><h1>Test</h1>
    21. <p>This is a test.</p><?php
    22. }
    23. // Called with ?action=noHTML
    24. protected function actionNoHTML() {
    25. // As nothing is rendered, be sure to set a Content-Type
    26. header('Content-Type: text/plain; charset=UTF-8');
    27. ?>this is plaintext, <b>no HTML is active</b>!<?php
    28. }
    29. }
    30. ?>
    Display All
    Vervolgens maak je een PHP bestand waarin je de bovenstaande klassen required (of je maakt gebruik van een autoloader) met de volgende code:

    PHP Source Code

    1. <?php
    2. $test = new MyPagetype();
    3. $test->execute();
    4. ?>


    Observaties
    We kijken nogmaals in detail naar de execute() methode, specifiek naar het deel waarbij we een compleet HTML document renderen:

    PHP Source Code

    1. <?php
    2. // Display a complete HTML document (header - content - footer).
    3. $this->__header();
    4. $this->$actionMethod(); // execute the calculated action method
    5. $this->__footer();
    6. ?>
    Niets mis mee, zou je zeggen, maar hierin zit een beperking. Wellicht was je het al opgevallen: de __header() methode heeft een lege title tag. Er is ook geen methode setTitle() en ook geen klasse variabele $(this->)title ofzo. In de huidige opzet is dit ook niet goed mogelijk. Immers, de titel van een pagina hangt mogelijk af van de actie methode, maar de actie methode wordt pas uitgevoerd (en weergegeven) nadat je de __header() methode hebt uitgevoerd (en weergegeven). Dit is een beetje een kip-ei situatie.

    De enige manier om ervoor te zorgen dat er in de __header() methode een titel kan worden afgedrukt is als de actie methode eerst wordt uitgevoerd. Maar mogelijk wordt dan output in de verkeerde volgorde afgedrukt. Dit laatste kunnen we makkelijk oplossen door de herintroductie van output buffering (die in dit geval wel geoorloofd is :)).

    We verrichten hiertoe de volgende wijzigingen in de Pagetype klasse:
    • de toevoeging van de klasse variabele $(this->)output die de output van een actie methode bevat
    • bij wijze van illustratie van het gebruik van dit concept de introductie van de klasse variabele $(this->)title en een protected methode setTitle() voor het instellen hiervan; als je de titel van een document buiten de Pagetype klasse om wilt regelen verander je de visibility in public
    • de toevoeging van een breder inzetbare escape-functie, in het kader van filter input, escape output
    De Pagetype klasse wordt aldus:

    PHP Source Code

    1. <?php
    2. abstract class Pagetype
    3. {
    4. protected $action; // contains calculated action
    5. protected $renderAsHTML; // indicates whether to render the page as HTML
    6. protected $title; // HTML document title
    7. protected $output; // contains action method output when page is rendered as HTML
    8. // Possible addition: pass parameter to indicate the name of the action variable.
    9. public function __construct() {
    10. // Note that method_exists() does not care about case.
    11. if (isset($_GET['action']) && method_exists($this, 'action'.$_GET['action'])) {
    12. $this->action = $_GET['action'];
    13. } else {
    14. // Alternative: throw exception.
    15. $this->action = 'default';
    16. }
    17. $this->renderAsHTML = true;
    18. $this->title = '';
    19. }
    20. // Optional method, not required to implement this in derived class.
    21. protected function init() {}
    22. // To be implemented in classes derived from Pagetype.
    23. abstract protected function actionDefault();
    24. protected function escape($in) {
    25. return htmlspecialchars($in, ENT_QUOTES, 'UTF-8');
    26. }
    27. protected function setTitle($title) {
    28. $this->title = $title;
    29. }
    30. protected function __header() {
    31. // Standard HTML header.
    32. header('Content-Type: text/html; charset=UTF-8');
    33. ?><!DOCTYPE html>
    34. <html>
    35. <head>
    36. <meta charset="UTF-8">
    37. <title><?php echo $this->escape($this->title) ?></title>
    38. </head>
    39. <body><?php
    40. }
    41. protected function __footer() {
    42. // Standard HTML footer.
    43. ?></body></html><?php
    44. }
    45. public function execute() {
    46. $this->init(); // last minute initialisation
    47. $actionMethod = 'action'.$this->action; // calculated action method
    48. if ($this->renderAsHTML) {
    49. // Execute action method first, catch output in $this->output.
    50. ob_start();
    51. $this->$actionMethod();
    52. $this->output = ob_get_clean();
    53. // Display a complete HTML document in the correct order (header - content - footer).
    54. $this->__header();
    55. echo $this->output;
    56. $this->__footer();
    57. } else {
    58. // Just execute the action method.
    59. $this->$actionMethod();
    60. }
    61. }
    62. }
    63. ?>
    Display All
    En een aangepaste variant van MyPagetype wordt bijvoorbeeld:

    PHP Source Code

    1. <?php
    2. class MyPagetype extends Pagetype
    3. {
    4. protected function init() {
    5. // Turn off rendering as HTML document in case of the following actions:
    6. if (in_array($this->action, array(
    7. 'noHTML',
    8. ))) {
    9. $this->renderAsHTML = false;
    10. }
    11. }
    12. // We are required to implement this abstract method.
    13. // Called without an action or with ?action=default
    14. protected function actionDefault() {
    15. $this->setTitle('Hello World');
    16. ?><h1>Hello World</h1>
    17. <p>Hi there.</p><?php
    18. }
    19. // Called with ?action=test
    20. protected function actionTest() {
    21. $this->setTitle('Testing');
    22. ?><h1>Test</h1>
    23. <p>This is a test.</p><?php
    24. }
    25. // Called with ?action=noHTML
    26. protected function actionNoHTML() {
    27. // As nothing is rendered, be sure to set a Content-Type
    28. header('Content-Type: text/plain; charset=UTF-8');
    29. ?>this is plaintext, <b>no HTML is active</b>!<?php
    30. }
    31. }
    32. ?>
    Display All
    Op eenzelfde wijze kun je ook functionaliteit toevoegen waarmee je in specifieke actie methoden de volgende dingen kunt doen:
    • het toevoegen van specifieke CSS of JavaScript bestanden die in de __header() worden ingeladen
    • het toevoegen van inline JavaScript snippets (denk aan jQuery) die weer via output buffering worden opgevangen en aan het einde van het document voor de </body> tag worden afgedrukt in de __footer() methode
    Overigens kun je de actie methoden die niet gerenderd worden als HTML document ook gebruiken om data van een ander Content-Type in plaats van "text/html" te serveren, bijvoorbeeld JSON. Als je in de init() methode van je Pagetype aangeeft dat de actie methode "gimmeJSON" niet als HTML gerenderd moet worden, en vervolgens de volgende actie methode toevoegt aan je Pagetype klasse dan kan je Pagetype ook JSON serveren:

    PHP Source Code

    1. <?php
    2. // Called with ?action=gimmeJSON
    3. protected function actionGimmeJSON() {
    4. header('Content-Type: application/json; charset=UTF-8');
    5. echo json_encode(array(
    6. 'one' => 1,
    7. 'two' => 2,
    8. 'three' => 3,
    9. ));
    10. }
    11. ?>
    Display All
    Door een slim gebruik van deze klasse en(waar nodig) output buffering kun je deze standaard klasse uitbouwen tot een sjabloon met een heleboel potentiële out-of-the-box functionaliteit waarmee het bouwen van pagina's (en zelfs websites) een stuk eenvoudiger wordt. Deze klasse kun je ook blijven gebruiken voor "echte" standalone scripts.

    Tot slot
    Zoals aangegeven kun je deze klasse nog verder uitbouwen maar je zou ook kunnen overwegen om deze nog verder op te splitsen in de volgende onderdelen / klassen:
    • een Page class waarin de administratieve zaken worden geregeld zoals het instellen van headers() en het uiteindelijk comprimeren van de totale output middels output buffering in combinatie met een output callback functie (denk aan ob_gzhandler)
    • een Maintemplate class waarin je de algemene layout van je website scheidt van je Pagetype specifieke layout
    Daarmaast zou je de pagina's van je website op kunnen nemen in een soort van boomstructuur. Een pagina bestaat dan uit een configureerbare eenheid waar je onder andere een maintemplate, een pagetype en andere -pagetype-specifieke- eigenschappen aan op kunt hangen. Je bent dan al een eind op weg met het schrijven van je eigen framework of CMS.

    1,237 times read

Comments 0