Injection de dépendances


1Présentation

L'injection de dépendances permet de découpler la logique entre les objets en faisant appel au principe d'inversion de contrôle.

Temma crée automatiquement un composant pour gérer l'injection de dépendance dans les objets métier. Ce composant est la colonne vertébrale sur laquelle tous les objets de votre application peuvent se reposer pour accéder les uns aux autres.
Il est disponible dans les contrôleurs sous l'attribut privé $_loader.

Par défaut, le composant contient les éléments suivants :

  • $loader->dataSources : Tableau associatif contenant les objects de connexion aux sources de données (voir la documentation des contrôleurs).
  • $loader->session : Objet de gestion des sessions (voir les sessions).
  • $loader->config : Objet de gestion de la configuration, qui donne accès à toutes les directives de configuration.
  • $loader->request : Objet de gestion de la requête entrante, qui permet de manipuler le flux d'exécution du framework.
  • $loader->response : Objet utilisé par le framework pour gérer la réponse vers le client.
  • $loader->controller : Instance du plugin ou du contrôleur en cours d'utilisation.

2Utilisation avec vos DAO

Les objets DAO peuvent être instanciés dans les contrôleurs en utilisant leur méthode _loadDao(). En dehors des contrôleurs, le composant d'injection de dépendances peut être utilisé pour créer des instances d'objets DAO que vous avez développés.

Par exemple, si vous avez créé un objet UserDao (dans le fichier lib/UserDao.php), vous pouvez l'utiliser de cette manière :

$user = $this->_loader->UserDao->getFromEmail($email);

3Utilisation avec vos objets métiers

Un objet qui implémente l'interface \Temma\Base\Loadable peut être chargé par le composant. Il faut alors que son constructeur prenne un seul paramètre, de type \Temma\Base\Loader.

Voici l'exemple d'un objet qui écrit des données dans un fichier var/liste.txt (dans l'arborescence du projet) :

class Liste implements \Temma\Base\Loadable {
    /** Chemin vers le fichier dans lequel écrire. */
    private string $_path = null;

    /**
     * Constructeur.
     * @param \Temma\Base\Loader $loader Composant.
     */
    public function __construct(\Temma\Base\Loader $loader) {
        $this->_path = $loader->config->varPath . '/liste.txt';
    }

    /**
     * Fonction qui écrit dans le fichier.
     * @param string $text Le texte à écrire.
     */
    public function ecrit(string $text) {
        file_put_contents($this->_path, "$text\n", FILE_APPEND);
    }
}
  • Ligne 1 : L'objet implémente l'interface \Temma\Base\Loadable.
  • Ligne 3 : Un attribut privé va contenir le chemin complet vers le fichier dans lequel on écrira par la suite.
  • Ligne 9 : Constructeur de l'objet, qui reçoit en paramètre une instance du composant d'injection de dépendances.
    • Ligne 10 : On construit le chemin vers le fichier, en utilisant l'objet de configuration, dont l'attribut varPath donne le chemin vers le répertoire var/ du projet.
  • Ligne 17 : La méthode ecrit() pourra être utilisée pour écrire dans le fichier.

Pour que cet objet soit chargeable par le framework, il faut qu'il soit accessible dans les chemins d'inclusion du projet. Par défaut, cela veut dire qu'on va l'enregsitrer dans un fichier nommé Liste.php, placé dans le répertoire lib/ du projet.

À partir de ce moment-là, l'objet est disponible directement comme s'il était un attribut du composant. Une seule instance de l'objet sera créée (au premier appel de l'objet).

Voici un exemple de contrôleur qui utilise l'objet Liste grâce au composant :

class Homepage extends \Temma\Web\Controller {
    /** Action racine. */
    public function index() {
        // écriture dans le fichier
        $this->_loader->Liste->ecrit('Ça marche');
    }
}
  • Ligne 5 : On passe par le composant pour accéder à l'objet Liste, puis à sa méthode ecrit().

Maintenant imaginons qu'on crée un autre objet métier, chargeable via le composant, qui utilise l'objet Liste.

class Calcul implements \Temma\Base\Loadable {
    /** Instance du composant d'injection de dépendances. */
    private $_loader = null;

    /**
     * Constructeur.
     * @param \Temma\Base\Loader $loader Composant.
     */
    public function __construct(\Temma\Base\Loader $loader) {
        $this->_loader = $loader;
    }

    /**
     * Fonction qui effectue une addition.
     * @param int $i Nombre.
     * @param int $j Nombre.
     * @return int Résultat de l'addition.
     */
    public function addition(int $i, int $j) : int {
        $resultat = $i + $j;
        $this->_loader->Liste->ecrit("Calculé : $resultat");
        return ($resultat);
    }
}
  • Ligne 3 : Contrairement à l'objet Liste, cet objet va garder dans un attribut privé l'instance du composant d'injection de dépendances.
  • Ligne 10 : Dans le constructeur, on copie l'instance du composant (reçue en paramètre) dans l'attribut privé.
  • Ligne 21 : On utilise le composant pour appeler l'objet Liste.

Ceci illustre comment les objets métier peuvent s'appeler les uns les autres, avec le composant qui gère les accès et les instanciations.


4Accès alternatif

Les exemples vus précédemment partent du principe que les objets gérés par le composant sont placés dans le namespace racine (autrement dit, ils ne sont pas dans un namespace explicite), et que leurs codes sont dans des fichiers placés dans le répertoire lib/ du projet.

