Autoloading simple

Avant de rentrer dans les détails violents, nous allons commencer par faire un petit rappel (ou tout du moins une introduction) sur la notion. Les développeurs qui ne sont pas vraiment familiers avec le langage ne seront pas perdus : les mécanismes mis en place ici sont tout à fait accessibles.

On donne généralement plusieurs noms à cette fonctionnalité apparue avec PHP 5 : autoloading, chargement dynamique (ou automatique) de classes, inclusion implicite, et j’en passe. En fait, tous ces noms sont corrects… en fonction de la manière dont vous l’utiliserez. Disons qu’à la base, l’autoloading est arrivé pour faciliter la vie des développeurs dans la gestion des dépendances.

Dans un projet où l’orienté objet prend une place importante, on se retrouve souvent avec de nombreuses classes, elles-mêmes généralement réparties dans de nombreux fichiers (souvent une ou deux classes par fichier). Le développeur devait alors vérifier l’existence d’une classe avant de l’utiliser : il est nécessaire de demander à PHP d’interpréter sa définition pour qu’elle “existe”. On observait alors souvent une série de directives include ou require en tête de fichier, voire include_once et require_once pour éviter une inclusion multiple, causant une erreur fatale puisque PHP interdit la redéfinition d’un symbole existant. C’était un peu abstrait, mais j’imagine que vous ne découvrez pas ces problèmes si vous prenez la peine de vous renseigner sur le chargement automatique.

La fonction magique __autoload() a donc été introduite pour supprimer ces nombreuses inclusions, dont la maintenance devient fastidieuse dès que l’organisation des fichiers de l’application change un peu. On dit que cette fonction est magique, pas seulement parce qu’elle rend un grand service, mais aussi parce que PHP va être capable de l’appeler tout seul au bon moment, sans l’aide du développeur, afin d’alléger l’écriture du code. En fait, lorsque l’interpréteur rencontrera un nom (ou symbole) de classe inconnu, il appellera implicitement cette fonction en passant ce nom en paramètre.

À titre informatif, il est impossible de lancer une exception dans la fonction __autoload() car elle est exécuté à un moment qui ne peut être défini à priori (lors de l’interprétation du fichier ou de l’exécution du code utilisant la classe, par exemple ?). Plusieurs solutions basées sur la création dynamique d’une classe à l’aide de la fonction eval() on été proposées. Je les trouve généralement inutiles et dangereuses.

On peut imaginer une implémentation sommaire sous cette forme :

  1. <?php
  2. define('CLASSES_DIR', 'myproject/classes');
  3.  
  4. function __autoload($classname)
  5. {
  6. require(CLASSES_DIR.'/class.'.$classname.'.php');
  7. }

Ouf, du code ! Ici, on fait en sorte que pour une classe MyClass, PHP charge le fichier myproject/classes/class.MyClass.php, qui devrait, le cas échéant, contenir la définition de MyClass. C’est bien, mais encore un peu light dès qu’on cherche à décomposer l’organisation des fichiers en plusieurs répertoires.

On a alors plusieurs solutions : on peut demander à PHP de parcourir l’ensemble de nos répertoires pour trouver la classe, à l’aide de différentes implémentations :

  1. <?php
  2.  
  3. define('CLASSES_DIR', 'myproject/classes');
  4.  
  5. function __autoload($classname)
  6. {
  7. $classes_dirs = array('core', 'tools', 'model', 'dblayer');
  8. foreach($classes_dir as $dir)
  9. {
  10. $classpath = CLASSES_DIR.'/'.$dir.'/class.'.$classname.'.php';
  11. if(file_exists($classpath))
  12. {
  13. require $classpath;
  14. return;
  15. }
  16. }
  17. }

On peut aussi faire encore pire : parcourir les répertoires à la recherche du bon fichier et l’inclure ensuite ! Ces méthodes, en plus d’être hasardeuses, sont très couteuses en ressources : on utilise une boucle et on réalise plusieurs opérations sur le système de fichier (qui font partie des opérations les plus lentes d’un programme, à cause notamment du temps d’accès au disque dur).

Une méthode plus élégante serait d’enregistrer dans un tableau la liste de chaque classe associé au fichier qui contient sa définition, on aurait alors, par exemple :

  1. <?php
  2. $_autoload['MyClass'] = 'core/class.MyClass.php';
  3. $_autoload['MyModelClass'] = 'model/class.MyModelClass.php';
  4. $_autoload['EmailEasing'] = 'tools/class.EmailEasing.php';

