Laravel

Un framework qui rend heureux

Voir cette catégorie
Vers le bas
Laravel 4 : chapitre 31 : Le conteneur de dépendances (IoC)
Vendredi 14 février 2014 14:10

L'objet Application

Le conteneur de dépendances de Laravel constitue la clé de fonctionnement du framework. On peut très bien utiliser Laravel sans jamais avoir regardé dans la "salle des machines", mais il devient indispensable de s'y intéresser dès qu'on veut ajouter des fonctionnalités. Alors c'est quoi ce conteneur ? Comme son nom l'indique c'est une "boîte" dans laquelle on va mettre tout ce qui nous est utile. déjà il porte un nom : c'est l'objet Application. Voici le cycle de démarrage de Laravel :

img45

L'objet Application sert de conteneur pour Laravel. Si on regarde la signature de la classe :

class Application extends Container implements HttpKernelInterface, TerminableInterface, ResponsePreparerInterface

On se rend compte qu'elle étend la classe Container. Regardons celle-ci :

class Container implements ArrayAccess

Cette classe au nom explicite implémente l'interface ArrayAccess qui est une interface standard de PHP :

 ArrayAccess {
/* Méthodes */
abstract public boolean offsetExists ( mixed $offset )
abstract public mixed offsetGet ( mixed $offset )
abstract public void offsetSet ( mixed $offset , mixed $value )
abstract public void offsetUnset ( mixed $offset )
}

Ce qui veut dire qu'on peut accéder au contenu de l'application comme s'il s'agissait d'un tableau. La classe Container évidemment définit les 4 méthodes :

	/**
	 * Determine if a given offset exists.
	 *
	 * @param  string  $key
	 * @return bool
	 */
	public function offsetExists($key)
	{
		return isset($this->bindings[$key]);
	}

	/**
	 * Get the value at a given offset.
	 *
	 * @param  string  $key
	 * @return mixed
	 */
	public function offsetGet($key)
	{
		return $this->make($key);
	}

	/**
	 * Set the value at a given offset.
	 *
	 * @param  string  $key
	 * @param  mixed   $value
	 * @return void
	 */
	public function offsetSet($key, $value)
	{
		// If the value is not a Closure, we will make it one. This simply gives
		// more "drop-in" replacement functionality for the Pimple which this
		// container's simplest functions are base modeled and built after.
		if ( ! $value instanceof Closure)
		{
			$value = function() use ($value)
			{
				return $value;
			};
		}

		$this->bind($key, $value);
	}

	/**
	 * Unset the value at a given offset.
	 *
	 * @param  string  $key
	 * @return void
	 */
	public function offsetUnset($key)
	{
		unset($this->bindings[$key]);

		unset($this->instances[$key]);
	}

On peut se demander maintenant comment on accède à cet objet Application. Quand on regarde la documentation on ne trouve que des appels statiques du genre :

Route::get

C'est le système des façades utilisé par Laravel qui permet ce genre d'appel. Mais comment avoir une référence du conteneur si on en a besoin ? Il faut utiliser la méthode getFacadeRoot. Dans le fichier des routes entrez ce code :

Route::get('/', function()
{
	$app = App::getFacadeRoot();
	dd($app);
});

Vous verrez apparaître le contenu de l'objet Application, il est très fourni Surprised.

Gérer du contenu

Puisqu'on a un conteneur et qu'on peut y accéder comme un tableau, voyons comment utiliser cette possibilité :

Route::get('/', function()
{
	$app = App::getFacadeRoot();
	$app['nom'] = 'Toto';
	echo $app['nom'];
});

Si tout va bien on voit apparaître "Toto" comme résultat. On a donc mis une valeur textuelle dans le conteneur et on a pu la récupérer. On obtient le même résultat avec ce code :

Route::get('/', function()
{
	$app = App::getFacadeRoot();
	$app->offsetSet('nom', 'Toto');
	echo $app->offsetGet('nom');
});

On a juste appelé directement les méthodes de la classe Container.

On peut mettre ce que l'on veut dans le conteneur. Voici le code enrichi pour montrer que ça fonctionne pour des tableaux, des objets et des closures :

