Autoloader PHP Universel – Jouons avec les Patterns

Nous avons vu quelques Modèles de Conception, je propose ici de les mettre en pratique sur un sujet concret, la réalisation d’un autoloader universel qui sera capable de trouver vos classes et interfaces PHP dans n’importe quelle arborescence de répertoires (que vous définirez vous même).

Nous allons mettre en oeuvre le singleton, le décorateur, l’itérateur et la chaine de responsabilité.

Vous êtes prêt ?

Qu’est-ce qu’un Autoloader ?

Une question facile pour commencer. L’autoload est un mécanisme arrivé depuis la version 5 de PHP. Il permet de coder la recherche de classes afin d’éviter au développeur de systématiquement inclure ses bibliothèques.

Concrètement, cela transforme le code suivant

require_once ('./chemin/biblio1.php');
require_once ('./chemin/biblio2.php');
require_once ('./chemin/biblio3.php');
... ... ... ...
$Biblio = new Biblio1 (Biblio2::Constante, Biblio3::Constante);

en

require_once ('chemin/Autoloader.php');
//a partir de la, on peut utiliser ce que l'on veut qui fait parti de l'autoload
$Biblio = new Biblio1 (Biblio2::Constante, Biblio3::Constante);

Exposé de l’exemple

Nous avons une arborescence de code source avec plusieurs répertoires, à savoir :

  • /biblio1/et des sous répertoires
  • /autre bibliothèque
  • ../../../biblitothèque commune

Et nous voulons que notre code ne se préoccupe pas de l’inclusion de ces bibliothèques, qu’il se contente de les utiliser.

Ce que nous allons réaliser

  • Nous allons réaliser une méthode capable de trouver depuis un fichier PHP des Classes et des Interfaces.
  • Nous allons créer un code capable de parcourir des fichiers depuis un ou plusieurs répertoires, de façon récursive ou non, en utilisant les Itérateurs
  • Nous allons créer un code capable de filtrer uniquement les fichiers « .php » de nos Itérateurs, en utilisant un Décorateur.
  • Nous allons enregistrer notre Autoloader dans la chaine de responsabilité
  • Nous allons faire en sorte que notre Autoloader soit unique, en utilisant un Singleton

Le Singleton

class DirectoriesAutoloaderException extends Exception {}
class DirectoriesAutoloader {
   //--- Singleton
   private function __construct (){}
   private static $_instance = false;
   public static function instance (){
      if(self::$_instance === false){
         self::$_instance = new DirectoriesAutoloader();
      }
      return self::$_instance;
   }
   //--- /Singleton

C’est une partie facile, nous avons rendu le constructeur privé et avons crée une méthode statique qui retournera toujours la même instance de notre objet.

Le code « classique »

Nous voulons pouvoir paramétrer notre Autoloader pour y définir les répertoires ou chercher nos classes. Nous allons ainsi développer les méthodes concernées.

   public function addDirectory ($pDirectory, $pRecursive = true){
      if (! is_readable ($pDirectory)){
         throw new DirectoriesAutoloaderException('Cannot read from ['.$pDirectory.']');
      }
      $this->_directories[$pDirectory] = $pRecursive ? true : false;
      return $this;
   }
   private $_directories = array ();

Rien d’extraordinaire ici. On enregistre la liste des répertoires dans lesquels on va vouloir chercher les classes, en indiquant si l’on souhaite parcourir ces derniers de façon récursive.

A noter que l’on a utilisé une classe d’Exception personnalisée (Donner un type à vos Exceptions est une bonne pratique car elle vous permet de segmenter la gestion de vos erreurs).

Un dernier point à noter : la méthode retourne $this. J’aime utiliser ce genre de retour pour les méthodes de configuration afin de chaîner les appels de configuration.

Avec un tel chaînage, on pourra configurer notre autoloader de la façon suivante :

$autoloader = DirectoriesAutoloader::instance ()->addDirectory ('../utils/')
                            ->addDirectory ('../framework/');
                            ->addDirectory ('../widgets/');

L’extraction des classes

