Injection de dépendances


1Présentation

1.1Principe

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 (appelé “loader”) 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.

Le loader peut instancier automatiquement les objets qu'on lui demande, mais il peut aussi contenir des valeurs (scalaires ou objets) qui lui sont fournis explicitement.

Le fonctionnement nominal du loader est qu'il fournit toujours la même instance pour un type d'objet donné.
Lorsqu'un objet est demandé au loader :

  • Si le loader ne connaît pas encore cet objet, il l'instancie, garde l'instance dans son cache, et retourne l'instance.
  • Si le loader connaît déjà cet objet (soit parce qu'il lui a été founi, soit parce que le loader l'a déjà instancié), le loader retourne l'instance

1.2Utilisation

Le loader peut être utilisé de deux manières différentes : comme un service locator, ou en faisant du l'autowiring.

Service locator

C'est typiquement l'usage dans les contrôleurs. Le loader est utilisé pour réclamer les objets nécessaires. Dans vos contrôleurs, vous utilisez le loader pour récupérer les instances des objets dont vous avez besoin.

Exemple d'utilisation :

// dans un contrôleur => service locator
class MonController extends \Temma\Web\Controller {
    public function __invoke() {
        // utilisation d'un objet métier en passant par le loader
        $this['data'] = $this->_loader->MonObjet->traitement();
    }
}
  • Ligne 5 : On utilise le loader pour récupérer une instance de l'objet MonObjet, dont on appelle la méthode traitement(). Si le loader possède déjà cette instance en cache, il la retourne ; sinon il instancie l'objet avant de le retourner.
Autowiring

Vos objets (hors contrôleurs) reçoivent leurs dépendances en paramètres de leurs constructeurs. Au besoin, le loader va instancier les objets attendus avant de les fournir au constructeur.

// dans un objet métier => autowiring
class MonObjet {
    // Injection des dépendances par autowiring.
    // Le loader instancie d'abord les objets qui doivent être passés au constructeur.
    public function __construct(
        private UserDao $userDao,
        private BucketBo $bucketBo
    ) {
    }

    // utilisation des dépendances dans les autres méthodes
    public function traitement() : array {
        $users = $this->userDao->getList();
        $buckets = $this->bucketBo->getFromUsers($users);
        return $buckets;
    }
}
  • Lignes 5 à 8 : Le constructeur de l'objet attend deux paramètres, de type UserDao et BucketBo. Lorsque l'objet MonObjet est automatiquement instancié par le loader, celui-ci va fournir au constructeur les paramètres qu'il attend, en les instanciant d'abord si nécessaire.
    Ici, on utilise la promotion de propriétés pour stocker ces paramètres dans des propriétés privées de l'objet.
  • Lignes 12 et 13 : Les propriétés privées sont utilisées.

2Données accessibles

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

Les connexions aux sources de données sont accessibles de deux manières différentes :

  • $loader->dataSources est un Registre qui contient les objets de connexion (voir la documentation des contrôleurs).
    Par exemple, une connexion à MySQL nommée db sera accessible avec $loader->dataSources->db.
  • Si le nom de la source de données n'est pas un nom déjà présent dans le composant (comme session, config, etc.), celle-ci est directement accessible depuis le composant.
    Par exemple, une connexion à MySQL nommée db sera accessible avec $loader->db.

3Accès alternatif

Dans l'exemple vu précédemment, le loader (en mode service locator) était utilisé en partant 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 fichiers sources sont 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);

Vous verrez plus bas comment simplifier l'écriture, en utilisant des alias et des préfixes.


4Stockage

Les données stockées dans le loader sont identifiées par un clé (la plupart du temps, il s'agit du type de l'objet). Si un caractère backslash (\) est présent au début de la clé, il est supprimé.

Ce traitement est là pour que les trois écritures suivantes soient équivalentes :

$res = $loader['\Math\Base\Calcul']->addition(3, 4);
$res = $loader['Math\Base\Calcul']->addition(3, 4);
$res = $loader[\Math\Base\Calcul::class]->addition(3, 4);

5Configuration du loader

5.1Préchargement