Route::get('/', function()
{
	$app = App::getFacadeRoot();

	class MaClasse { public $propriété = 'je suis une propriété'; };
	$app['nom'] = 'Toto';
	$app['tableau'] = Array('un' => 1, 'deux' => 2);
	$app['objet'] = new MaClasse;
	$app['closure'] = function() { return 'Je suis une closure !'; };

	echo $app['nom'], '<br>';
	echo var_dump($app['tableau']), '<br>';
	echo var_dump($app['objet']), '<br>';
	echo $app['closure'];
});

On obtient ce résultat :

Toto
array(2) { ["un"]=> int(1) ["deux"]=> int(2) }
object(MaClasse)#224 (1) { ["propriété"]=> string(23) "je suis une propriété" }
Je suis une closure !

Si vous regardez de plus près le code de la méthode offsetSet vous allez voir qu'on transforme en fait toutes les valeurs en closures :

public function offsetSet($key, $value)
{
	// If the value is not a Closure, we will make it one. This simply gives
	// more "drop-in" replacement functionality for the Pimple which this
	// container's simplest functions are base modeled and built after.
	if ( ! $value instanceof Closure)
	{
		$value = function() use ($value)
		{
			return $value;
		};
	}

	$this->bind($key, $value);
}

On va comprendre un peu plus loin pourquoi il doit en être ainsi...

Liaison avec le conteneur

On va pousser un peu plus loin la compréhension du conteneur avec la liaison. Regardons cette ligne de code de la méthode offsetSet déjà vue ci-dessus :

$this->bind($key, $value);

C'est cette méthode bind qui va établir la liaison avec ce qu'on veut mettre dans le conteneur. Voici la signature de cette méthode :

public function bind($abstract, $concrete = null, $shared = false)

On a 3 paramètres dont les deux derniers sont optionnels. Faisons un essai :

Route::get('/', function()
{
	$app = App::getFacadeRoot();
	$app->bind('closure', function() { return 'Je suis une closure !'; });
	echo $app['closure'];
});

On se rend compte que ça fonctionne, on a bien mis la closure dans le conteneur en lui transmettant un identifiant et une valeur.

La méthode make est l'inverse de bind. Elle permet de récupérer ce qu'on a mis dans le conteneur :

Route::get('/', function()
{	
	$app = App::getFacadeRoot();
	$app->bind('closure', function() { return 'Je suis une closure !'; });
	echo $app->make('closure');
});
Si on utilise la façade on rend le code plus simple à lire :
Route::get('/', function()
{	
	App::bind('closure', function() { return 'Je suis une closure !'; });
	echo App::make('closure');
});

Liaison de classes et injection de dépendance

C'est bien de pouvoir mettre dans le conteneur des valeurs, des fonctions anonymes, des objets... mais c'est beaucoup plus intéressant d'y placer des classes. Mais il s'agit là d'un abus de langage, une classe n'est pas une entité mais juste un moule pour des objets. On va alors parler plutôt de dépendance. Regardez cet exemple :

Route::get('/', function()
{
	class Expression { 
		public function coucou()	{
			return 'Coucou toi !';
		}
		public function dire($texte)	{
			return 'Je dis ' . $texte;
		}
	};

	App::bind('expression', function()
	{
	    return new Expression;
	});

	$expression =  App::make('expression');
	echo $expression->coucou(), '<br>', $expression->dire('que je vais bien');
});

Avec comme résultat :

Coucou toi !
Je dis que je vais bien

Je définis une classe Expression avec deux méthodes. Ensuite la classe est référencée dans le conteneur avec la méthode bind avec le nom expression et une fonction anonyme, celle-ci se contentant de créer un objet à partir de la classe. Lorsque je veux me servir de ma classe j'utilise la méthode make. Quel est l'avantage de cette approche ? C'est que je peux modifier comme je veux ma classe sans toucher à l'application.

Le conteneur peut utiliser la réflexion pour les classes non enregistrées. Reprenons l'exemple précédent sous cette forme :

Route::get('/', function()
{
	class Expression { 
		public function coucou()	{
			return 'Coucou toi !';
		}
		public function dire($texte)	{
			return 'Je dis ' . $texte;
		}
	};
	$expression =  App::make('Expression');
	echo $expression->coucou(), '<br>', $expression->dire('que je vais bien');
});

