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 : Registre permettant d'accéder aux objets 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);

Vous pouvez ajouter n'importe quel élément dans le composant (pas uniquement des objets qui implémentent l'interface \Temma\Base\Loadable) :

$this->_loader->unEntier = 3;
$this->_loader->unTableau = ['a', 'b', 'c'];
$this->_loader->unObjet = new \Toto();

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 (str_ends_with($key, 'Bo')) {
        $classname = substr($key, 0, -2);
        return new \MyApp\Bo\$classname($loader);
    } else if (str_ends_with($key, '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);

8Gestion des alias

8.1Définition d'alias

Il est possible de définir des alias de nommage, qui seront utilisés lorsqu'un objet (implémentant obligatoirement l'interface \Temma\Base\Loadable) sera appelé via le loader.

Vous pouvez déclarer un alias avec la méthode setAlias() :

// définition d'un alias
$this->_loader->setAlias('UserBo', '\MyApp\User\UserBo');

// utilisation de l'alias
$list = $this->_loader->UserBo->getList();
// est équivalent à
$list = $this->_loader['\MyApp\User\UserBo']->getList();

Vous pouvez aussi déclarer plusieurs alias en passant un tableau associatif à la méthode setAliases() :

// définition d'un tableau d'alias
$this->_loader->setAliases([
    'UserBo'  => '\MyApp\User\UserBo',
    'TµEmail' => '\Temma\Utils\Email',
]);

// utilisation des alias
$list = $this->_loader->UserBo->getList();
$this->_loader->TµEmail->textMail($from, $to, $title, $message);

// équivalent à
$list = $this->_loader['\MyApp\User\UserBo']->getList();
$this->_loader['\Temma\Utils\Email']->textMail($from, $to, $title, $message);

Il est possible de supprimer un alias préalablement défini, en fournissant la valeur null pour le même nom d'alias, que ce soit avec la méthode setAlias() ou avec la méthode setAliases().


8.2Configuration des alias

Plutôt que de définir les alias avec les méthode setAlias() et setAliases(), il est plus simple de les lister dans la configuration du projet.

Par exemple, avec le fichier etc/temma.php suivant :

<?php

return [
    'x-loader' => [
        'aliases' => [
            'UserBo'  => '\MyApp\User\UserBo',
            'TµEmail' => '\Temma\Utils\Email',
        ]
    ]
];

Vous pourrez écrire le code suivant :

$list = $this->_loader->UserBo->getList();

$this->_loader->TµEmail->textMail($from, $to, $title, $message);

8.3Gestion des alias par callback

Quand on définit un alias (que ce soit par les méthodes setAlias() et setAliases() ou dans le fichier de configuration), il est possible de lui définir une valeur qui soit de type callable. Dans ce cas, lorsque l'élément est demandé au loader, la fonction pointée est exécutée (en lui passant en paramètre l'instance du loader), et son retour est utilisé comme valeur associée au nom demandé.

Exemple de code avec une fonction :

// définition d'une fonction qui retourne un objet en fonction de la configuration
function createUser(\Temma\Base\Loader $loader) {
    if ($loader->config->xtra('userType', 'internal', false))
        return new \MyApp\User($loader);
    return new \OtherApp\User($loader);
}

// ajout de l'alias
$this->_loader->setAlias('User', 'createUser');

Exemple avec une fonction anonyme :

// ajout de l'alias, avec la définition de la fonction anonyme
$this->_loader->setAlias('WalletDao', function(\Temma\Base\Loader $loader) {
    return new \MyApp\Wallet\WalletDao($loader->dataSources->db);
});

Exemple avec un objet :

// définition d'un objet contenant une méthode qui retourne un objet
class CategoryManager {
    public function generate(\Temma\Base\Loader $loader) {
        return new \MyApp\CategoryDao($loader);
    }
}

// ajout de l'alias
$this->_loader->setAlias('Cat', ['CategoryManager', 'generate']);

Exemple avec un objet statique :

namespace MyApp {
    // définition d'un objet avec une méthode statique
    class ArticleBuilder {
        static public function build(\Temma\Base\Loader $loader) {
            $objectName = $loader->config->xtra('articleManager', 'objectName', '\MyApp\Article');
            return new $objectName($loader);
        }
    }
}

namespace {
    // ajout de l'alias
    $this->_loader->setAlias('Article', '\MyApp\ArticleBuilder::build');
    // écriture alternative
    $this->_loader->setAlias('Article', ['\MyApp\ArticleBuilder', 'build']);
}

8.4Utilisation des alias pour les tests

Lorsque vous exécutez des tests automatisés sur une application Temma, vous pouvez avoir besoin de remplacer un objet par un mock, un "objet bouchon" qui simule le comportement de l'objet original.

