Injection de dépendances, Containers & PHP-DI

Bonjour

Matthieu Napoli

My C-Sense (myclabs), Lyon

mnapoli.fr / @matthieunapoli

github.com/mnapoli

PHP-DI

Container d'injection de dépendances

depuis Mars 2012

DI ≠ DIC

Injection de dépendances

Construire son code pour ne plus créer ses dépendances

=> méthode

Container

Construit des objets et injecte les dépendances

=> outil

Injection de dépendances

Workflow d'un code classique

Application :


$foo = new FooController();
$foo->loginAction();
                    

Classe FooController :


public function loginAction() {
    $bar = new Bar(); // ou $bar = Bar::getInstance()
    $bar->doSomething();
}
                        

Classe Bar :


public function doSomething() {
    $bim = new Bim(); // ou $bim = Bim::getInstance()
    $bim->doSomethingElse();
}
                        

Workflow d'un code classique

=> dépendances choisies lors de l'écriture du code (hard-coded)

  • Tests : tester du code qui utilise un service web, des fichiers, une BDD, du cache, …
  • Extensibilité : mon système de log loggue dans un fichier, j'aimerai qu'il écrive en BDD et les envoie par email…
  • Coupling : je veux changer de système de cache (ou sa config) -> je dois changer le code partout…

Transformation

Transformation

Avant :


class FooController {
    public function loginAction() {
        $bar = new Bar(); // ou $bar = Bar::getInstance()
        $bar->doSomething();
    }
}
                    

Après :


class FooController {
    private $bar;

    public function __construct(Bar $bar) {
        $this->bar = $bar;
    }

    public function loginAction() {
        $this->bar->doSomething();
    }
}
                    

Workflow d'un code utilisant l'injection de dépendances

Application :


$bim = new Bim();
$bar = new Bar($bim);
$foo = new FooController($bar);

$foo->loginAction();
                    

Classe FooController :


public function loginAction() {
    $this->bar->doSomething();
}
                        

Classe Bar :


public function doSomething() {
    $this->bim->doSomethingElse();
}
                        

Inversion of Control

=> dépendances choisies lors de l'exécution

  • Possibilité de remplacer les dépendances injectées
  • Dans les classes, on ne gère plus les dépendances, leurs configurations…
  • Code plus générique :
    code against interfaces

Code against interfaces


class StoreService {
    public function __construct(GeolocationService $geolocationService) { … }
}
                    

interface GeolocationService {
    public function getCoordinates($address);
}

class GoogleMaps implements GeolocationService { … }

class OpenStreetMap implements GeolocationService { … }
                    

Mais

Nécessite de gérer les dépendances dans l'application


$bim = new Bim();
$bar = new Bar($bim);
$foo = new FooController($bar);
                    

Container

Workflow d'un code utilisant un Container

Application :


$foo = $container->get('FooController');

$foo->doSomething();
                    

Classe FooController :


public function loginAction() {
    $this->bar->doSomething();
}
                        

Classe Bar :


public function doSomething() {
    $this->bim->doSomethingElse();
}
                        

$foo = $container->get('FooController');
                    

=


$bim = new Bim();
$bar = new Bar($bim);
$foo = new FooController($bar);
                    

Le container fait les injections, et aide à construire les graphes d'objets

Attention !


$foo = $container->get('FooController');
                    

Service Locator : anti-pattern

Configuration du container

Nécessite de configurer le container pour qu'il injecte les bons objets

Chaque container se configure différement

Exemple simple


$container = new Container();

$bim = new Bim();
$bar = new Bar($bim);

$container->set('Bim', $bim);
$container->set('Bar', $bar);
                    

Problème : initialise tous les objets à chaque requête

Callbacks


$container = new Container();

$container->set('Bim', function() {
    return new Bim();
});
$container->set('Bar', function() use ($container) {
    return new Bar($container->get('Bim');
});
                    

Verbeux, mais efficace

Fichiers de configuration

  • Tableau PHP
  • YAML
  • XML

Formats différents pour chaque librairie

  • Symfony DI
  • Zend\Di (+ tous les autres)
  • Pimple
  • Aura DI
  • Laravel
  • Mouf
  • Orno
  • PPI

PHP-DI

Pourquoi ?

  • Pratique
  • Volontairement copieur
  • Framework-agnostic

PHP-DI

  • v1.0 (août 2012) : injections via annotations (POC)
  • v2.0 (décembre 2012) : refactoring, version stable
  • v3.0 (avril 2013) : annotations, config PHP, YAML, …
  • v3.5 (octobre 2013)

Reflection

Résolution de dépendances intelligente


class Foo {
    public function __construct(Bar $param1) {
    }
}
                    

~ 90% des cas -> 0 configuration

Annotations

Inspiré de Java/Spring


use DI\Annotation\Inject;

class UserController {
    /**
     * @Inject
     * @var UserRepository
     */
    private $userRepository;

    public function loginAction($email, $password) {
        $user = $this->userRepository->login($email, $password);
        // ...
    }
}
                    

Pas la solution à tout, mais très pratique dans les contrôleurs

Configuration PHP


$container->set('Bar');

$container->set('Foo')
    ->withConstructor(['Bar']);

$container->set('My\Interface')
    ->bindTo('My\Implementation');
                    

YAML


Bar:

Foo:
  constructor: [ Bar ]

My\Interface:
  class: My\Implementation
                    

Futur

  • PHP-DI : v4.0, intégration avec d'autres frameworks, Symfony
  • PHP-FIG : collaboration pour une PSR ContainerInterface, chainage de containers…
  • Communauté : diffusion, meilleure doc et best-practices dans ZF2 et Symfony 2… (service locator)

Merci