Cette fois je n'ai pas utilisé la méthode bind pour enregistrer la classe dans le conteneur. Mais la réflexion PHP permet de retrouver automatiquement la classe Smile.

Supposons maintenant que notre classe implémente une interface. On va créer une liaison en enregistrant le type abstrait (l'interface) et le type concret (la classe) :

Route::get('/', function()
{
	interface TexteInterface {
		public function dire($texte);
	}

	class Expression implements TexteInterface { 
		public function dire($texte)	{
			return 'Je dis ' . $texte;
		}
	};

	App::bind('TexteInterface', 'Expression');

	$expression =  App::make('TexteInterface');
	echo $expression->dire('que je vais bien');	
});

De cette manière lorsque j'utilise la méthode make en désignant l'interface j'ai au retour une instance de la classe  enregistrée. Vous me direz quel intérêt ? Au lieu de mettre TexteInterface comme nom j'aurais pu mettre n'importe quoi ! Je vais enrichir l'exemple pour vous montrer l'intérêt de procéder ainsi :

Route::get('/', function()
{
	interface TexteInterface {
		public function dire($texte);
	}

	class Expression implements TexteInterface { 
		public function dire($texte)	{
			return 'Je dis ' . $texte;
		}
	};

	class Parole {
	    public function __construct(TexteInterface $texte) {
	        $this->texte = $texte;
	    }
	}

	App::bind('TexteInterface', 'Expression');

    $parole =  App::make('Parole');
    echo $parole->texte->dire('que je vais bien');    	
});

J'ai ajouté la classe Parole qui attend dans son constructeur un objet qui implémente l'interface TexteInterface. Comme j'ai enregistré cette interface dans le conteneur en désignant la classe Expression je vais avoir automatiquement création d'une instance de Expression dans Parole. Ça ne fonctionne que parce que je crée l'objet parole par l'intermédiaire du conteneur en résolution automatique.

On injecte une dépendance dans la classe Parole, de cette façon cette classe n'a pas à se préoccuper de la manière dont est fabriqué ce qu'elle va utiliser. Le fait de définir cette dépendance comme une interface permet de l'implémenter comme on veut. Il suffit de changer une ligne du code ! Si vous regardez le code de Laravel vous verrez que ce principe a été appliqué partout.

Le fait d'utiliser des interfaces rend le code plus clair, plus facile à maintenir et à tester. Une interface est un peu un "contrat" qui indique comment implémenter les fonctionnalités. Ici l'interface TexteInterface oblige à prévoir une méthode dire. Au niveau de l'enregistrement on indique que ce "contrat" est rempli par la classe Expression. On peut facilement changer cela avec une autre classe qui respecte le "contrat" passé par l'interface. La classe Parole ignore tout de ces manipulations et n'a pas à s'en soucier. Elle utilise la méthode dire de l'objet qui se crée automatiquement et qui implémente l'interface.

Singleton et objet existant

Parfois on veut avoir une seule instance d'une classe, il faut utiliser alors la méthode singleton avec la même syntaxe :

App::singleton('TexteInterface', 'Expression');

Des fois un objet existe déjà et on veut l'enregistrer dans le conteneur. On a vu précédemment comment faire sans utiliser la façade. On peut également le faire à partir de la façade avec la méthode instance :

$expression = new Expression;
App::instance('TexteInterface', $expression);

Les Services Providers

On peut se poser maintenant la question : où placer les enregistrements du conteneur au niveau du code ? La façon la plus élégante est d'utiliser une classe Service provider. Je vais prendre comme exemple la partie encryptage de Laravel. On trouve le code ici :

img46

On a la classe Encrypter pour le code d'exécution et la classe EncryptionServiceprovider pour l'enregistrement dans le conteneur. Voyons comment est constituée cette dernière classe :

<?php namespace Illuminate\Encryption;

use Illuminate\Support\ServiceProvider;

class EncryptionServiceProvider extends ServiceProvider {

	/**
	 * Register the service provider.
	 *
	 * @return void
	 */
	public function register()
	{
		$this->app->bindShared('encrypter', function($app)
		{
			return new Encrypter($app['config']['app.key']);
		});
	}

}

On a une méthode register chargée de l'enregistrement dans le conteneur (et uniquement de cela ! ne comptez pas utiliser d'autres classes de Laravel à ce niveau, la méthode boot est faite pour cela). La liaison est accomplie avec la méthode bindShared (une version de liaison partagée), avec le nom encrypter et une fonction anonyme. Dans celle-ci on retourne une instance de la classe Encrypter en transmettant comme paramètre la clé de cryptage qui est dans le fichier de configuration. On retrouve donc ce que nous avons vu précédemment. Pour qu'il soit chargé automatiquement il est déclaré dans le fichier app/config/app.php :

	'providers' => array(

		....

		'Illuminate\Encryption\EncryptionServiceProvider',

		...

	),

