Autoloading in PHP

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

  • Inleiding
    Tegenwoordig is PHP code voor een groot deel of zelfs geheel object georiënteerd. Hierbij omvat elk PHP bestand één class definitie. Als je zelf object georiënteerde code schrijft is eenzelfde aanpak waarschijnlijk aan te raden.

    Maar dan komt het moment dat je deze classes gaat gebruiken en objecten wilt creëren. Je zult dan deze classes op een of andere manier moeten invoegen in je eigen code om objecten te kunnen maken. Een manier om dit te doen is via require(), of wellicht beter, om te voorkomen dat je een fout krijgt wanneer je eenzelfde class opnieuw probeert in te voegen, require_once(). Nadat je op deze manier een PHP bestand hebt ingevoegd kun je objecten maken van de class die in het ingevoegde bestand staat gedefinieerd.

    Nota bene: dit verdient waarschijnlijk nog de voorkeur boven include() en include_once() omdat de twee laatstgenoemde functies enkel een waarschuwing produceren als het bestand in kwestie niet gevonden kan worden en (nog belangrijker) de uitvoer van het script dan gewoon verder gaat. Als je script niet de noodzakelijke bouwstenen kan vinden kun je de uitvoer hiervan net zo goed stoppen (wat bij require() en require_once() gebeurt) want je kunt er bijna gif op innemen dat dit -indien je de uitvoering doorzet- niet het gewenste resultaat oplevert.

    Het dilemma
    Naarmate je meer classes gaat gebruiken wordt dit proces van requiren nogal bewerkelijk. Ook kan de volgorde van invoegen een grotere rol gaan spelen.

    Stel dat je een class B hebt die extend van class A, dan moet je zorgen dat class A eerst gedefinieerd is (omdat class B deze nodig heeft) door:
    • ofwel achtereenvolgens (eerst) class A en (dan) class B te requiren in het bestand waar je class B wilt gebruiken
    • ofwel in het bestand van class B class A te requiren (omdat class B deze toch altijd nodig heeft) en dan in het bestand waar je class B wilt gebruiken require je class B
    Zoals eerder aangegeven is dit dus nogal bewerkelijk maar daarnaast kan dit ook tot gevolg hebben dat je op voorhand alles maar invoegt wat je denkt nodig te hebben bijvoorbeeld omdat op voorhand niet precies vaststaat welke classes je allemaal nodig hebt. Hoe dan ook, elke page access waarin je dus zo'n script aanroept zou dus een heleboel resources kunnen claimen waarvan maar een (heel) klein deel echt wordt benut. Dit maakt je applicatie ook minder goed schaalbaar.

    Enter autoloading
    Vanaf PHP 5 is het concept van autoloading geïntroduceerd. Heel simpel gezegd komt dit er op neer dat je een PHP functie kunt definiëren die aangeeft waar PHP classes moet gaan zoeken als je nieuwe objecten aanmaakt van (tot dan toe onbekende) classes die nog niet (eerder) ingeladen waren. Een bijkomend voordeel van deze methode is dus ook dat uitsluitend classes die daadwerkelijk worden gebruikt (op afroep) worden ingeladen.

    Initieel werd voor autoloading __autoload() gebruikt, maar PHP.net raadt het gebruik hiervan af en deze functie wordt mogelijk op een gegeven moment deprecated. In plaats hiervan heeft spl_autoload_register() (vanaf PHP 5.1.2) nu de voorkeur. Binnen deze functie kun je een callback-functie aanwijzen of een anonieme functie definiëren (vanaf PHP 5.3.0).

    Dit ziet er dan bijvoorbeeld als volgt uit:

    PHP Source Code

    1. <?php
    2. // variant met standalone callback functie
    3. function myAutoloader($class) {
    4. require_once './classes/'.$class.'.php';
    5. }
    6. // registreer de functie als autoloader-functie
    7. spl_autoload_register('myAutoloader');
    8. // nu kun je vanuit dit script op de volgende manier de class classes/test.php laden
    9. $test = new Test(); // het creëren kan rechtstreeks - geen require meer nodig
    10. ?>
    Display All
    Of de variant met een anonieme functie (vanaf PHP 5.3.0):

    PHP Source Code

    1. <?php
    2. spl_autoload_register(function($class) {
    3. require_once './classes/'.$class.'.php';
    4. });
    5. $test = new Test();
    6. ?>
    Hiermee ben je eigenlijk nog niet veel verder geholpen tenzij je applicatie heel erg simpel is en je classes (letterlijk en/of figuurlijk) weinig diepgang hebben. Vaak hebben classes onderlinge verbanden en dit komt ook vaak tot uiting in de bestandsnamen en de algehele directorystructuur waarin deze classes staan. De bovenstaande implementaties schieten dan al snel tekort omdat ze de lading voor zulke complexe / uitgebreide structuren niet dekken.

    Valkuilen
    De verleiding is vervolgens groot om (heel veel) logica in je autoloader-functie te proppen om zodoende beslissingen te automatiseren van wat er ingeladen moet worden. Ik heb al redelijk wat voorbeelden gezien waarin programmeurs helemaal los (en ook overboord) gaan met zeer uitgebreide autoloader-functies.

    De kapitale fout die daarbij meestal wordt gemaakt is dat deze zich dan bedienen van functies die gerelateerd zijn aan het filesysteem (denk hierbij aan is_dir(), readdir(), file_exists() en dergelijke). Dit zijn relatief dure operaties waar zeer spaarzaam mee omgesprongen zou moeten worden. Deze zouden dan ook niet in een veelvuldig gebruikte functie zoals een autoloader-functie mogen voorkomen.

    Ook hier geldt de kunst van het weglaten, houd je autoloader-functie simpel en efficiënt: stop er niet een compleet beslis-algoritme in wat uiteindelijk bepaalt wat wordt ingeladen. Zorg dat je autoloader-functie geen black box is of op den duur wordt maar zorg ervoor dat het (redelijk) eenvoudig is om te herleiden hoe deze werkt.

    Een autoloader(-functie) zou ook intuïtief en flexibel in het gebruik moeten zijn. Op het moment dat je een nieuwe class definieert zou het niet nodig moeten zijn om nieuwe code aan deze functie toe te voegen. Dit zou namelijk inhouden dat deze functie toch een soort van veredeld switch-statement is.

    Standaard autoload-functie
    Indien je spl_autoload_register() zonder parameters aanroept maakt PHP gebruik van haar eigen standaard implementatie van de autoload-functie: spl_autoload(). Deze functie zoekt alle include-paden af naar een bestandsnaam die gelijk is aan de lowercase variant van de class naam (eerste parameter) en een specifieke extensie bevat (optionele tweede parameter, standaard extensies zijn .inc en .php). Voorbeeld:

    PHP Source Code

    1. <?php
    2. // gebruik standaard implementatie (spl_autoload)
    3. spl_autoload_register();
    4. // voeg pad classes toe aan include path
    5. set_include_path(getcwd().'/classes'.PATH_SEPARATOR.get_include_path());
    6. // geef weer waar gezocht wordt
    7. echo 'zoekt in: '.get_include_path().'<br />';
    8. // laad de klasse in classes/test.php of ./classes/test.inc
    9. $test = new Test();
    10. ?>
    Een interessante observatie is de volgende: de volgorde waarin je paden invoegt in je include path bepaalt in welke volgorde de paden gecontroleerd worden op het bestaan van het bestand met de overeenkomstige class. Dit biedt mogelijk ruimte voor interessante constructies zoals het "overrulen" van classes.

    Stel dat je een library met een set classes gebruikt, maar je wilt een bepaalde class toch net iets anders implementeren. Daarbij is het waarschijnlijk geen goed idee om in deze core bestanden te gaan modderen omdat je daarmee mogelijk het upgraden van deze (doorgaans extern onderhouden) library onmogelijk maakt. Wat je dus zou kunnen doen is het (eerst) definiëren van een applicatie-pad in je include path en (vervolgens) een library-pad. Vervolgens zet je de complete library in je library-pad en de classes die je anders wilt implementeren breng je, volgens eenzelfde structuur en naamgeving, onder in je applicatie-pad. Je kunt er natuurlijk ook altijd voor kiezen om classes te extenden en op die manier alle of specifieke methoden anders te implementeren.

    Rest ons nog een laatste hindernis, hoe komen we tot een efficiënte implementatie van een autoload-functie die classes uit (((sub)sub)sub) directories uit kan lezen zonder een beroep te doen op dure functies?

    (Super) simpele implementatie
    Ik zal eerlijk toegegeven, ik ben het onderstaande ergens tegengekomen in mijn zoektocht (PHP.net user comments? stackoverflow?). De credits moet ik iemand even schuldig blijven. Allereerst gewoon maar de code, daarna bespreken we de werking en mogelijke toepassingen.

    PHP Source Code

    1. <?php
    2. // zoals voorheen: definieer je applicatie-pad en library-pad
    3. // deze zou je in een configuratie-bestand kunnen definiëren
    4. // let op: geen trailing slashes
    5. // noot: gebruik DIRECTORY_SEPARATOR in plaats van slashes voor
    6. // platform onafhankelijke directory scheidingen
    7. define('APPLICATION_PATH', '/pad/naar/je/applicatie/directory');
    8. define('LIBRARY_PATH', '/pad/naar/je/library/directory');
    9. // let op: de volgorde is hier belangrijk
    10. set_include_path(APPLICATION_PATH.PATH_SEPARATOR.LIBRARY_PATH.PATH_SEPARATOR.get_include_path());
    11. // en de autoloader functie, komtie
    12. spl_autoload_register(function($class) {
    13. spl_autoload(strtr($class, '_', DIRECTORY_SEPARATOR));
    14. });
    15. ?>
    Display All
    Whoa whoa WHOA TIMEOUT! That's it? Eén regel? Ja dat is alles. Dit legt wel vast hoe de naamgeving van je classes verder geschiedt maar hier kom je toch niet omheen - er moet een zekere logica in je naamgeving zitten als je gebruik wilt maken van een autoloader dus waarom niet de simpelst denkbare vorm: een rechtstreekse "mapping" van een class-naam naar zijn directory-structuur. Hierbij worden underscores in de naam van de class in de autoloader-functie omgezet naar directory-scheidingstekens.

    Indien je een object van de klasse My_Test creëert zal dus achtereenvolgens op de volgende plaatsen worden gekeken:
    1. (eerst) /pad/naar/je/applicatie/directory/my/test.php (of .inc)
    2. (en vervolgens, als het bovenstaande niets opleverde) /pad/naar/je/library/directory/my/test.php (of .inc)
    Op deze wijze ben je verder vrij in de manier waarop je je directory-structuur vormgeeft waarin je je classes opslaat, deze is verder niet echt aan (hele) storende regels gebonden.

    Er zit verder ook geen (ingewikkelde) logica in je autoloader-functie zelf, maar zit enkel besloten in de volgorde van de directories zoals je deze toevoegt met behulp van set_include_path(). Deze directories zouden op hun beurt weer uit een configuratie-bestand kunnen komen waarbij de specifieke locaties afhankelijk zijn van een hostname zodat je code portable blijft.

    Op deze manier heb je dus een simpele, flexibele, efficiënte en locatie-onafhankelijke autoloader-functie die de noodzaak van require(_once) bijna helemaal opheft. Bijna helemaal, want je zult nog steeds je configuratie-bestand met je directory-paden ergens moeten invoegen :).

    Indien je het stukje autoloader-code invoegt in je "one point of entry" bestand in je applicatie (meestal index.php) heb je al een redelijk vliegende start bij de bouw van een eigen applicatie (of framework, of CMS).

    Tot slot
    Ondertussen is er in PHP-land en de programmeerwereld wel meer veranderd. Zo hebben namespaces hun intrede gedaan en zijn er zogenaamde PHP voorkeursstandaarden (PSR's) voor de naamgeving van classes (met specifieke functionaliteit).

    Dit artikel is enkel bedoeld om inzicht te verschaffen in het concept autoloading, er zijn vast betere of nettere implementaties denkbaar maar als je in kleine of zelfs middelgrote eigen projecten code aan het rollen bent kunnen een paar simpele regels je het programmeerwerk al een heel stuk aangenamer maken.

    965 times read

Comments 0