   private function _extractClasses ($pFileName){
      $toReturn = array ();
      $tokens = token_get_all (file_get_contents ($pFileName, false));
      $tokens = array_filter ($tokens, 'is_array');

      $classHunt = false;
      foreach ($tokens as $token){
         if ($token[0] === T_INTERFACE || $token[0] === T_CLASS){
            $classHunt = true;
            continue;
         }
         if ($classHunt && $token[0] === T_STRING){
            $toReturn[$token[1]] = $pFileName;
            $classHunt = false;
         }
      }
      return $toReturn;
   }

Cette méthode attend en paramètre un fichier que l’on va ouvrir et passer à la méthode token_get_all. Cette méthode parse le code reçu et le décompose en primitives PHP : Cela nous évite de développer un parser approximatif de code PHP.

Nous parcourons ensuite tous les tokens à la recherche d’une déclaration de classe ou d’interface, et lorsque nous avons cette déclaration, la chasse commence, nous recherchons son nom…… et ainsi de suite.

Revenons à nos patterns, les Iterateurs

Nous avions parlé d’Itérateurs. Nous allons les utiliser pour parcourir les répertoires à la recherche de nos fichiers PHP.

Le code est ici simpliste…

   private function _includesAll (){
      foreach ($this->_directories as $directory=>$recursive){
         $directories = new AppendIterator ();

         //On ajoute tous les chemins à parcourir
         if ($recursive){
            $directories->append (new RecursiveIteratorIterator (new RecursiveDirectoryIterator ($directory)));
         }else{
            $directories->append (new DirectoryIterator ($directory));
         }
         //On va filtrer les fichiers php depuis les répertoires trouvés.
         $files = new ExtensionFilterIteratorDecorator ($directories);
         $files->setExtension ('.php');

         foreach ($files as $fileName){
            $classes = $this->_extractClasses ((string) $fileName);
            foreach ($classes as $className=>$fileName){
               $this->_classes[strtolower ($className)] = $fileName;
            }
         }
      }
   }

Ici, nous utilisons plusieurs Itérateurs :

  • AppendIterator, un Itérateur de la SPL capable de parcourir une collection d’Itérateurs
  • DirectoryIterator, un Itérateur de la SPL capable de parcourir un répertoire pour y lister ses fichiers
  • RecursiveDirectoryIterator, un Itérateur de la SPL capable de parcourir un répertoire pour y lister ses fichiers, et qui propose des méthode pour parcourir le contenu des sous répertoires
  • RecursiveIteratorIterator, un Itérateur de la SPL capable de parcourir un Itérateur récursif comme s’il était un Itérateur à plat.

Le problème, c’est que nous devons uniquement parcourir les fichiers PHP, pas de problème, utilisons un décorateur !

Le décorateur

Nous allons créer une classe pour décorer nos Itérateurs et permettre de définir une extension de fichier pour filtrer les entrées (on ne souhaite parcourir que les fichiers .php)

class ExtensionFilterIteratorDecorator extends FilterIterator {
   private $_ext;
   public function accept (){
      if (substr ($this->current (), -1 * strlen ($this->_ext)) === $this->_ext){
         return is_readable ($this->current ());
      }
      return false;
   }
   public function setExtension ($pExt){
      $this->_ext = $pExt;
   }
}

Nous avons profité pour réaliser notre décorateur d’une classe de base de la SPL, le FilterIterator qui définit la méthode « accept » indiquant si l’entrée courante est acceptée. Seule les entrées acceptées seront parcourues, les autres seront ignorées.

Méthode Autoload & Enregistrement

La méthode Autoload sera basique, elle demande le parcours des répertoires configurés, l’extraction des classes des fichiers trouvés, puis l’inclusion des classes si elles existent bien.