dans un fichier autoloading.php, et une implémentation de la fonction __autoload comme celle-ci :

  1. <?php
  2. define('CLASSES_DIR', 'myproject/classes');
  3.  
  4. function __autoload($classname)
  5. {
  6. static $_autoload = null;
  7. if(is_null($_autoload))
  8. require 'autoloading.php';
  9.  
  10. require CLASSES_DIR.'/'.$_autoload;
  11. }

On fait un travail plus propre : le fichier contenant la liste des dépendances est inclus une seule fois grâce au fait que la variable soit statique et on évite des recherches inutiles. D’un autre côté, on perd une partie des avantages du chargement dynamique, puisque le développeur devra tout de même maintenir le fameux fichier de dépendances. Quand il n’y en a que quelques unes, ça va, mais c’est quand il y en a beaucoup…

De nombreuses solutions plus ou moins compliquées existent : effectuer une recherche puis mettre en cache, créer un script qui générera le fichier de dépendances qu’on appellera lors de la mise à jour du code, et plus exotique encore.

Un chargement dynamique natif avec spl_autoload()

Depuis PHP 5.1.2, les développeurs nous simplifient un peu la vie et offrent une réponse toute prête au dilemme posé plus tôt. Chouette !

Si aucune fonction __autoload() n’est définie, alors PHP utilisera la function native spl_autoload() par défaut à condition qu’on lui demande :

  1. <?php

Son comportement est assez simple et se base sur une stratégie bien connue avec include et require : on cherche partout où la configuration dit de chercher, et on prend le premier trouvé. Pour ma fameuse MyClass, spl_autoload() va parcourir chaque répertoire définit dans la directive include_path à la recherche d’un fichier appelé MyClass.php ou MyClass.inc.

On peut obtenir une solution proche de celle décrite dans le deuxième exemple avec deux avantages : d’une part l’implémentation sera légèrement plus rapide et plus souple, d’autre part, elle fera appel à des fonctions natives, donc écrites en C, et donc qui seront presque à tous les coups plus rapides que notre propre version de __autoload().

Tout ceci est bien entendu paramétrable. Pour modifier la liste des répertoires parcourus, on peut modifier la valeur de la directive include_path dans le php.ini, ou utiliser le couple de fonctions set_include_path() et get_include_path(). Il est également possible d’ajouter des extensions à la recherche grâce à spl_autoload_extensions().

Pour reprendre notre second exemple, on pourra l’implémenter ainsi :

  1. <?php
  2. define('CLASSES_DIR', 'myproject/classes');
  3.  
  4.  
  5. CLASSES_DIR.'/core'.PATH_SEPARATOR.
  6. CLASSES_DIR.'/tools'.PATH_SEPARATOR.
  7. CLASSES_DIR.'/model'.PATH_SEPARATOR.
  8. CLASSES_DIR.'/dblayer'.
  9. PATH_SEPARATOR.get_include_path()
  10. );
  11.  

On place nos répertoires en premier pour qu’ils soient prioritaires, sans oublier d’ajouter les valeurs déjà définies avec get_include_path(). Par ailleurs, il est nécessaire de modifier légèrement notre convention de nommage des fichiers : class.MyClass.php devient nécessairement MyClass.class.php. Finalement, tout ça n’est pas très satisfaisant : comme on l’a évoqué tout à l’heure, les opérations sur le système de fichier ne sont pas très amies avec l’optimisation temporelle d’un script.

Avec des tests correspondant à votre situation, vous pourrez probablement choisir entre l’implémentation suivante et celle proposée dans le troisième exemple.

Concurrence d’autoloads

Sur un gros projet, vous allez parfois devoir manipuler plusieurs bibliothèques ou frameworks qui devront travailler en collaboration lors de l’exécution d’un script. Imaginez que chacune de ces bibliothèques possèdent leur propre implémentation de la fonction __autoload(). Ce cas de figure ne se présentera pas puisque PHP ne supporte pas la surcharge de fonctions, et le seul résultat qu’on pourra obtenir est une erreur fatale…

