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

# Contextuele attributen en dependency-resolutie in packages

> Ontleed ContextualAttribute in Laravel 13 en leer over attributen die packageconfiguratie injecteren, de volgorde van resolutie, het verschil tussen make en call, en het testen en onderhouden van langlevende services.

Soms wil je dat gebruikers van je package de benodigde waarden kunnen laten injecteren zonder configuratiesleutels in een serviceprovider op te sommen. Door contextuele attributen aan te bieden, kun je packagespecifieke configuratie of services resolven op basis van metadata die aan een parameter is gekoppeld.

Op deze pagina bekijken we de officiële broncode van Laravel Framework `v13.39.0` en implementeren we een eigen attribuut dat de `courier`-configuratie leest. Als het ingebouwde `#[Config('courier.timeout')]` al volstaat, hoef je geen extra eigen attributen te maken. Overweeg dit pas wanneer je eigen sleutelconventies of een eigen resolutiebeleid als API van je package wilt aanbieden.

## De rol van een attribuut is bepalen welke waarde wordt geïnjecteerd

Attributen die `Illuminate\Contracts\Container\ContextualAttribute` implementeren, worden door de Laravel-container gelezen wanneer die parameters resolvet. Niet het attribuutobject zelf, maar de waarde die de resolver teruggeeft, wordt aan de gebruikende klasse doorgegeven.

| Methode | Waar je het instelt | Geschikt voor |
| - | - | - |
| `when()->needs()->give()` | Serviceprovider | Afhankelijkheden aan de applicatiekant wisselen zonder de gebruikende klasse te wijzigen |
| Ingebouwde attributen `Config`, `Storage` en `Tag` | Parameters van de gebruikende klasse | Een door Laravel geleverde resolutiemethode declareren |
| Eigen contextuele attributen van het package | Parameters van de gebruikende klasse en de attribuutklasse | Waarden resolven volgens packagespecifieke conventies |