   public function autoload ($pClassName){
      //On regarde si on connais la classe
      if (isset ($this->_classes[$className])){
         require_once ($this->_classes[$className]);
         return true;
      }

      //On va recharger la liste des classes connues
      if ($this->_canRegenerate){
         //pour ne charger les classes qu'une seule fois par page
         $this->_canRegenerate = false;
         $this->_includesAll ();
         return $this->autoload ($pClassName);
      }
      //on a vraiment rien trouvé.
      return false;
   }
   private $_canRegenerate = true;

Il n’y a plus qu’à enregistrer notre autoloader dans la chaîne de responsabilité grâce à la SPL (encore une fois).

$autoloader = DirectoriesAutoloader::instance ()->addDirectory ('repertoire');
spl_autoload_register (array ($autoloader, 'autoload'));

C’est fini !

Et voilà, nous avons un autoloader universel capable de trouver toutes les classes d’une arborescence.

EDIT 21/01/2011: Pour aller plus loin, vous pouvez consulter la suite de cet article ou l’autoloader est en mesure de gérer les espaces de nom pour PHP 5.3

L’ajout d’un Cache pour aller plus vite

Pour éviter de parcourir et analyser tous les fichiers à chaque demande de chargement, voici le code de l’autoloader complet, avec un système de cache pour sauvegarder d’un appel sur l’autre la liste des classes connues.

class ExtensionFilterIteratorDecorator extends FilterIterator {
	private $_ext;
	public function accept (){
		if (substr ($this->current (), -1 * strlen ($this->_ext)) === $this->_ext){
			return is_readable ($this->current ());
		}
		return false;
	}
	public function setExtension ($pExt){
		$this->_ext = $pExt;
	}
}

class DirectoriesAutoloaderException extends Exception {}
class DirectoriesAutoloader {
	//--- Singleton
	private function __construct (){}
	private static $_instance = false;
	public static function instance ($pTmpPath){
		if(self::$_instance === false){
			self::$_instance = new DirectoriesAutoloader();
			self::$_instance->setCachePath ($pTmpPath);
		}
		return self::$_instance; 
	}
	//--- /Singleton
	
	//--- Cache
	private $_cachePath;
	public function setCachePath ($pTmp){
		if (!is_writable ($pTmp)){
			throw new DirectoriesAutoloaderException('Cannot write in given CachePath ['.$pTmp.']');
		}
		$this->_cachePath = $pTmp;
	}
	//--- /Cache

	//--- Autoload
	public function autoload ($pClassName){
		//On regarde si on connais la classe
		if ($this->_loadClass ($pClassName)){
			return true;
		}

		//Si on a le droit de tenter la regénération du fichier d'autoload, on retente l'histoire
		if ($this->_canRegenerate){
			$this->_canRegenerate = false;//pour éviter que l'on 
			$this->_includesAll ();
			$this->_saveInCache ();
			return $this->autoload ($pClassName);
		}
		//on a vraiment rien trouvé.
		return false;
	}
	private $_canRegenerate = true;
	//--- /Autoload

	/**
	 * Recherche de toutes les classes dans les répertoires donnés
	 */
	private function _includesAll (){
		//Inclusion de toute les classes connues
		foreach ($this->_directories as $directory=>$recursive){
			$directories = new AppendIterator ();

			//On ajoute tous les chemins à parcourir
			if ($recursive){
				$directories->append (new RecursiveIteratorIterator (new RecursiveDirectoryIterator ($directory)));	
			}else{
				$directories->append (new DirectoryIterator ($directory));
			}

			//On va filtrer les fichiers php depuis les répertoires trouvés.
			$files = new ExtensionFilterIteratorDecorator ($directories);
			$files->setExtension ('.php');

			foreach ($files as $fileName){
				$classes = $this->_extractClasses ((string) $fileName);
				foreach ($classes as $className=>$fileName){
					$this->_classes[strtolower ($className)] = $fileName;
				}
			}
		}
	}