Grâce aux fonctions spl_autoload_*(), il est possible de manipuler plusieurs implémentations de chargement dynamique sans collisions. On utilise alors les méthodes spl_autoload_register() et éventuellement spl_autoload_unregister(). Ces fonctions permettent de gérer une file de fonctions exécutées les unes après les autres jusqu’à ce que le problème de dépendance soit résolu. Un petit exemple sera peut-être plus parlant :

  1. <?php
  2.  
  3. function fantastic_framework_autoload($classname)
  4. {
  5. file_exists('fantasticframework/classes/class.'.$classname.'.php') &&
  6. require 'fantasticframework/classes/class.'.$classname.'.php';
  7. }
  8.  
  9. function helperslib_autoload($classname)
  10. {
  11. file_exists('helpers/class.'.$classname.'.php') &&
  12. require 'helpers/class.'.$classname.'.php';
  13. }
  14.  
  15. function __autoload($classname)
  16. {
  17. static $_autoload = null;
  18. if(is_null($_autoload))
  19. require 'autoloading.php';
  20.  
  21. require CLASSES_DIR.'/'.$_autoload;
  22. }
  23.  
  24. spl_autoload_register('fantastic_framework_autoload');
  25. spl_autoload_register('helperslib_autoload');
  26. spl_autoload_register('__autoload');

Ici, pour chaque dépendance non satisfaite, PHP exécutera chaque fonction une à une jusqu’à ce que le problème soit résolu. Permettant ainsi de définir plusieurs stratégies différentes en fonction du contexte. L’ordre d’exécution est celui définit par l’ordre d’enregistrement des fonctions : ici fantastic_framework_autoload, helperslib_autoload et enfin __autoload.

Notez qu’il est nécessaire d’enregistrer manuellement __autoload(), spl_autoload_register remplace en effet le comportement par défaut de PHP. Il sera également nécessaire d’enregistrer manuellement la fonction spl_autoload() vue plus tôt si vous souhaitez l’utiliser.

Il est également important de noter qu’ici, il est nécessaire que les différentes fonctions utilisées ne puissent pas retourner d’erreur : le test de l’existence du fichier avant l’inclusion est obligatoire, sinon PHP affichera une erreur (causant l’arrêt du script avec require).

Synchroniser le système de fichier et le nom des classes

Une méthode qui peut être explorée consiste à utiliser le nom de la classe à charger pour deviner l’emplacement du fichier dans notre organisation. Ce cas de figure est particulièrement intéressant quand l’organisation est découpée en modules.

Par exemple, mon programme contient, entre autres, les classes suivantes : MyAppRequest, MyAppContext, MyAppConfig, ViewTemplate, ViewCache, DbConnection, et pour finir, EmailEasing.

Comme le projet a été réfléchi en amont et que ces noms correspondent à des conventions définies, tout ceci a un sens facilitant l’organisation du code :

  • le préfixe MyApp correspond aux classes faisant partie du coeur de l’application,
  • View correspond aux classes composant la partie vue (moteur de templates, etc),
  • Db aux outils permettant de manipuler la base de données,
  • et enfin le suffixe Easing signifie que la classe correspond à une bibliothèque simplifiant certaines opérations.

On peut alors facilement répartir ces classes dans des fichiers bien organisés :

  • sources/core, par exemple, MyAppRequest sera dans le fichier sources/core/class.Request.php,
  • sources/view,
  • sources/dblayer,
  • sources/libs.

On peut alors proposer une implémentation efficace, fine et protégée contre la concurrence assez facilement :

  1. <?php
  2. define('SOURCES_DIR', 'myproject/sources');
  3.  
  4. /**
  5.  * Autoload pour notre application.
  6.  * Attention, les noms de classes et de fichiers sont sensibles
  7.  * à la casse.
  8.  */
  9. myapp_autoload($classname)
  10. {
  11. $path = SOURCES_DIR.'/';
  12. $file = '';
  13. if(substr($classname, 0, 5) == 'MyApp'))
  14. {
  15. $path .= 'core/';
  16. $file = substr($classname, 5);
  17. }
  18. elseif(substr($classname, 0, 4) == 'View'))
  19. {
  20. $path .= 'view/';
  21. $file = substr($classname, 4);
  22. }
  23. elseif(substr($classname, 0, 2) == 'Db'))
  24. {
  25. $path .= 'dblayer/';
  26. $file = substr($classname, 2);
  27. }
  28. elseif(substr($classname, -6) == 'Easing'))
  29. {
  30. $path .= 'libs/';
  31. $file = substr($classname, 0, -6);
  32. }
  33. }
  34.  
  35. spl_autoload_register('myapp_autoload');
  36. if(function_exists('__autoload'))
  37. spl_autoload_register('__autoload');
  38. spl_autoload_register('spl_autoload');

