> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Attributs contextuels et résolution des dépendances dans un package

> Analysez les ContextualAttribute de Laravel 13 pour comprendre les attributs qui injectent la configuration d'un package, l'ordre de priorité de la résolution, les différences entre make et call, ainsi que les tests et la maintenance des services à longue durée de vie.

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.

| Méthode | Où la configurer | Usage adapté |
| - | - | - |
| `when()->needs()->give()` | Service provider | Changer une dépendance côté application sans modifier la classe consommatrice |
| Attributs intégrés `Config`, `Storage`, `Tag` | Paramètres de la classe consommatrice | Déclarer une méthode de résolution fournie par Laravel |
| Attribut contextuel propre au package | Paramètres de la classe consommatrice et classe d'attribut | Résoudre une valeur selon les conventions propres au package |

Pour les liaisons classiques et les attributs intégrés, consultez [Service container](https://kawax.biz/fr/service-container.md). 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.

```php theme={null}
<?php

return [
    'timeout' => 30,
    'retries' => 3,
];
```

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](https://kawax.biz/fr/advanced/package-discovery.md), et pour la publication et le cache de configuration, consultez [Fusion et cache de la configuration des packages](https://kawax.biz/fr/advanced/package-config-merging.md).

```php theme={null}
<?php

namespace Acme\Courier;

use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->mergeConfigFrom(
            __DIR__.'/../config/courier.php', 'courier'
        );
    }
}
```

### Implémenter `ContextualAttribute`

Dans `src/Attributes/CourierConfig.php`, définissez un attribut qui préfixe la clé de configuration par `courier.`.

```php theme={null}
<?php

namespace Acme\Courier\Attributes;

use Attribute;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Container\ContextualAttribute;
use ReflectionParameter;

#[Attribute(Attribute::TARGET_PARAMETER)]
final class CourierConfig implements ContextualAttribute
{
    public function __construct(
        public ?string $key = null,
        public mixed $default = null,
    ) {}

    public static function resolve(
        self $attribute,
        Container $container,
        ReflectionParameter $parameter,
    ): mixed {
        $key = $attribute->key ?? $parameter->getName();

        return $container->make('config')->get(
            'courier.'.$key, $attribute->default
        );
    }
}
```

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.

> [!WARNING]
>
> 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`.

```php theme={null}
<?php

namespace Acme\Courier;

use Acme\Courier\Attributes\CourierConfig;

final class CourierOptions
{
    public function __construct(
        #[CourierConfig('timeout', default: 30)] public int $timeout,
        #[CourierConfig('retries', default: 3)] public int $retries,
    ) {}
}
```

Si l'application consommatrice surcharge la configuration, cette valeur est injectée lors de la résolution suivante.

```php theme={null}
use Acme\Courier\CourierOptions;

config(['courier.timeout' => 45]);

$options = app()->make(CourierOptions::class);
```

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.

```mermaid theme={null}
flowchart TD
    A["Paramètre du constructeur"] --> B{"Surchargé par nom de paramètre ?"}
    B -->|Yes| C["Utiliser la valeur de surcharge"]
    B -->|No| D{"ContextualAttribute présent ?"}
    D -->|Yes| E["Appeler le resolver de l'attribut"]
    E --> F{"Résultat null ?"}
    F -->|No| G["Utiliser la valeur de l'attribut"]
    F -->|Yes| H["Résolution classique par type / primitive"]
    D -->|No| H
    G --> I["Post-traitement de l'attribut"]
    H --> I
    C --> J["Transmettre au constructeur"]
    I --> J
```

Les clés de `makeWith()` sont les noms des paramètres de la classe consommatrice, et non les clés de configuration.

```php theme={null}
use Acme\Courier\CourierOptions;

$options = app()->makeWith(CourierOptions::class, [
    'timeout' => 5,
]);
```

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.

```php theme={null}
use Acme\Courier\Attributes\CourierConfig;

final class NullableTimeoutOptions
{
    public function __construct(
        #[CourierConfig('missing')] public ?int $timeout = 99,
    ) {}
}

$readTimeout = function (
    #[CourierConfig('missing')] ?int $timeout = 99,
): ?int {
    return $timeout;
};
```

| Appel | Résultat | Raison |
| - | - | - |
| `app()->make(NullableTimeoutOptions::class)->timeout` | `99` | L'attribut renvoie `null` et la résolution passe à la valeur par défaut de la primitive |
| `app()->call($readTimeout)` | `null` | Le résultat de l'attribut est transmis tel quel |
| `$readTimeout()` | `99` | Il s'agit d'un appel PHP classique, qui ne résout pas les attributs |

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

> [!NOTE]
>
> 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](https://kawax.biz/fr/advanced/package-service-lifetimes.md). 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.

```php theme={null}
use Acme\Courier\Attributes\CourierConfig;
use Acme\Courier\CourierOptions;
use Illuminate\Config\Repository;
use Illuminate\Container\Container;
use Illuminate\Contracts\Container\Container as ContainerContract;
use PHPUnit\Framework\TestCase;

class CourierConfigTest extends TestCase
{
    private function container(array $settings = []): Container
    {
        $container = new Container;
        $container->instance('config', new Repository([
            'courier' => $settings,
        ]));

        return $container;
    }

    public function testConfigDefaultsAndNamedOverrides(): void
    {
        $container = $this->container();
        $this->assertSame(30, $container->make(CourierOptions::class)->timeout);

        $container->make('config')->set('courier.timeout', 45);
        $container->when(CourierOptions::class)->needs('$timeout')->give(77);
        $this->assertSame(45, $container->make(CourierOptions::class)->timeout);

        $options = $container->makeWith(CourierOptions::class, ['timeout' => 5]);
        $this->assertSame(5, $options->timeout);
        $this->assertSame(3, $options->retries);
    }

    public function testParameterNameIsAvailableToTheResolver(): void
    {
        $container = $this->container(['timeout' => 45]);

        $value = $container->call(function (
            #[CourierConfig(default: 30)] int $timeout,
        ): int {
            return $timeout;
        });

        $this->assertSame(45, $value);
    }

    public function testRegisteredHandlerOverridesResolve(): void
    {
        $container = $this->container(['timeout' => 45]);
        $container->whenHasAttribute(
            CourierConfig::class,
            static fn (CourierConfig $attribute, ContainerContract $app) => 7,
        );

        $this->assertSame(7, $container->make(CourierOptions::class)->timeout);
    }

    public function testNullDiffersBetweenMakeAndCall(): void
    {
        $container = $this->container();
        $readTimeout = function (
            #[CourierConfig('missing')] ?int $timeout = 99,
        ): ?int {
            return $timeout;
        };

        $this->assertSame(99, $container->make(NullableTimeoutOptions::class)->timeout);
        $this->assertNull($container->call($readTimeout));
        $this->assertSame(99, $readTimeout());
    }

    public function testSingletonKeepsItsResolvedValues(): void
    {
        $container = $this->container(['timeout' => 45]);
        $container->singleton(CourierOptions::class);
        $options = $container->make(CourierOptions::class);

        $container->make('config')->set('courier.timeout', 60);

        $this->assertSame($options, $container->make(CourierOptions::class));
        $this->assertSame(45, $options->timeout);
    }
}
```

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](https://kawax.biz/fr/advanced/package-testing.md), 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

> [card] [Fusion et cache de la configuration des packages](https://kawax.biz/fr/advanced/package-config-merging.md)
>
> Vérifiez la configuration par défaut, la configuration publiée et les limites de mise à jour du cache.

> [card] [Conception du cycle de vie des services d'un package](https://kawax.biz/fr/advanced/package-service-lifetimes.md)
>
> Choisissez la durée de vie des services qui conservent les valeurs injectées.

> [card] [Tags de conteneur et agrégation de plugins d'un package](https://kawax.biz/fr/advanced/package-container-tags.md)
>
> Découvrez l'attribut intégré Tag et l'agrégation des implémentations enregistrées.

> [card] [API PHP Reflection](https://kawax.biz/fr/advanced/php-reflection.md)
>
> 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](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/container.md#defining-custom-attributes)
* [Documentation officielle de Laravel : configuration des packages](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/packages.md#configuration)
* [Container : resolveDependencies(), resolveFromAttribute(), whenHasAttribute() et post-traitement des attributs](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Container/Container.php)
* [BoundMethod : résolution des arguments des callables](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Container/BoundMethod.php)
* [Util : détection des ContextualAttribute par Reflection](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Container/Util.php)
* [ContextualAttribute : interface marqueur](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Contracts/Container/ContextualAttribute.php)
* [Config : attribut de configuration intégré](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Container/Attributes/Config.php)
* [Config Repository : get() et récupération des valeurs de configuration](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Config/Repository.php)
* [Arr : valeur par défaut de get() et traitement d'un null existant](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Collections/Arr.php)
* [Tests officiels : resolver d'attribut, handler enregistré, ReflectionParameter et résolution via call](https://github.com/laravel/framework/blob/v13.39.0/tests/Container/ContextualAttributeBindingTest.php)


## Related topics

- [Enregistrement des commandes Artisan d'un package et résolution des dépendances](https://kawax.biz/fr/advanced/package-commands.md)
- [Enregistrer et tester des transports de mail dans un package](https://kawax.biz/fr/advanced/package-mail-transports.md)
- [Enregistrer des drivers Storage dans un package et les résoudre à nouveau](https://kawax.biz/fr/advanced/package-filesystem-drivers.md)
- [Tags de conteneur et agrégation de plugins d'un package](https://kawax.biz/fr/advanced/package-container-tags.md)
- [Service container](https://kawax.biz/fr/service-container.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.