	/**
	 * Extraction des classes & interfaces d'un fichier
	 */
	private function _extractClasses ($pFileName){
		$toReturn = array ();
		$tokens = token_get_all (file_get_contents ($pFileName, false));
		$tokens = array_filter ($tokens, 'is_array');

		$classHunt = false;
		foreach ($tokens as $token){
			if ($token[0] === T_INTERFACE || $token[0] === T_CLASS){
				$classHunt = true;
				continue;
			}

			if ($classHunt && $token[0] === T_STRING){
				$toReturn[$token[1]] = $pFileName;
				$classHunt = false;
			}
		}

		return $toReturn;
	}
	private $_classes = array ();
	private function _saveIncache (){
		$toSave = '<?php $classes = '.var_export ($this->_classes, true).'; ?>';
		file_put_contents ($this->_cachePath.'directoriesautoloader.cache.php', $toSave);
	}

	/**
	 * Tente de charger une classe
	 */
	private function _loadClass ($pClassName){
		$className = strtolower ($pClassName);
		if (count ($this->_classes) === 0){
			if (is_readable ($this->_cachePath.'directoriesautoloader.cache.php')){
				require ($this->_cachePath.'directoriesautoloader.cache.php');	
                                $this->_classes = $classes;
			}
		}
		if (isset ($this->_classes[$className])){
			require_once ($this->_classes[$className]);
			return true;
		}
		return false;
	}
	
	/**
	 * Ajoute un répertoire a la liste de ceux à autoloader 
	 */
	public function addDirectory ($pDirectory, $pRecursive = true){
		if (! is_readable ($pDirectory)){
			throw new DirectoriesAutoloaderException('Cannot read from ['.$pDirectory.']');
		}
		$this->_directories[$pDirectory] = $pRecursive ? true : false;
		return $this;		
	}
	private $_directories = array ();
}
  1. Bonsoir,

    Très bon article une fois de plus, chapeau bas !
    Qu’en est-il côté rapidité/performance ?