Il est possible de configurer le loader, pour lui fournir des valeurs qu'il n'aura pas à instancier lui-même (ou qu'il ne pourrait pas instancier).

Ainsi, dans le fichier etc/temma.php, vous pouvez définir des valeurs fixes, qui seront accessibles dans toute l'application :

<?php

return [
    'x-loader' => [
        'preload' => [
            // ajoute une instance de ZipArchive
            'zip_manager' => new ZipArchive(),

            // ajoute une chaîne de caractères récupérée depuis l'environnement
            'appPassword' => getenv('APP_PASSWORD'),

            // ajoute un nombre qui sera accessible de manière globale
            'maxArticles' => 100,
        ]
    ]
];

Ces valeurs seront directement utilisables par le loader.
Ainsi, il sera possible d'écrire ceci :

print("Nombre maximal d'articles : " . $this->_loader->maxArticles);

Une valeur enregistrée de cette manière sera aussi utilisable par le loader dans le cadre de l'autowiring.

Vous trouverez plus d'informations sur l'ajout explicite de données dans la section consacrée.


5.2Alias

Il est possible de définir des alias de nommage, qui seront utilisés lorsqu'un objet sera appelé via le loader.

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();
// est équivalent à
$list = $this->_loader['\MyApp\User\UserBo']->getList();

$this->_loader->TµEmail->textMail($from, $to, $title, $message);
// est équivalent à
$this->_loader['\Temma\Utils\Email']->textMail($from, $to, $title, $message);

Vous trouverez plus d'informations sur les alias dans la section consacrée.


5.3Préfixes

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.

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 :

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

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

$object = $this->_loader->€xUser;
// équivalent à
$object = $this->_loader['\Europa\Source\Bo\User'];

Vous trouverez plus d'informations sur les préfixes dans la section consacrée (plus bas).


6Autowiring : découplage

6.1Principe de l'autowiring

L'autowiring est la capacité du loader à détecter les dépendances qu'un objet attend dans son constructeur.

Ainsi, n'importe quel objet peut être appelé via le loader. Au premier appel, le loader va instancier l'objet, en lui passant ses dépendances en paramètre. Si le loader ne contient pas déjà les instances de ces dépendances, il les créera à la volée.

Voici un exemple de deux objets, l'un étant une dépendance de l'autre :

/**
 * Objet servant à calculer des clés à partir d'identifiants.
 */
class Hasher {
    /**
     * Méthode qui retourne un hash à partir d'une chaîne.
     * @param string  $texte  Texte d'entrée.
     * @return string  Hash calculé.
     */
    public function hash(string $texte) : string {
        return hash('sha256', $texte);
    }
}

/**
 * Objet gérant les utilisateurs en base de données.
 */
class UserDao {
    /**
     * Constructeur.
     * @param \Temma\Datasources\Redis  $ndb     Connexion à la base de données.
     * @param Hasher                    $hasher  Objet de hashage.
     */
    public function __construct(
        private \Temma\Datasources\Redis $ndb,
        private Hasher $hasher,
    ) {
    }

    /**
     * Retourne les informations sur un utilisateur.
     * @param  int  $id  Identifiant de l'utilisateur.
     * @return array  Tableau associatif.
     */
    public function get(int $id) : array {
        $user = $this->ndb["user-$id"];
        $user['hash'] = $this->hasher->hash($user['email']);
        return $user;
    }
}
  • Lignes 4 à 13 : Objet utilitaire Hasher, qui n'a aucune dépendance.
  • Lignes 18 à 40 : Objet UserDao.
    • Lignes 23 à 27 : Constructeur, qui attend deux dépendances.
    • Ligne 35 : Utilisation de la connexion à la base Redis.
    • Ligne 36 : Utilisation de l'objet Hasher.

Et voici le code du contrôleur qui fait appel à l'objet UserDao :

class Account extends \Temma\Web\Controller {
    public function show(int $id) {
        $this['user'] = $this->_loader->UserDao->get($id);
    }
}
  • Ligne 3 : Utilisation du loader pour appeler l'objet UserDao. Une instance est créée à la volée, et pour la créer, le loader utilise la connexion existante sur la base Redis, et crée une instance de l'objet Hasher.

6.2Gestion des dépendances

Lorsque le loader instancie un objet automatiquement, il parcourt les paramètres attendus par le constructeur.

Pour un paramètre donné (par exemple \App\UserManager $userMgt), il y a trois cas de figure :

  • Si le loader contient déjà une entrée au nom du type (par exemple \App\UserManager), elle est utilisée.
  • S'il y a une entrée au nom du paramètre (par exemple $userMgt), elle est utilisée.
  • Si le type du paramètre (par exemple \App\UserManager) est instanciable, le loader va créer une instance et l'utiliser.

Une dépendance peut ne pas être un objet instanciable. Par exemple, si le loader contient une chaîne de caractères name, et qu'un constructeur contient un paramètre string $name, le loader utilisera cette valeur.


7Service locator : optimisation des performances

Si l'autowiring est très simple et pratique à utiliser, il présente l'inconvénient d'être coûteux en performances.

Pour une application dont les performances sont critiques, vous pouvez prévilégier l'approche service locator.
Dans ce cas, un objet ne va pas recevoir ses dépendances en paramètre de son constructeur. Il recevra le loader, qu'il utilisera ensuite directement pour accéder aux objets dont il aura besoin.

Pour cela, il implémentera l'interface \Temma\Base\Loadable, qui impose que son constructeur ne prenne qu'un seul paramètre, de type \Temma\Base\Loader.

Voici l'exemple d'un objet qui utilise le loader pour accéder à ses dépendances :

class LogSpecial implements \Temma\Base\Loadable {
    /** Constructeur. */
    public function __construct(private \Temma\Base\Loader $loader) {
    }

    /**
     * Méthode qui ajoute des lignes dans le fichier 'var/liste.txt'.
     * @param string  $texte  Le texte à écrire.
     */
    public function ecrit(string $texte) : void {
        $dest = $this->loader->config->varPath . '/liste.txt';
        file_put_contents($dest, "$texte\n", FILE_APPEND);
    }

    /**
     * Méthode qui utilise l'objet AutreObjet.
     */
    public function traitement() : void {
        $valeur = $this->loader->AutreObjet->faisUnCalcul();
        $this->ecrit($valeur);
    }
}
  • Ligne 1 : L'objet implémente l'interface \Temma\Base\Loadable.
  • Ligne 3 : Constructeur de l'objet, qui reçoit en paramètre ue instance du composant d'injection de dépendances. Il est stocké comme propriété privée.
  • Ligne 11 : Dans la méthode ecrit(), on appelle $this->loader->config pour accéder à l'objet de configuration (et la propriété varPath de celui-ci).
  • Ligne 19 : Dans la méthode traitement(), on appelle $this->loader->AutreObjet. Si le loader possédait déjà une instance de AutreObjet, elle est utilisée ; sinon, il crée une nouvelle instance et la retourne.
    AutreObjet peut utiliser de l'autowiring ou implémenter l'interface \Temma\Base\Loadable.

Pour que cet objet soit chargeable par le loader, 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é LogSpecial.php, placé dans le répertoire lib/ du projet.

À partir de ce moment-là, l'objet est disponible directement comme s'il était une propriété du loader. 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 LogSpecial grâce au loader :

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

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

class Calcul implements \Temma\Base\Loadable {
    /** Constructeur. */
    public function __construct(private \Temma\Base\Loader $loader) {
    }

    /**
     * Fonction qui effectue une addition.
     * @param int  $i  Premier opérande.
     * @param int  $j  Second opérande.
     * @return int   Résultat de l'addition.
     */
    public function addition(int $i, int $j) : int {
        $resultat = $i + $j;
        $this->_loader->LogSpecial->ecrit("Calculé : $resultat");
        return ($resultat);
    }
}
  • Ligne 3 : Constructeur de l'objet, qui stocke le loader en propriété privée.
  • Ligne 14 : On utilise le loader pour appeler l'objet LogSpecial.

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.


8Ajouts de données dans le loader

8.1Ajouts explicites

Vous avez la possibilité de créer vous-même l'objet et de l'ajouter dans le loader 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);