Zie [Service container](https://kawax.biz/nl/service-container.md) voor gewone bindings en ingebouwde attributen. Contextuele attributen zijn een ander mechanisme dan attributen als `Bind`, `Singleton` en `Scoped`, die bindings voor klassen of interfaces declareren.

## Een attribuut maken dat packageconfiguratie leest

### Standaardconfiguratie registreren

Zet eerst standaardwaarden klaar in `config/courier.php` van het package.

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

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

Voeg de configuratie samen in `register()` van `src/CourierServiceProvider.php`. Zie [De interne structuur van package discovery](https://kawax.biz/nl/advanced/package-discovery.md) voor het automatisch registreren van providers, en [Packageconfiguratie samenvoegen en cachen](https://kawax.biz/nl/advanced/package-config-merging.md) voor publiceren en de configuratiecache.

```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'
        );
    }
}
```

### `ContextualAttribute` implementeren

Definieer in `src/Attributes/CourierConfig.php` een attribuut dat `courier.` voor de configuratiesleutel plaatst.

```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
        );
    }
}
```

In de gecontroleerde implementatie krijgt de resolver de volgende drie argumenten:

1. De instantie van het attribuut dat aan de parameter is gekoppeld.
2. De container die de afhankelijkheid resolvet.
3. Een `ReflectionParameter` die het te resolven doel voorstelt.

In dit voorbeeld wordt de parameternaam gebruikt als de sleutel wordt weggelaten. `#[CourierConfig(default: 30)] int $timeout` verwijst naar `courier.timeout`. Wil je dezelfde configuratie lezen, ook als de parameternaam aan de gebruikende kant verandert, geef de sleutel dan expliciet op, zoals `#[CourierConfig('timeout', default: 30)]`.

`ContextualAttribute` is op het moment van controle een marker-interface die geen methoden declareert. `implements` alleen garandeert dus niet dat er een resolver bestaat of welke signatuur die heeft. Als je zoals hierboven een `resolve()` voorziet, hoef je voor dit attribuut geen gewone `bind()` te registreren.

> [!WARNING]
>
> Gebruik de constructor van het attribuut alleen om metadata vast te houden. De container roept `newInstance()` afzonderlijk aan bij het resolven en bij de nabewerking van het attribuut, dus het is geen plek voor "initialisatie die maar één keer draait". Laat het attribuut geen externe communicatie doen of veranderlijke state vasthouden.

### De gebruikende klasse via de container aanmaken

Voeg het attribuut toe in `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,
    ) {}
}
```

Als de gebruikende applicatie de configuratie overschrijft, wordt die waarde bij de volgende resolutie geïnjecteerd.

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

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

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

In dit voorbeeld is `$options->timeout` gelijk aan `45` en `$options->retries` gelijk aan `3`. Het attribuut haalt alleen de configuratie op en voert geen bedrijfsmatige validatie uit, zoals of het een positief geheel getal is of binnen het toegestane maximum valt. Ook typedeclaraties vervangen geen validatie van configuratiewaarden.

## De resolutievolgorde in de constructor lezen

`Container::resolveDependencies()` bepaalt de waarde van elke parameter in de volgende volgorde:

1. Gebruik een overschrijfwaarde die bijvoorbeeld via `makeWith()` is meegegeven en overeenkomt met de parameternaam.
2. Zoek met `Util::getContextualAttributeFromDependency()` naar een contextueel attribuut en resolve het.
3. Als het resultaat van het attribuut `null` is, ga verder met de gewone resolutie van primitieve waarden of klassen.
4. Geef na de nabewerking van het attribuut de bepaalde waarde door aan de constructor.

Het overschrijfpad uit stap 1 slaat de attribuutresolutie en de nabewerking voor die parameter over.

```mermaid theme={null}
flowchart TD
    A["Constructorparameter"] --> B{"Overschreven via parameternaam?"}
    B -->|Yes| C["Overschrijfwaarde gebruiken"]
    B -->|No| D{"Is er een ContextualAttribute?"}
    D -->|Yes| E["Resolver van het attribuut aanroepen"]
    E --> F{"Is het resultaat null?"}
    F -->|No| G["Waarde van het attribuut gebruiken"]
    F -->|Yes| H["Gewone resolutie van type of primitieve waarde"]
    D -->|No| H
    G --> I["Nabewerking van het attribuut"]
    H --> I
    C --> J["Doorgeven aan de constructor"]
    I --> J
```

De sleutels van `makeWith()` zijn parameternamen van de gebruikende klasse, geen configuratiesleutels.

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

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

In dit geval is `timeout` altijd `5`, ongeacht de configuratiewaarde. Geeft het attribuut daarentegen iets anders dan `null` terug, dan heeft het resultaat van het attribuut voorrang op een gewone `when()->needs('$timeout')->give(...)`. Test daarom ook of het toevoegen van een attribuut aan je package de contextuele bindings van bestaande gebruikers kan veranderen.

`Util` haalt via Reflection de attributen op die `ContextualAttribute` implementeren en kiest de eerste daarvan voor de resolutie. Ook als je meerdere verschillende contextuele attributen naast elkaar zet, worden hun resolvers niet na elkaar gecombineerd.

### Geregistreerde handlers hebben voorrang op `resolve()`

`Container::resolveFromAttribute()` zoekt eerst, met de klassenaam van het attribuut als sleutel, naar een handler die via `whenHasAttribute()` is geregistreerd. Is die er niet, dan wordt `resolve()` van het attribuut gebruikt, en als geen van beide bestaat, wordt een `BindingResolutionException` gegooid.

`whenHasAttribute()` is een instelling per attribuutsoort, niet per gebruikende klasse. Opnieuw registreren voor dezelfde attribuutklasse overschrijft de vorige handler. Als een package onvoorwaardelijk de handlers van andere packages of van ingebouwde Laravel-attributen wijzigt, raakt dat de hele gebruikende applicatie. Leg het standaardgedrag van je eigen attribuut vast in `resolve()` en behandel wijzigingen via handlers als een expliciet uitbreidingspunt.

## `make()`, `call()` en directe aanroepen zijn niet hetzelfde

Ook `app()->call()` resolvet attributen, maar het gewone `BoundMethod`-pad voegt het resultaat direct als argument toe zodra een attribuut wordt gevonden. Anders dan bij de constructorresolutie valt het niet terug op de resolutie van de standaardwaarde, ook niet als het resultaat van het attribuut `null` is.

Hier een voorbeeld dat de niet-bestaande `courier.missing` leest, zonder andere contextuele bindings.

```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;
};
```

| Aanroep | Resultaat | Reden |
| - | - | - |
| `app()->make(NullableTimeoutOptions::class)->timeout` | `99` | Het attribuut geeft `null` terug en de resolutie gaat verder naar de standaardwaarde van de primitieve parameter |
| `app()->call($readTimeout)` | `null` | Het resultaat van het attribuut wordt ongewijzigd doorgegeven |
| `$readTimeout()` | `99` | Een gewone PHP-aanroep die geen attributen resolvet |

Ook `new CourierOptions(timeout: 5, retries: 1)` is gewone aanmaak via PHP. Alleen een attribuut toevoegen voegt geen dependency injection toe aan een directe `new` of methodeaanroep.

> [!NOTE]
>
> Heb je zowel in constructors als in callables hetzelfde standaardgedrag nodig, vertrouw dan niet alleen op de standaardwaarde van de parameter, maar definieer de standaard aan de kant van het attribuut, zoals `#[CourierConfig('timeout', default: 30)]`. Deze `default` wordt gebruikt als de configuratiesleutel niet bestaat; hij vervangt niet de waarde van een bestaande sleutel die `null` is. Verifieer bij een API die `null` zelf als betekenisvolle waarde behandelt beide resolutiepaden afzonderlijk.

## Opnieuw injecteren van waarden en de levensduur van services scheiden

Een attribuut leest de configuratie op het moment dat de parameter wordt geresolved. Het is geen functie die de eigenschappen van een al aangemaakte `CourierOptions` bewaakt of bijwerkt.

Als je `CourierOptions` bijvoorbeeld als singleton registreert, houdt die instantie de waarden vast die als eerste zijn geïnjecteerd. Ook als je daarna de configuratie wijzigt en een gewone `make()` aanroept, veranderen de waarden van dezelfde instantie niet.

Ook wanneer je een attribuut maakt dat per-requestinformatie leest, bepaalt het attribuut zelf niet de singleton- of scoped-levensduur. Kies de levensduur van de service die de geïnjecteerde waarden of objecten vasthoudt volgens [Levensduur van packageservices ontwerpen](https://kawax.biz/nl/advanced/package-service-lifetimes.md). Beschouw het opnieuw opbouwen van de configuratiecache los van het bijwerken van langlevende processen of al aangemaakte services.

## De resolutiepaden testen

Tests voor het attribuut zelf kun je, net als de officiële tests, schrijven met een nieuwe `Container` en `Illuminate\Config\Repository`. Het volgende PHPUnit-voorbeeld gaat uit van een testomgeving waarin de hierboven gedefinieerde `CourierConfig`, `CourierOptions` en `NullableTimeoutOptions` zijn geladen.

```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);
    }
}
```

Het voorbeeld waarin de handler altijd `7` teruggeeft, is uitsluitend bedoeld om de voorrang te testen. Binnen deze container beïnvloedt het alle resoluties van hetzelfde attribuut, inclusief `retries`.

Met deze unittests alleen controleer je niet de integratie met de providerregistratie van het package, `mergeConfigFrom()` of de configuratiecache. Verifieer ook in de applicatieomgeving van [Laravel-packages testen met Orchestra Testbench](https://kawax.biz/nl/advanced/package-testing.md) de toestanden vóór publicatie, na publicatie en met configuratiecache.

### Controlepunten vóór een release

* Behandel de klassenaam van het attribuut, de namen van de constructorparameters, de configuratiesleutels en het returntype als publieke API. Let ook op wijzigingen in `key` en `default`, die als benoemde argumenten worden gebruikt.
* Als de parameternaam wordt gebruikt wanneer de sleutel ontbreekt, controleer dan of het hernoemen van een parameter in de gebruikende klasse niet verandert welke configuratie wordt gelezen.
* Test afzonderlijk bestaande bindings zonder attribuut, resolutie met attribuut en overschrijven via de parameternaam.
* Test `null`, ontbrekende configuratie en ongeschikte types zowel met `make()` als met `call()`.
* Controleer dat het ontwerp singletons en langlevende workers geen verouderde waarden laat vasthouden.
* Controleer bij het bijwerken van de ondersteunde Laravel-versie opnieuw de argumenten van de resolver, de voorrangsvolgorde en de nabewerking van attributen aan de hand van de officiële broncode en tests.

## Gerelateerde pagina's

> [card] [Packageconfiguratie samenvoegen en cachen](https://kawax.biz/nl/advanced/package-config-merging.md)
>
> Bekijk de grenzen tussen standaardconfiguratie, gepubliceerde configuratie en het bijwerken van de cache.

> [card] [Levensduur van packageservices ontwerpen](https://kawax.biz/nl/advanced/package-service-lifetimes.md)
>
> Kies de levensduur van services die geïnjecteerde waarden vasthouden.

> [card] [Containertags in packages en het bundelen van plugins](https://kawax.biz/nl/advanced/package-container-tags.md)
>
> Bekijk het ingebouwde Tag-attribuut en het bundelen van geregistreerde implementaties.

> [card] [PHP Reflection API](https://kawax.biz/nl/advanced/php-reflection.md)
>
> Bekijk hoe metadata van attributen en parameters wordt uitgelezen.

## Geraadpleegde primaire bronnen

* [Officiële Laravel-documentatie: Defining custom attributes](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/container.md#defining-custom-attributes)
* [Officiële Laravel-documentatie: Packageconfiguratie](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/packages.md#configuration)
* [Container: resolveDependencies(), resolveFromAttribute(), whenHasAttribute() en nabewerking van attributen](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Container/Container.php)
* [BoundMethod: argumentresolutie voor callables](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Container/BoundMethod.php)
* [Util: detectie van ContextualAttribute via Reflection](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Container/Util.php)
* [ContextualAttribute: marker-interface](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Contracts/Container/ContextualAttribute.php)
* [Config: ingebouwd configuratieattribuut](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Container/Attributes/Config.php)
* [Config Repository: get() en het ophalen van configuratiewaarden](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Config/Repository.php)
* [Arr: standaardwaarden van get() en de behandeling van bestaande null-waarden](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Collections/Arr.php)
* [Officiële tests: attribuutresolvers, geregistreerde handlers, ReflectionParameter en resolutie via call](https://github.com/laravel/framework/blob/v13.39.0/tests/Container/ContextualAttributeBindingTest.php)


## Related topics

- [Service container](https://kawax.biz/nl/service-container.md)
- [Containertags in packages en het bundelen van plugins](https://kawax.biz/nl/advanced/package-container-tags.md)
- [PHP Reflection API](https://kawax.biz/nl/advanced/php-reflection.md)
- [Levensduur van packageservices ontwerpen](https://kawax.biz/nl/advanced/package-service-lifetimes.md)
- [Hooks na het resolven van packageservices](https://kawax.biz/nl/advanced/package-service-resolution.md)


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