    Merci par avance :)

  2. Bonjour et merci.

    Niveau performances, avec la mise en place du cache, les impacts sont de mon point de vue négligeables comparé au confort récupéré.

    Je n’ai pas fait de bench précis avec l’exemple présenté ici (qui est une version simplifiée d’un des autoloader que j’utilise sur plusieurs applications).

    Toutefois, l’autoloading est un processus coûteux pour qui est à la milliseconde prêt, pour les autres, inutile de s’en passer.

  3. Bonjour,

    J’ai juste une question :

    Est-ce que tu utilises cet autoloader sur un vrai projet, ou bien est-ce que c’est juste l’implémentation d’un cas d’école ?

  4. La veille du week-end (neuvième) | LoïcG - pingback on 10 décembre 2010 at 6 h 07 min
  5. Bonjour,
    comme je le dis dans le commentaire juste au dessus, c’est une version simplifiée d’un autoloader que j’utilise en production sur plusieurs applications.

    Les différences notables sont :
    – Celui de l’exemple gère un cache global la ou celui que j’utilise en production gère un cache par module
    – Celui de l’exemple prend la première classe venue la ou celui que j’utilise cherche d’abord par contexte (le module courant)

    La version utilisée en production, pour les plus curieux :
    http://svn.copix.org/browser/trunk/utils/copix/autoload/CopixModuleClassAutoloader.class.php?rev=8356

  6. Bonjour, je viens de tester votre exemple mais il me retourne une page blanche sans erreur.
    En faites je cherche un moyen plus rapide et plus souple pour mon autoload qui se fait vieux :)

    J’ai tester l’exemple avec mise en cache.
    J’ai simplement créer un dossier, placer une classe simple avec un print et fais l’appel dans un index mais rien ne se passe ?
    L’appel se fait bien comme sa :
    abstractdb = dossier contenant une classe pour l’exemple
    $autoloader = DirectoriesAutoloader::instance ()->addDirectory (‘abstractdb’);
    spl_autoload_register (array ($autoloader, ‘autoload’));

  7. Avec ini_set (‘display_errors’, 1); en première ligne, la page est toujours blanche ? ça nous permettra de voir plus clair (par exemple si c’est la mise en cache qui échoue)

  8. Instance attend un paramètre (le chemin du cache), c’est tout simplement surement ça.

  9. J’ai créer le dossier cache et placer ‘cache’ comme paramètre sa fonctionne mais je ne vois aucun fichier dans ce dossier ? Ne doit il pas créer des fichiers pour la mise en cache ?
    Je souhaite améliorer votre autoload car le mien est très vieux et pas super flexible.

    Voir:

    class frontend_Autoloader{
    /**
    * @static
    * @var path
    * string
    */
    private static $path;
    /**
    * @static
    * @var prefix
    * string
    */
    private static $prefix;
    /**
    * Registration
    * @access public
    * @static
    * @name register
    */
    public static function register(){
    self::$prefix = substr(__CLASS__, 0, strpos(__CLASS__, ‘_’)+1);
    self::$path = dirname(dirname(realpath(__FILE__))).DIRECTORY_SEPARATOR;
    // ici est opéré la registration
    spl_autoload_register(array(__CLASS__, ‘autoload’));
    }
    /**
    * Autoload
    * @param void $class
    * @access public
    * @static
    */
    public static function autoload($class)
    {
    // vérifie que ‘backend_’ est bien le prefix demandé
    if (strpos($class, self::$prefix) === 0) {
    if(file_exists(self::$path.str_replace(‘_’, DIRECTORY_SEPARATOR, $class).’.php’)){
    include self::$path
    .str_replace(‘_’, DIRECTORY_SEPARATOR, $class)
    .’.php’;
    }
    }
    }
    /**
    * Supprime un fichier de l’autoload (contraire de register)
    * @param void $class
    * @access public
    * @static
    */
    public static function unregister($class){
    spl_autoload_unregister(array($class, ‘autoload’));
    }
    }
    Je souhaite garder cette esprit de ne charger que les classes avec un prefixe particulier comment dois je m’y prendre ?
    Comme sa je n’aurai que les classes avec comme prefixe : mage_maclassphp{}
    Merci

  10. Il y a plusieurs solutions, l’une qui consiste à ajouter des objets en mesure de filter les noms de classes à conserver, l’autre qui se contente d’ajouter « en dur » un filtre sur les noms de classe (ligne 80).

    Un bête if strpos === 0 sur le préfixe et le tour est joué.

  11. Pour la problématique du fichier non crée, il est probable que l’application ne dispose pas des droits d’écriture sur ce dernier. Pour vérifier cela, vous pouvez modifier l’autoloader pour ajouter une exception lorsque file_put_contents retourne false (ligne 112).

  12. Bonjour,

    Ce chargeur ne fonctionnera pas « à priori » avec les espaces de noms. autoload recevra le nom de la classe complet de la classe par exemple, my\name\space\class ce qui ne pose pas de problème outre mesure dans ce code. Toutefois, la méthode _loadClasses ne s’occupe que des déclarations brutes de classes quelque soit l’espace de nom dans lequel elles sont faites, autrement dit elle ne recherche que le T_STRING qui suit T_CLASS. Pour deux classes de même nom mais de deux espaces de nom différents, le chargeur aura un comportement aléatoire qui aboutira à une erreur : La classe n’existe pas. Toutefois, plusieurs fonctions peuvent être déclarées comme autoload, si l’un échoue, la suivante est appelée. Si toutes échouent, cela provoque une erreur, si une réussi mais que la classe demandée n’existe pas, cela provoque aussi une erreur. Dans la mesure du possible, l’autoloader qui est ajouté ne devrait se soucier que de ce qui le concerne. Ainsi, ce chargeur ne se préoccupant que des dossiers qui lui ont été indiqués, le problème sera contourné tant que les classes attendus de ces dossiers sont définies dans l’espace de nom global. C’est pour cela que je débutais par ‘Ce chargeur ne fonctionnera pas « à priori » ‘

  13. Tout à fait d’accord Gabriel, comment améliorer l’autoload pour qu’il fonctionne convenablement avec les espaces de nom sachant que rester compatible PHP5.2 n’est pas un luxe.
    Remplacer la méthode _loadClasses brute par une version plus flexible.Sachant que dans mon cas comme cité plus haut je ne souhaite pas charger toutes les classes mais seulement celle avec un préfixe défini pour plus de faciliter et ne pas ajouter mes classes « outils » dans l’autoload (le strpos dans le filtre est une bonne idée).

  14. Excellente remarque Gabriel !

    L’autoloader présenté ne propose pas la gestion des espaces de nom (namespace) apparus en PHP 5.3+

    Voilà qui va me permettre d’enchaîner sur la présentation d’autres patterns pour étoffer l’exemple :-)

    Stay tuned !

  15. Il ne faut pas grand chose pour ajouter le support des namespaces dans ce chargeur. Il « faut juste » savoir quel est le dernier espace de nom déclaré dans un fichier lorsque je rencontre un T_CLASS. Si aucun espace de non n’est mentionné, la classe est dans l’espace globale, sinon dans l’espace de nom déclaré. Un espace de nom sera toujours déclaré avant une classe, ce qui nous facilite la tâche.
    D’ailleurs, j’ai peut être ton prochain. Le design pattern Template.
    Pour améliorer ce chargeur, l’idée est de créer un TemplateTokenizer qui se charge de fournir une API pour parcourir une collection de tokens. Les classes filles se chargent elles de fournir la collection de tokens à parcourir. De là, on peut réaliser quelque chose qui ressemble à une ReflectionFile qui aura la charge, à l’aide d’un PHPTokenizer (qui hérite de TemplateTokenizer) de trouver toutes les classes déclarées dans un fichier (sans l’inclure évidement). Le chargeur utilise ReflectionFile pour chaque fichier qu’il rencontre.

  16. La Stratégie (Strategy) en PHP | Gerald's Blog - pingback on 29 décembre 2010 at 14 h 35 min
  17. J’ai choisi d’utiliser le modèle Stratégie pour illustrer l’exemple, même si dans le billet en question je donne l’option Template.

    Si je suis courageux d’ici ce soir, je ferais l’adaptation de l’exemple avec le Template.

  18. Voila pour le prefix mais c’est en dur, pas très flexible à moins de le déclarer dynamiquement via une autre fonction.
    foreach ($files as $fileName){
    $classes = $this->_extractClasses ((string) $fileName);
    foreach ($classes as $className=>$fileName){
    if(strpos($className,'factory') === 0){
    $this->_classes[strtolower ($className)] = $fileName;
    }
    }
    }

  19. Pour les gens pressés, il existe un Zend_Autoloader qui marche très bien en standalone (comme toute les modules zend).

  20. @Vincent : Je rajoute le lien : http://framework.zend.com/manual/fr/zend.loader.autoloader.html (je suppose que c’est de celui ci dont tu parlais)

  21. Effectivement l’autoloader de zend marche très bien mais quand est il des personnes qui n’utilise pas le zend framework ?
    C’est mon cas et je cherche un moyen de remplacer mon autoload actuel par une solution plus flexible.
    Qui est utilisable avec ou sans espace de nom suivant sa version de PHP.
    Pas évident je l’accorde mais celui-ci me semble très proche de ce que je cherche même si je n’ai pas encore décortiquer l’entièreté.
    Mon histoire de préfixe est simplement pour standardiser un peu tout sa tout en restant flexible et surtout rapide.
    Le cache est régénérer si l’autoload rencontre une nouvelle classes ?
    Merci à vous tous

  22. salut, j’aimerais avoir un exemple d’utilisation de votre code.

  23. Bonjour ou plutôt bonsoir
    voilà j’ai aimé le tutoriel
    Bon sauf moi voici mon problème:
    j’ai cette arborescence de fichier:
    -images
    -config
    -autoload.php
    -bg_class
    -classes(toutes les classes du projet)
    -manager(tous les managers)

    Ce que j’aimerais savoir c’est comment adapter mon fichier autoload.php pour qu’il scanne le contenu de du repertoire bg_class et cs sous repertoires
    et qu’il inclut les classes qui sy trouvent
    Merci d’avance de vos réponses!

  24. Hello. think jobwe’d I do not not anticipate thiswe would Thia password is a fantastic argument. grasping!

Laisser un commentaire


NOTE - Vous pouvez utiliser les éléments et attributs HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Trackbacks and Pingbacks: