Passer au contenu principal
Il arrive que vous souhaitiez permettre aux utilisateurs de votre package d’injecter les valeurs dont ils ont besoin sans aligner des clés de configuration dans un service provider. En exposant un attribut contextuel, vous pouvez résoudre une configuration ou un service propre au package à partir des métadonnées attachées à un paramètre. Cette page s’appuie sur le code source officiel de Laravel Framework 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émente Illuminate\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 fichier config/courier.php du package.
Fusionnez la configuration dans la méthode 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..
Dans l’implémentation examinée, le resolver reçoit les trois arguments suivants.
  1. L’instance de l’attribut attaché au paramètre.
  2. Le conteneur qui résout la dépendance.
  3. Le ReflectionParameter représentant la cible de la résolution.
Dans cet exemple, si la clé est omise, le nom du paramètre est utilisé. #[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.
Utilisez le constructeur de l’attribut uniquement pour conserver des métadonnées. Le conteneur appelle newInstance() séparément lors de la résolution et lors du post-traitement de l’attribut : ce n’est donc pas l’endroit pour une « initialisation exécutée une seule fois ». N’y placez ni communication externe ni état mutable.

Générer la classe consommatrice depuis le conteneur

Ajoutez les attributs dans src/CourierOptions.php.
Si l’application consommatrice surcharge la configuration, cette valeur est injectée lors de la résolution suivante.
Dans cet exemple, $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.
  1. Utiliser la valeur de surcharge correspondant au nom du paramètre, transmise par exemple via makeWith().
  2. Rechercher et résoudre un attribut contextuel avec Util::getContextualAttributeFromDependency().
  3. Si le résultat de l’attribut est null, passer à la résolution classique de la valeur primitive ou de la classe.
  4. Après le post-traitement de l’attribut, transmettre la valeur déterminée au constructeur.
Le chemin de surcharge de l’étape 1 ignore la résolution de l’attribut et son post-traitement pour ce paramètre. Les clés de makeWith() sont les noms des paramètres de la classe consommatrice, et non les clés de configuration.
Dans ce cas, 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’un CourierOptions 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 nouveau Container et un Illuminate\Config\Repository. L’exemple PHPUnit suivant suppose un environnement de test qui charge CourierConfig, CourierOptions et NullableTimeoutOptions définis plus haut.
L’exemple où le handler renvoie toujours 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 key et default, 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 avec make() et avec call().
  • 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

Dernière modification le 31 octobre 2026