Le Design Pattern Stratégie (Strategy) en PHP

Continuons notre tour d’horizon des Modèles de Conception avec cette fois l’étude de la Stratégie, modèle qui vise à permettre le choix d’un algorithme dédié à une tâche particulière au moment de l’exécution.

Dans l’exemple, nous allons utiliser l’autoloader universel réalisé précédemment pour lui adjoindre la possibilité de détecter les espaces de nom.

J’en profite pour remercier Gabriel pour ses commentaires sur le sujet, commentaires qui ont initiés l’idée de ce même article.

Solution sans les Design Pattern

Modifier la classe

Sans les Design Pattern, nous pouvons simplement modifier le code de notre autoloader, en particulier celui de la méthode _loadClasses, afin de lui permettre de détecter les espaces de nom.

Problème : L’autoloader devient compatible uniquement à partir de PHP 5.3+  à cause des constantes que nous allons utiliser (même si bien sûr nous pouvons contourner le problème).

Dupliquer la classe

Un copier / coller de la classe est possible, une version PHP5.3- et une version PHP5.3+. Le problème est ensuite une question de maintenance.

Avec le modèle de conception Template method

Une classe AbstractDirectoriesAutoloader avec toute la logique de l’autoload en laissant _loadClasses abstraite, puis deux classes filles dédiées qui implémentent la recherche propre aux différentes versions de PHP.

Problème : Le code client devra choisir s’il utilise la version PHP5.2 ou PHP5.3, problème facilement contournable avec l’utilisation d’une Fabrique qui sera capable de choisir la bonne implémentation en fonction de la version de notre système, et de fait devra gérer elle même le Singleton (ce qui dans l’absolu n’est pas un mal).

Solution avec le Modèle de Conception Stratégie

Avouons le tout de suite, l’utilisation du modèle de conception stratégie aurait été idéal s’il avait été question de modifier régulièrement, au moment de l’exécution, la configuration du système. Même si ce n’est pas le cas, l’exemple reste très correct et prouve que les patterns ne sont pas des solutions figées, que les patterns sont un outils de réflexion…

Revenons à notre objectif : Nous voulons adapter la méthode _loadClasses et pouvoir en fonction de notre système détecter les espaces de nom ou non (une version PHP5.2 de cette méthode, puis une version PHP5.3).

Pour cela nous allons encapsuler la recherche dans des classes dédiées (des stratégies), classes qui respecterons une interface donnée (l’algorithme encapsulé).

Nous allons donc définir une interface IClassHunter et créer l’implémentation ClassHunterForPHP5_2 (la version actuelle)

interface IClassHunter {
	public function find ($pFileName);
}

class ClassHunterForPHP5_2 implements IClassHunter {
	public function find ($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;
	}
}

Sans oublier bien sûr de modifier notre autoloader pour qu’il utilise ces composants.