PHP 5.3 et les namespaces pour simplifier tout ça

La dernière solution que je vais développer utilise une nouvelle notion introduite dans PHP 5.3. C’est une version simplifiée de la précédente, et que j’envisage d’utiliser dans la plupart des projets en PHP 5.3 sur lesquels je travaillerais.

L’utilisation des namespaces (ou espaces de noms en français) permet d’organiser logiquement les classes en modules, afin d’éviter les collisions entre plusieurs classes portant le même nom. Par analogie, il est souvent intéressant de calquer l’organisation des fichiers avec l’organisation choisie pour les espaces de nom.

Avec les espaces de nom, une classe possède un nom absolu sous la forme namespace\subnamespace\MyClass. on aurait donc les classes et namespaces suivants :

  • namespace MyApp
    • MyApp\Request
    • MyApp\Config
    • MyApp\Context
  • namespace View
    • View\Template
    • View\Cache
  • namespace Db
    • Db\Connection
  • namespace Easing
    • Easing\Email

Notre fonction de chargement automatique sera :

  1. <?php
  2. define('SOURCES_DIR', 'myproject/sources');
  3. myapp_autoload($classname)
  4. {
  5. // Au passage, pensez à échapper le caractère \ !
  6. $classname = explode('\\', $classname);
  7. // Remplacement des namespaces dont le répertoire ne concorde pas :
  8. if($classname[0] == 'MyApp')
  9. $classname[0] == 'core';
  10. elseif($classname[0] == 'Db')
  11. $classname[0] == 'dblayer';
  12. if($classname[0] == 'Easing')
  13. $classname[0] == 'libs';
  14.  
  15. $path = SOURCES_DIR.'/';
  16. for($i = 0, $j = sizeof($classname)-1; $i < $j; ++$i)
  17. $path .= strtolower($classname).'/';
  18. $path .= 'class.'.array_pop($classname).'.php';
  19.  
  20. if(file_exists($path))
  21. require $path;
  22. }
  23.  
  24. spl_autoload_register('myapp_autoload');
  25. if(function_exists('__autoload'))
  26. spl_autoload_register('__autoload');
  27. spl_autoload_register('spl_autoload');

Ici, on retiendra l’organisation des fichiers suivante : myproject/sources/core/class.Request.php, myproject/sources/view/class.Templates.php, etc. L’utilisation du test sur l’existance du fichier avec file_exists() pourrait être évitée (et faire économiser du temps) si on s’assure qu’il n’est pas nécessaire de définir d’autre méthode de chargement automatique exécutée si celle-ci échoue, ou en inversant l’ordre d’exécution des différentes fonctions pour obtenir la plus efficace possible.

En plus, il est possible que la fonction myapp_autoload() ne soit plus modifiée, car si on souhaite créer un nouveau namespace et de nouvelles classes, on obtient automatiquement une correspondance, par exemple la classe Cookies dans le namespace Tools\Http, dont le nom absolu sera Tools\Http\Cookies sera, par défaut, recherchée dans myproject/sources/tools/http/class.Cookies.php.

Conclusion

La fonctionnalité d’auto-chargement de classes de PHP permet de simplifier la gestion des dépendances de plusieurs manières. Les opérations d’inclusion implicites sont couteuses en temps et en mémoire, il est donc pertinent d’envisager un moyen d’optimiser ce mécanisme. D’une manière générale, on évitera le plus possible les tests sur l’organisation du système de fichier, qui prennent beaucoup de temps. Dans une moindre mesure, on peut préférer éviter de charger un tableau en mémoire et devoir rechercher une valeur dedans à chaque chargement, bien que l’implémentation des tableaux associatifs en PHP offre des performances très satisfaisantes.

Enfin, l’arrivée de nouvelles fonctionnalités avec PHP 5.3, dont les namespaces, peuvent nettement simplifier tout ceci en adoptant une structure intelligente.