v13.39.0 et implémente un attribut personnalisé qui lit la configuration courier. Si l’attribut intégré #[Config('courier.timeout')] suffit à vos besoins, inutile de multiplier les attributs personnalisés. Envisagez-les lorsque vous voulez proposer, dans l’API du package, vos propres conventions de clés ou votre propre stratégie de résolution.
Le rôle de l’attribut est de déterminer la valeur injectée
Un attribut qui implémenteIlluminate\Contracts\Container\ContextualAttribute est lu par le conteneur de Laravel lors de la résolution des paramètres. Ce n’est pas l’objet attribut lui-même qui est transmis à la classe consommatrice, mais la valeur renvoyée par le resolver.
Pour les liaisons classiques et les attributs intégrés, consultez Service container. Les attributs contextuels sont un mécanisme distinct des attributs qui déclarent la liaison d’une classe ou d’une interface, comme
Bind, Singleton ou Scoped.
Créer un attribut qui lit la configuration du package
Enregistrer la configuration par défaut
Commencez par préparer les valeurs par défaut dans le fichierconfig/courier.php du package.
register() de src/CourierServiceProvider.php. Pour l’enregistrement automatique du provider, consultez Fonctionnement interne de la découverte automatique des packages, et pour la publication et le cache de configuration, consultez Fusion et cache de la configuration des packages.
Implémenter ContextualAttribute
Dans src/Attributes/CourierConfig.php, définissez un attribut qui préfixe la clé de configuration par courier..
- L’instance de l’attribut attaché au paramètre.
- Le conteneur qui résout la dépendance.
- Le
ReflectionParameterreprésentant la cible de la résolution.
#[CourierConfig(default: 30)] int $timeout fait référence à courier.timeout. Si vous voulez lire la même configuration même lorsque le nom du paramètre change côté consommateur, indiquez la clé explicitement, comme dans #[CourierConfig('timeout', default: 30)].
Au moment de la vérification, ContextualAttribute est une interface marqueur qui ne déclare aucune méthode. Le seul implements ne garantit donc ni l’existence ni la signature du resolver. Si vous fournissez resolve() comme ci-dessus, il n’est pas nécessaire d’enregistrer un bind() classique pour cet attribut.
Générer la classe consommatrice depuis le conteneur
Ajoutez les attributs danssrc/CourierOptions.php.
$options->timeout vaut 45 et $options->retries vaut 3. L’attribut se contente de récupérer la configuration et n’effectue aucune validation métier, par exemple vérifier qu’il s’agit d’un entier positif ou que la valeur ne dépasse pas un maximum autorisé. Les déclarations de type ne remplacent pas non plus la validation des valeurs de configuration.
Lire l’ordre de résolution dans le constructeur
Container::resolveDependencies() détermine la valeur de chaque paramètre dans l’ordre suivant.
- Utiliser la valeur de surcharge correspondant au nom du paramètre, transmise par exemple via
makeWith(). - Rechercher et résoudre un attribut contextuel avec
Util::getContextualAttributeFromDependency(). - Si le résultat de l’attribut est
null, passer à la résolution classique de la valeur primitive ou de la classe. - Après le post-traitement de l’attribut, transmettre la valeur déterminée au constructeur.
makeWith() sont les noms des paramètres de la classe consommatrice, et non les clés de configuration.
timeout vaut 5 quelle que soit la configuration. En revanche, si l’attribut renvoie une valeur autre que null, son résultat est prioritaire sur un when()->needs('$timeout')->give(...) classique. Testez aussi la possibilité que l’ajout d’un attribut au package modifie les liaisons contextuelles des utilisateurs existants.
Util récupère par Reflection les attributs qui implémentent ContextualAttribute et sélectionne le premier d’entre eux pour la résolution. Si vous alignez plusieurs attributs contextuels différents, leurs resolvers ne sont pas composés successivement.
Un handler enregistré est prioritaire sur resolve()
Container::resolveFromAttribute() recherche d’abord, avec le nom de la classe d’attribut comme clé, un handler enregistré via whenHasAttribute(). À défaut, il utilise la méthode resolve() de l’attribut, et si aucun des deux n’existe, il lève une BindingResolutionException.
whenHasAttribute() se configure par type d’attribut, et non par classe consommatrice. Un nouvel enregistrement pour la même classe d’attribut écrase le handler précédent. Si un package modifie sans condition le handler d’un autre package ou d’un attribut intégré de Laravel, cela affecte l’ensemble de l’application consommatrice. Placez le comportement standard de votre attribut dans resolve() et traitez la modification par handler comme un point d’extension explicite.
make(), call() et l’appel direct ne sont pas équivalents
app()->call() résout aussi les attributs, mais le chemin classique de BoundMethod ajoute directement le résultat de l’attribut aux arguments dès qu’il en trouve un. Contrairement à la résolution du constructeur, il ne revient pas à la valeur par défaut même si le résultat de l’attribut est null.
Voici un exemple qui lit la clé inexistante courier.missing, en l’absence de toute autre liaison contextuelle.
new CourierOptions(timeout: 5, retries: 1) est également une instanciation PHP classique. Le simple ajout d’un attribut n’ajoute pas d’injection de dépendances à un new direct ni à un appel de méthode.
Si vous avez besoin du même comportement par défaut pour le constructeur et pour les callables, ne vous fiez pas uniquement à la valeur par défaut du paramètre : définissez une valeur par défaut côté attribut, comme dans
#[CourierConfig('timeout', default: 30)]. Ce default est utilisé lorsque la clé de configuration n’existe pas ; il ne remplace pas la valeur d’une clé existante qui vaut null. Pour une API qui traite null comme une valeur significative, vérifiez séparément les deux chemins de résolution.Séparer la réinjection des valeurs et la durée de vie des services
L’attribut lit la configuration au moment où le paramètre est résolu. Ce n’est pas une fonctionnalité qui surveille ou met à jour les propriétés d’unCourierOptions déjà généré.
Par exemple, si vous faites de CourierOptions un singleton, les valeurs injectées la première fois sont conservées dans cette instance. Même si vous modifiez ensuite la configuration et appelez un make() classique, les valeurs de cette même instance ne changent pas.
Même lorsque vous créez un attribut qui lit des informations propres à chaque requête, ce n’est pas l’attribut lui-même qui détermine une durée de vie singleton ou scoped. Choisissez la durée de vie du service qui conserve les valeurs ou objets injectés en suivant Conception du cycle de vie des services d’un package. Distinguez également la régénération du cache de configuration de la mise à jour des processus à longue durée de vie et des services déjà générés.
Tester les chemins de résolution
Comme dans les tests officiels, vous pouvez écrire les tests unitaires de l’attribut avec un nouveauContainer et un Illuminate\Config\Repository. L’exemple PHPUnit suivant suppose un environnement de test qui charge CourierConfig, CourierOptions et NullableTimeoutOptions définis plus haut.
7 sert uniquement à tester l’ordre de priorité. Dans ce conteneur, il affecte toutes les résolutions du même attribut, y compris retries.
Ces tests unitaires seuls ne permettent pas de vérifier l’intégration avec l’enregistrement du provider du package, mergeConfigFrom() ou le cache de configuration. Vérifiez également, dans l’environnement applicatif décrit dans Tester des packages Laravel avec Orchestra Testbench, les états avant publication, après publication et avec cache de configuration.
Points à vérifier avant la publication
- Traiter le nom de classe de l’attribut, les noms des paramètres de son constructeur, les clés de configuration et le type de retour comme une API publique. Faire aussi attention aux modifications de
keyetdefault, utilisés comme arguments nommés. - Si la spécification utilise le nom du paramètre lorsque la clé est omise, vérifier qu’un renommage du paramètre dans la classe consommatrice ne change pas la configuration référencée.
- Tester séparément les liaisons existantes sans attribut, la résolution avec attribut et la surcharge par nom de paramètre.
- Tester
null, une configuration manquante et un type inapproprié, à la fois avecmake()et aveccall(). - Vérifier que la conception ne fait pas conserver de valeurs obsolètes aux singletons ni aux workers à longue durée de vie.
- Lors de la mise à jour des versions de Laravel prises en charge, revérifier les arguments du resolver, l’ordre de priorité et le post-traitement des attributs dans le code source et les tests officiels.
Pages associées
Fusion et cache de la configuration des packages
Vérifiez la configuration par défaut, la configuration publiée et les limites de mise à jour du cache.
Conception du cycle de vie des services d'un package
Choisissez la durée de vie des services qui conservent les valeurs injectées.
Tags de conteneur et agrégation de plugins d'un package
Découvrez l’attribut intégré Tag et l’agrégation des implémentations enregistrées.
API PHP Reflection
Découvrez le mécanisme de lecture des métadonnées des attributs et des paramètres.
Sources primaires consultées
- Documentation officielle de Laravel : Defining custom attributes
- Documentation officielle de Laravel : configuration des packages
- Container : resolveDependencies(), resolveFromAttribute(), whenHasAttribute() et post-traitement des attributs
- BoundMethod : résolution des arguments des callables
- Util : détection des ContextualAttribute par Reflection
- ContextualAttribute : interface marqueur
- Config : attribut de configuration intégré
- Config Repository : get() et récupération des valeurs de configuration
- Arr : valeur par défaut de get() et traitement d’un null existant
- Tests officiels : resolver d’attribut, handler enregistré, ReflectionParameter et résolution via call