class DirectoriesAutoloader {
	private $_classHunterStrategy;
	public static function instance ($pTmpPath){
		if(self::$_instance === false){
			//........
			self::$_instance->_classHunterStrategy = new ClassHunterForPHP5_2 ();
		//......
        }
//..... plus loin

	private function _includesAll (){
		foreach ($this->_directories as $directory=>$recursive){
			//..............
			foreach ($files as $fileName){
				$classes = $this->_classHunterStrategy->find ((string) $fileName);
				foreach ($classes as $className=>$fileName){
					$this->_classes[strtolower ($className)] = $fileName;
				}
		//.................
	}

Nous pouvons maintenant ajouter une stratégie capable de prendre en charge les particularités de PHP 5.3+, à savoir les espaces de nom, en ajoutant simplement une détection de l’espace de nom courant.

class ClassHunterForPHP5_3 implements IClassHunter {
	public function find ($pFileName){
		$toReturn = array ();
		$tokens = token_get_all (file_get_contents ($pFileName, false));

		$currentNamespace = '';
		$namespaceHunt = false;
		$validatedNamespaceHunt = false;
		$classHunt = false;
		$whitespaceCount = 0;
		foreach ($tokens as $token){
			if (is_array ($token)){
				if ($token[0] === T_INTERFACE || $token[0] === T_CLASS){
					$classHunt = true;
					continue;
				}elseif ($token[0] === T_NAMESPACE){
					$namespaceHunt = true;
					continue;
				}

				if ($classHunt && $token[0] === T_STRING){
					$toReturn[(strlen ($currentNamespace) > 0 ? $currentNamespace.'\\' : '').$token[1]] = $pFileName;
					$classHunt = false;
				}elseif ($namespaceHunt && $validatedNamespaceHunt && ($token[0] === T_STRING || $token[0] === T_NS_SEPARATOR)){
					$currentNamespace .= $token[1];
				}elseif ($namespaceHunt && !$validatedNamespaceHunt && $token[0] === T_WHITESPACE){
					$currentNamespace = '';
					$validatedNamespaceHunt = true;
				}elseif ($namespaceHunt && !$validatedNamespaceHunt && $token[0] !== T_WHITESPACE){
					$namespaceHunt = false;
				}
			}else{
				if ($token === ';' || $token === '{'){
					//le seul cas ou cela permet de valider un namespace est la déclaration d'un namespace par défaut namespace{}
					if ($namespaceHunt && !$validatedNamespaceHunt && $token === '{'){
						$currentNamespace = '';
					}
					$classHunt = false;
					$namespaceHunt = false;
					$validatedNamespaceHunt = false;
				}
			}
		}
		return $toReturn;
	}
}

Finalisons maintenant le code de notre autoloader pour qu’il demande à une Fabrique de lui donner la bonne classe de recherche en fonction du système.

class ClassHunterFactory {
	public static function create ($version){
		if (($result = version_compare ($version, '5.3.0')) >= 0){
			return new ClassHunterForPHP5_3 ();
		}
		return new ClassHunterForPHP5_2 ();
	}
}

class DirectoriesAutoloader {
        // .....
	public static function instance ($pTmpPath){
		if(self::$_instance === false){
			self::$_instance = new DirectoriesAutoloader();
			self::$_instance->setCachePath ($pTmpPath);
			self::$_instance->_classHunterStrategy = ClassHunterFactory::create (PHP_VERSION);
//......

Conclusions

Et voilà, notre autoloader fonctionne maintenant en PHP 5.2 ET en PHP 5.3.

En résumé, nous avons séparé l’algorithme de recherche des classes dans des objets dédiés, interchangeables au moment de l’exécution.

En bénéfice nous avons des classes de recherche qui peuvent faire l’objet de tests unitaires très facilement !

Note : La classe de recherche en l’état n’est pas en mesure de traiter les syntaxes type new namespace\Foo () ou namespace\Foo::foo (); EDIT 30/12/2010: Mise à jour du code de recherche permettant une bonne détection des classes et namespaces

Note 2 : afin d’éviter toute erreur / notice en PHP 5.2, il sera nécessaire de séparer les classes de recherche dans des fichiers séparés car la constante T_NAMESPACE n’existe pas en PHP 5.2. Ce sera la Fabrique qui sera en charge d’inclure les fichiers d’implémentations spécifiques.

Code complet

<?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;
	}
}

interface IClassHunter {
	public function find ($pFileName);
}

class ClassHunterForPHP5_3 implements IClassHunter {
	public function find ($pFileName){
		$toReturn = array ();
		$tokens = token_get_all (file_get_contents ($pFileName, false));

		$currentNamespace = '';
		$namespaceHunt = false;
		$validatedNamespaceHunt = false;
		$classHunt = false;
		$whitespaceCount = 0;
		foreach ($tokens as $token){
			if (is_array ($token)){
				if ($token[0] === T_INTERFACE || $token[0] === T_CLASS){
					$classHunt = true;
					continue;
				}elseif ($token[0] === T_NAMESPACE){
					$namespaceHunt = true;
					continue;
				}

				if ($classHunt && $token[0] === T_STRING){
					$toReturn[(strlen ($currentNamespace) > 0 ? $currentNamespace.'\\' : '').$token[1]] = $pFileName;
					$classHunt = false;
				}elseif ($namespaceHunt && $validatedNamespaceHunt && ($token[0] === T_STRING || $token[0] === T_NS_SEPARATOR)){
					$currentNamespace .= $token[1];
				}elseif ($namespaceHunt && !$validatedNamespaceHunt && $token[0] === T_WHITESPACE){
					$currentNamespace = '';
					$validatedNamespaceHunt = true;
				}elseif ($namespaceHunt && !$validatedNamespaceHunt && $token[0] !== T_WHITESPACE){
					$namespaceHunt = false;
				}
			}else{
				if ($token === ';' || $token === '{'){
					//le seul cas ou cela permet de valider un namespace est la déclaration d'un namespace par défaut namespace{}
					if ($namespaceHunt && !$validatedNamespaceHunt && $token === '{'){
						$currentNamespace = '';
					}
					$classHunt = false;
					$namespaceHunt = false;
					$validatedNamespaceHunt = false;
				}
			}
		}
		return $toReturn;
	}
}

class ClassHunterForPHP5_2 implements IClassHunter {
	public function find ($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;
	}
}

class DirectoriesAutoloaderException extends Exception {}

class DirectoriesAutoloader {
	private $_classHunterStrategy;

	//--- 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);
			self::$_instance->_classHunterStrategy = ClassHunterFactory::create (PHP_VERSION);
		}
		return self::$_instance;
	}
	//--- /Singleton

	public function register (){
		spl_autoload_register (array ($this, 'autoload'));
	}

	//--- 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->_classHunterStrategy->find ((string) $fileName);
				foreach ($classes as $className=>$fileName){
					$this->_classes[strtolower ($className)] = $fileName;
				}
			}
		}
	}

	private $_classes = array ();
	private function _saveIncache (){
		$toSave = '<?php $classes = '.var_export ($this->_classes, true).'; ?>';
		if (file_put_contents ($this->_cachePath.'directoriesautoloader.cache.php', $toSave) === false){
			throw new DirectoriesAutoloaderException ('Cannot write cache file '.$this->_cachePath.'directoriesautoloader.cache.php');
		}
	}

	/**
	 * 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 ();
}

class ClassHunterFactory {
	public static function create ($version){
		if (($result = version_compare ($version, '5.3.0')) >= 0){
			return new ClassHunterForPHP5_3 ();
		}
		return new ClassHunterForPHP5_2 ();
	}
}

utilisé comme suit :

<?php
require_once ('./DirectoriesAutoloader.php');
DirectoriesAutoloader::instance('/tmp/')->addDirectory ('./libraries/')->register ();
///...
  1. Il y potentiellement un comportement aléatoire dans la recherche du namespace. il fait confiance a deux T_WHITESPACE à suivre pour déterminer la fin de la déclaration. Même si dans la plupart des cas, cela restera vrai, on peut renforcer la détection.
    une déclaration de namespace est constituée :
    1 – T_NAMESPACE
    2 – T_STRING
    3 – T_NS_SEPARATOR
    4 – T_STRING
    5 – succession de T_STRING et T_NAMESPACE mais toujours un T_STRING à la fin. Chacun pouvant être séparé par un T_WHITESPACE
    6 – ; ou { (namespace avec accolade)
    Il faut donc poursuivre l’analyse. Si ; ou { sont trouvés, nous sommes bien à la fin de notre déclaration, sinon la déclaration est abandonnée.
    Du même coup ca permet de solutionner la syntaxe
    $o = new namespace\maClasse()
    En trouvant ‘(‘, autre chose que T_WHITESPACE, T_NS_SEPARATOR, T_STRING ;, {, la déclaration sera abandonnée

    • Dans la première version de l’article je suis allé un peu vite et j’ai trop voulu simplifier la détection des namespace.

      J’ai mis à jour l’article et le code de détection qui cette fois (a confirmer avec des tests unitaires accompagnés de jeux de tests complets) fonctionne convenablement.

  2. j’ai oublié de préciser que PHP interdit qu’un fichier puisse dire
    namespace espace/nom;

    /*….*/