Il est ensuite possible de récupérer cet objet auprès du loader (utilisé comme service locator) :

// 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);

Cela fonctionne aussi avec l'autowiring (qui se base ici sur le nom calculatrice et non pas sur le type) :

class MonObjet {
    // constructeur : on reçoit la dépendance préalablement ajoutée dans le loader
    public function __construct(private \Math\Base\Calcul $calculatrice) {
    }

    public function traitement(int $i, int $j) : int {
        // on utilise l'objet privé
        return $this->calculatrice->addition($i, $j);
    }
}

Vous pouvez ajouter n'importe quel élément dans le loader (pas uniquement des objets) :

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

8.2Ajouts par callback

Il est aussi possible d'assigner une fonction anonyme. Cette fonction sera exécutée lors du premier appel au loader ; la valeur qu'elle retourne sera ensuite gardée par le loader, et la fonction anonyme ne sera plus jamais appelée.

La fonction anonyme reçoit en paramètre l'instance du loader. Elle doit retourner la valeur qui écrasera la fonction anonmye dans le loader, et qui sera retournée par le loader pour chaque nouvel appel pour la même clé.

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']).

Voici un exemple de contrôleur :

class Homepage extends \Temma\Web\Controller {
    // initialisation
    public function __wakeup() {
        // ajout de la clé 'calc' associée à une fonction anonyme
        $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:

        // la fonction anonmye est exécutée pour générer la clé 'calc'
        $this['res'] = $this->_loader->calc->addition(3, 4);

        // la valeur de 'calc' est directement récupérée
        $this['zzz'] = $this->_loader->calc->addition(5, 6);
    }

    // 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 5 : On crée la clé calc dans le loader, en y affectant une fonction anonyme. Cette fonction prend un seul paramètre, qui recevra l'instance du loader. La fonction doit retourner l'objet qui sera retourné par la suite.
  • Lignes 6 à 8 : 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 14 : On assigne à la variable de template param la valeur reçue dans le paramètre $type.
  • Ligne 17 : On utilise le loader sans avoir à se soucier de savoir quelle implémentation est utilisée (c'est la fonction anonyme qui choisit l'objet qui sera instancié).
  • Ligne 19 : On utilise le loader pour récupérer la même instance qu'à la ligne précédente.
  • Ligne 24 : On utilise le loader. Ici, ce sera toujours l'objet \Math\Other\Calcul qui sera utilisé.

8.3Ajouts par callback dynamique

Avec l'ajout par callback (vu au chapitre précédent), la fonction anonyme est exécutée une seule fois, et la valeur qu'elle retourne est gardée par le loader pour remplacer la fonction anonyme.

Mais parfois, on veut que la fonction anonyme s'exécute dynamiquement à chaque fois qu'on fait appel au loader, sans que la valeur retournée ne soit stockée.
Dans ce cas, on utilisera la méthode dynamic() du loader, en lui donnant la clé et une fonction anonyme :

// ajout d'une valeur dynamique dans le loader
$loader->dynamic('valeurHasardeuse', function() {
    return mt_rand(0, 255);
});

// on va afficher plusieurs fois la valeur, à chaque fois elle sera générée à nouveau
print($loader->valeurHasardeuse . "\n");
print($loader->valeurHasardeuse . "\n");
print($loader->valeurHasardeuse . "\n");
print($loader->valeurHasardeuse . "\n");

8.4Ajouts 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 loader.
Cette fonction prend deux paramètres : le premier est l'instance du loader ; 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);

8.5Ajouts de 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);

9Gestion des alias

9.1Définition d'alias

Comme vu plus haut, il est possible de définir des alias de nommage dans le fichier de configuration.

Vous pouvez aussi déclarer des alias directement dans le loader avec la méthode alias() :

// définition d'un alias
$this->_loader->alias('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 :

// définition d'un tableau d'alias
$this->_loader->alias([
    '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);

// est é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 à la méthode alias().


9.2Utilisation 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);

10Gestion des préfixes

10.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. 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.


10.2Définition de préfixe

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

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

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

// deuxième préfixe
$this->_loader->prefix('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->prefix('€x', '\Europa\Source\Bo');

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

Vous pouvez aussi déclarer plusieurs préfixes en passant un tableau associatif :

// définition d'un tableau de préfixes
$this->_loader->prefix([
    '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.


10.3Gestion des préfixes par callback

Quand on définit un préfixe (que ce soit par la méthode prefix() 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;
// est équivalent à
$computer = $this->_loader['\Sinclair\Computers\ZxSpectrum'];

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

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

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']).


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