On parle alors de fournisseur de service. On peut donc écrire ce code :

$secret = $app['encrypter']->encrypt('passe');
echo $secret;

Puisque la classe a été enregistrée dans le conteneur ça va fonctionner. Ou avec cette syntaxe équivalent :

$secret = app('encrypter')->encrypt('passe');
echo $secret;

Mais comme cette classe a aussi une façade autant l'utiliser ainsi :

$encrypt = App::make('encrypter');
$secret = $encrypt->encrypt('passe');
echo $secret;

Étant donné que la classe Encrypter est enregistrée elle peut être injectée dans un constructeur. regardez par exemple le fichier Cookie/Guard.php :

/**
 * Create a new CookieGuard instance.
 *
 * @param  \Symfony\Component\HttpKernel\HttpKernelInterface  $app
 * @param  \Illuminate\Encryption\Encrypter  $encrypter
 * @return void
 */
public function __construct(HttpKernelInterface $app, Encrypter $encrypter)
{
	$this->app = $app;
	$this->encrypter = $encrypter;
}

On trouve en second paramètre du constructeur une référence à la classe Encrypter qui sera instanciée automatiquement.

En analysant le code de Laravel

On apprend beaucoup de choses en analysant le code de Laravel. Pour rester dans notre thème d'interfaces pour organiser le code et d'injection de dépendance voyons de plus près ce qui concerne les vues :

img47

On trouve une interface pour la recherche des vues :

<?php namespace Illuminate\View;
interface ViewFinderInterface {
	public function find($view);
	public function addLocation($location);
	public function addNamespace($namespace, $hint);
	public function addExtension($extension);
}

J'ai retiré les commentaires pour ne pas trop charger la page. On trouve la définition de 4 méthodes dans cette interface. La première pour localiser une vue, la seconde pour ajouter une localisation au finder, la troisième pour ajouter un espace de nom et la dernière pour ajouter une extension. Ces 4 méthodes sont codées dans la classe FileViewFinder :

<?php namespace Illuminate\View;
use Illuminate\Filesystem\Filesystem;
class FileViewFinder implements ViewFinderInterface {
...
	public function __construct(Filesystem $files, array $paths, array $extensions = null)
	{
		$this->files = $files;
		$this->paths = $paths;

		if (isset($extensions))
		{
			$this->extensions = $extensions;
		}
	}
...
	public function find($name)
...
	public function addLocation($location)
...
	public function addNamespace($namespace, $hints)
...
	public function addExtension($extension)
...

J'ai encore juste mis en évidence le code important. Remarquez au passage l'injection dans le constructeur de la classe Filesystem. On trouve enfin une injection du finder dans la classe Environment :

	public function __construct(EngineResolver $engines, ViewFinderInterface $finder, Dispatcher $events)
	{
		$this->finder = $finder;
		$this->events = $events;
		$this->engines = $engines;

		$this->share('__env', $this);
	}

Pour que ça fonctionne il faut évidemment que le conteneur soit au courant qu'il doit créer ici une instance de la classe FileViewFinder. Cela est effectué dans le service provider :

	public function registerViewFinder()
	{
		$this->app->bindShared('view.finder', function($app)
		{
			$paths = $app['config']['view.paths'];

			return new FileViewFinder($app['files'], $paths);
		});
	}

Finalement la classe Environment est injectée dans le constructeur de la classe View:

	public function __construct(Environment $environment, EngineInterface $engine, $view, $path, $data = array())
	{
		...
		$this->environment = $environment;
                ...
	}

Le code est ainsi bien organisé et hiérarchisé grâce au conteneur. Il est aussi facile à tester puisque chaque classe à une tâche bien délimitée.



Par bestmomo

Aucun commentaire