Avec les alias de nommage, il devient très facile de créer un fichier etc/temma.test.php (si l'environnement d'exécution s'appelle test), qui redéfinit les objets chargés pour un nom donné.

Exemple de fichier etc/temma.php :

<?php

return [
    'x-loader' => [
        'aliases' => [
            'UserBo'  => '\MyApp\User\UserBo'
        ]
    ]
];

Exemple de fichier etc/temma.test.php :

<?php

return [
    'x-loader' => [
        'aliases' => [
            'UserBo'             => '\MyMock\UserBo',
            '\Temma\Utils\Email' => '\MyMock\Email',
        ]
    ]
];

Votre code applicatif pourra contenir :

// cette ligne de code :
$list = $this->_loader->UserBo->getList();
// en temps normal, sera équivalente à :
$list = $this->_loader['\MyApp\User\UserBo']->getList();
// en environnement de test, sera équivalente à :
$list = $this->_loader['\MyMock\UserBo']->getList();

// cette ligne de code :
$this->_loader['\Temma\Utils\Email']->textMail($from, $to, $title, $message);
// en environnement de test, sera équivalente à :
$this->_loader['\MyMock\Email']->textMail($from, $to, $title, $message);

9Gestion des préfixes

9.1Présentation des préfixes

En plus des alias, il est aussi possible de définir des préfixes de nommage, qui résument des namespaces (ou des morceaux de namespace). Ces préfixes peuvent ensuite être utilisés au début du nom d'un objet géré par le loader.

Attention : Évitez de lister trop de préfixes, car ils sont tous passés en revue à chaque fois qu'un objet est demandé pour la première fois. Cela peut avoir un impact sur les performances.


9.2Définition de préfixe

Vous pouvez déclarer un préfixe avec la méthode setPrefix() :

// définition d'un préfixe
$this->_loader->setPrefix('global', '\MyApp');

// utilisation du préfixe
$object = $this->_loader->globalArticles;
// est équivalent à
$object = $this->_loader['\MyAppArticles'];

// deuxième préfixe
$this->_loader->setPrefix('App', '\OtherApp\Extension\OtherApp\\');

// utilisation du préfixe
$object = $this->_loader->AppBidule;
// est équivalent à
$object = $this->_loader['\OtherApp\Extension\OtherApp\Bidule'];

// troisième préfixe
$this->_loader->setPrefix('€x', '\Europa\Source\Bo');

// utilisation du préfixe
$object = $this->_loader->€xUser;
// est équivalent à
$object = $this->_loader['\Europa\Source\BoUser'];

Vous pouvez aussi déclarer plusieurs préfixes en passant un tableau associatif à la méthode setPrefixes() :

// définition d'un tableau de préfixes
$this->_loader->setPrefixes([
    'global' => '\MyApp',
    'App'    => '\OtherApp\Extension\OtherApp\\',
    '€x'     => '\Europa\Source\Bo',
]);

Il est possible de supprimer un préfixe préalablement défini, en fournissant la valeur null pour le même préfixe, que ce soit avec la méthode setPrefix() ou avec la méthode setPrefixes().


9.3Configuration des préfixes

Plutôt que de définir les préfixes avec les méthode setPrefix() et setPrefixes(), il est plus simple de les lister dans la configuration du projet.

Exemple de fichier etc/temma.php :

<?php

return [
    'x-loader' => [
        'prefixes' => [
            'global' => '\MyApp',
            'App'    => '\OtherApp\Extension\OtherApp\\',
            '€x'     => '\Europa\Source\Bo',
        ]
    ]
];

Vous pourrez ensuite écrire le code suivant :

// écritures équivalentes
$object = $this->_loader->globalArticles;
$object = $this->_loader['\MyAppArticles'];

// écritures équivalentes
$object = $this->_loader->AppBidule;
$object = $this->_loader['\OtherApp\Extension\OtherApp\Bidule'];

// écriture équivalentes
$object = $this->_loader->€xUser;
$object = $this->_loader['\Europa\Source\BoUser'];

9.4Gestion des préfixes par callback

Quand on définit un préfixe (que ce soit par les méthodes setPrefix() et setPrefixes() ou dans le fichier de configuration), il est possible de lui définir une valeur qui soit de type callable. Dans ce cas, lorsque l'élément est demandé au loader, la fonction pointée est exécutée (en lui passant en paramètre l'instance du loader et le nom d'objet demandé auquel a été retiré le préfixe), et son retour est utilisé comme valeur associée au nom demandé.

Exemple de code avec une fonction :

// définition d'une fonction qui retourne un objet en fonction de la configuration
function sinclairManager(\Temma\Base\Loader $loader, string $name) {
    if ($name == 'Spectrum')
        return new \Sinclair\Computers\ZxSpectrum();
    if ($name == '81')
        return new \Sinclair\Computers\Zx81();
    if ($name == 'QL')
        return new \Sinclair\Computers\QantumLeap();
    return (null);
}

// ajout du préfixe
$this->_loader->setPrefix('Zx', 'sinclairManager');

// utilisation
$computer = $this->_loader->ZxSpectrum;
// équivalent à
$computer = $this->_loader['\Sinclair\Computers\ZxSpectrum'];

// autre utilisation
$computer = $this->_loader->Zx81;
// équivalent à
$computer = $this->_loader['\Sinclair\Computers\Zx81'];

// autre utilisation
$computer = $this->_loader->ZxQL;
// équivalent à
$computer = $this->_loader['\Sinclair\Computers\QantumLeap'];

Comme pour les alias, il est possible de passer le nom d'une fonction, ou une fonction anonyme, une chaîne d'appel statique (par exemple 'MonObjet::maMethode') ou un tableau d'appel à un objet instancié (par exemple [$objet, 'maMethode']).


10Redé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);