Mais parfois, vous allez vouloir utiliser des objets qui sont dans des namespaces profonds. Dans ce cas, il va falloir accéder aux objets en utilisant une écriture de type tableau associatif, et non plus orientée objet.

Par exemple, si on veut utiliser la méthode addition() de l'objet \Math\Base\Calcul, il faudra écrire :

$res = $this->_loader['\Math\Base\Calcul']->addition(3, 4);

Il est aussi possible d'utiliser la méthode get() :

$res = $this->_loader->get('\Math\Base\Calcul')->addition(3, 4);

5Ajouts explicites dans le composant

Vous avez aussi la possibilité de créer vous-même l'objet et de l'ajouter dans le composant en spécifiant le nom que vous souhaitez lui donner :

// on crée l'instance de l'objet
$calcul = new \Math\Base\Calcul($this->_loader);

// on ajoute l'instance dans le composant
// (les trois écritures sont équivalentes)
// - soit avec une écriture orientée objet
$this->_loader->calculatrice = $calcul;
// - soit avec une écriture en tableau associatif
$this->_loader['calculatrice'] = $calcul;
// - soit en utilisant la méthode set()
$this->_loader->set('calculatrice', $calcul);

// maintenant que l'instance est enregistrée dans le composant,
// on peut l'utiliser partout où le composant est accessible
$res = $this->_loader->calculatrice->addition(3, 4);

6Ajouts par callback

Il est aussi possible d'assigner une fonction anonyme. Cette fonction sera exécutée lors du premier appel via le composant ; elle doit retourner l'objet qui sera ensuite retourné par le composant.

Voici un exemple de contrôleur :

class Homepage extends \Temma\Web\Controller {
    // méthode appelée avant l'exécution de l'action
    public function __wakeup() {
        $this->_loader->calc = function($loader) {
            if ($this['param'] == 'base')
                return (new \Math\Base\Calcul($loader);
            return (new \Math\Other\Calcul($loader));
        };
    }

    // action
    public function compute(string $type) {
        $this['param'] = $type:
        $this['res'] = $this->_loader->calc->addition(3, 4);
    }

    // autre action
    public function compute2() {
        $this['res'] = $this->_loader->calc->addition(3, 4);
    }
}
  • Ligne 3 : On utilise la méthode __wakeup() pour initialiser le contrôleur.
  • Ligne 4 : On crée la clé calc dans le composant, en y affectant une fonction anonyme. Cette fonction prend un seul paramètre, qui recevra l'instance du composant. La fonction doit retourner l'objet qui sera retourné par la suite.
  • Lignes 5 à 7 : La variable $this se réfère au contrôleur lui-même. On regarde la valeur contenue par la variable de template param pour savoir quelle implémentation sera retournée.
  • Ligne 13 : On assigne à la variable de template param la valeur reçue dans le paramètre $type.
  • Ligne 14 : On utilise le composant sans avoir à se soucier de savoir quelle implémentation est utilisée.
  • Ligne 19 : On utilise le composant. Ici, ce sera toujours l'objet \Math\Other\Calcul qui est utilisé, mais cela pourrait changer sans avoir besoin de modifier l'action.

7Ajouts par builder

Un builder est une fonction qui se charge de gérer les instanciations, et qu'on enregistre avec la méthode setBuilder() du composant.
Cette fonction prend deux paramètres : le premier est une instance du composant ; le second est le nom de l'objet auquel on essaye d'accéder.

Voici un exemple :

// definition du builder
$this->_loader->setBuilder(function($loader, $key) {
    // on regarde si le nom de l'objet demandé se termine
    // par "BO" ou par "DAO"
    if (substr($key, -2) == 'Bo') {
        $classname = substr($key, 0, -2);
        return new \MyApp\Bo\$classname($loader);
    } else if (substr($key, -3) == 'Dao') {
        $classname = substr($key, 0, -3);
        return new \MyApp\Dao\$classname($loader);
    }
});

// cet appel va utiliser l'objet \MyApp\Bo\Mail
$this->_loader->MailBo->send();

// cet appel va utiliser l'objet \MyApp\Dao\User
$this->_loader->UserDao->remove($userId);

8Redéfinition du loader en configuration

Plutôt que d'appeler explicitement la méthode setBuilder(), il est possible de spécifier un objet loader dans le fichier etc/temma.php (voir la documentation de la configuration). Cet objet doit hériter de la classe \Temma\Base\Loader, et contenir une méthode protégée builder(). Cette méthode doit prendre en paramètre le nom de l'objet à instancier, et retourner une instance de ce dernier.

Voici un exemple. Pour commencer, la configuration Temma dans le fichier etc/temma.php :

<php

return [
    'application' => [
        'loader' => 'MyLoader',
    ]
]

Ensuite, dans le fichier lib/MyLoader.php :

class MyLoader extends \Temma\Base\Loader {
    protected function builder(string $key) {
        if ($key == 'User')
            return new \MyApp\Dao\User($this);
        else if ($key == 'HttpClient')
            return new \Utils\Http\Client($this);
        throw new Exception("Unknown object '$key'.");
    }
}

Il devient alors possible d'écrire :

// utilise \MyApp\Dao\User
$user = $this->_loader->User->get($userId);

// utilise \Utils\Http\Client
$this->_loader->HttpClient->post($url, $data);