    namespace autre\espace{

    }
    mais même avec cela, la détection se fera correctement.

  3. ça fonctionne, seulement ma méthode pour exclure les classes qui ne portent pas de préfixes échoue avec les namespaces :cry:

    foreach ($files as $fileName){
    $classes = $this->_classHunterStrategy->find ((string) $fileName);
    foreach ($classes as $className=>$fileName){
    //$this->_classes[strtolower ($className)] = $fileName;
    if(strpos($className,'factory') === 0){
    $this->_classes[strtolower ($className)] = $fileName;
    }
    }
    }

    J’ai ceci dans le fichier externe:

    <?php
    namespace Factory;
    {
    class factory_DataObject{
    public function test(){
    print "test";
    }
    }
    class test{
    public function truc(){
    print "truc";
    }
    }
    }
    namespace deuxieme;{
    class premier{
    function _tostring(){
    echo 'string';
    }
    }
    }

    J’ai modifié les appels :

    addDirectory ('abstractdb/')->register();
    use Factory\test as ns;
    use Factory\factory_DataObject as object;
    use deuxieme\premier as p;
    $hello = new ns;
    $hello->truc();
    $test = new object;
    $test->test();
    $p = new p;
    $p->_tostring();
    ?>

    • Si j’utilise tes deux fichiers (test + déclaration) avec le code fourni dans mon exemple, tout semble ok.

      Je n’ai par contre pas testé ton patch de détection de nom de classe (qui effectivement ne prends pas en compte les namespace)

  4. Pourquoi pas :

    if (! defined(« T_NAMESPACE »)){
    define(« T_NAMESPACE », »UNKNOW_TOKEN »);
    }

    • C’est ce que je voulais dire en mettant la remarque (même si bien sûr nous pouvons contourner le problème)

      Dans notre cas, outre le coté conceptuel moins fun, il n’y a pas de raisons particulière à ne pas le faire (si ce n’est un algo qui se complexifie inutilement pour les cas PHP5.2-).

      Il aurait pu y avoir des raisons comme un comportement différent du tokenizer ou l’utilisation de fonctions spécifiques.

      N’oublions pas non plus le concept de base du pattern : « pouvoir permuter les algorithmes à l’exécution ».

      Dans notre exemple concret un « if !defined suffit », mais si nous avions 10 stratégies possibles il en serait autrement (ou alors avec une méthode magique capable de traiter tous les cas).

      Je ferais un autre article dédié aux stratégies avec des exemples plus adaptés et indiscutables…..

  5. si je place dans le foreach le :

    if (! defined(T_NAMESPACE)){
    define(T_NAMESPACE,UNKNOW_TOKEN);
    }

    Sous php5.2 on aura une belle erreur, ne serait-il pas judicieux de le placer dans le hunterPHP5.3 ou un composant uniquement charger pour php5.3 ?
    Je cherche un moyen de remplacer mon autoload par quelque chose de bien plus modulable, qui me permettrai de passez de php 5.2 => 5.3 sans devoir repasser manuellement dans l’autoload

  6. Bonjour,

    merci beaucoup pour cet excellent article qui m’a fait gagné énormément de temps.

    Bonne continuation.

  7. De l’usage des méthodes statiques | Gerald's Blog - pingback on 7 janvier 2013 at 18 h 08 min
  8. merci bq pour ce code , Est ce que ce code déja utilisé dans un vrai projet ?

  9. Bonjour,

    oui, ce code (ou presque) a déjà été utilisé dans un projet d’entreprise.

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: