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

# Kontextuelle Attribute und Abhängigkeitsauflösung in Paketen

> Analysiert ContextualAttribute in Laravel 13 und erläutert Attribute zum Injizieren von Paketkonfiguration, die Reihenfolge der Auflösung, die Unterschiede zwischen make und call sowie das Testen und die Wartung langlebiger Services.

Manchmal sollen Nutzer eines Pakets benötigte Werte injizieren können, ohne Konfigurationsschlüssel im Service Provider aufzulisten. Wenn Sie kontextuelle Attribute bereitstellen, lassen sich paketspezifische Konfigurationswerte und Services anhand von Metadaten an einem Parameter auflösen.

Diese Seite prüft den offiziellen Quellcode von Laravel Framework `v13.39.0` und implementiert ein eigenes Attribut, das die Konfiguration `courier` liest. Wenn das eingebaute `#[Config('courier.timeout')]` bereits ausreicht, müssen Sie keine eigenen Attribute hinzufügen. Ziehen Sie sie in Betracht, wenn Ihr Paket eigene Schlüsselkonventionen oder Auflösungsregeln als Teil seiner API anbieten soll.

## Die Aufgabe eines Attributs ist es, den injizierten Wert festzulegen

Attribute, die `Illuminate\Contracts\Container\ContextualAttribute` implementieren, werden vom Laravel-Container beim Auflösen von Parametern gelesen. An die nutzende Klasse wird nicht das Attributobjekt selbst übergeben, sondern der Wert, den der Resolver zurückgibt.

| Methode | Ort der Konfiguration | Geeigneter Einsatz |
| - | - | - |
| `when()->needs()->give()` | Service Provider | Abhängigkeiten in der Anwendung austauschen, ohne die nutzende Klasse zu ändern |
| Eingebaute Attribute `Config`, `Storage`, `Tag` | Parameter der nutzenden Klasse | Eine von Laravel bereitgestellte Auflösungsmethode deklarieren |
| Paketeigene kontextuelle Attribute | Parameter der nutzenden Klasse und Attributklasse | Werte nach paketspezifischen Konventionen auflösen |

Normale Bindings und die eingebauten Attribute finden Sie unter [Service-Container](https://kawax.biz/de/service-container.md). Kontextuelle Attribute sind ein anderer Mechanismus als Attribute wie `Bind`, `Singleton` und `Scoped`, die Bindings für Klassen oder Interfaces deklarieren.

## Ein Attribut zum Lesen der Paketkonfiguration erstellen

### Standardkonfiguration registrieren

Legen Sie zunächst in `config/courier.php` des Pakets die Standardwerte an.

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

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

Führen Sie die Konfiguration in `register()` von `src/CourierServiceProvider.php` zusammen. Die automatische Registrierung des Providers ist unter [Interne Struktur der automatischen Paket-Erkennung](https://kawax.biz/de/advanced/package-discovery.md) beschrieben, das Veröffentlichen und den Umgang mit dem Konfigurations-Cache finden Sie unter [Paketkonfiguration zusammenführen und cachen](https://kawax.biz/de/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'
        );
    }
}
```

### `ContextualAttribute` implementieren

Definieren Sie in `src/Attributes/CourierConfig.php` ein Attribut, das dem Konfigurationsschlüssel `courier.` voranstellt.

```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 der geprüften Implementierung erhält der Resolver die folgenden drei Argumente.

1. Die Instanz des Attributs, das am Parameter angebracht ist.
2. Den Container, der die Abhängigkeit auflöst.
3. Ein `ReflectionParameter`, der das Auflösungsziel darstellt.

In diesem Beispiel wird der Parametername verwendet, wenn der Schlüssel weggelassen wird. `#[CourierConfig(default: 30)] int $timeout` verweist auf `courier.timeout`. Wenn dieselbe Konfiguration auch nach einer Umbenennung des Parameters auf Nutzerseite gelesen werden soll, geben Sie den Schlüssel explizit an, etwa `#[CourierConfig('timeout', default: 30)]`.

`ContextualAttribute` ist zum Zeitpunkt der Prüfung ein Marker-Interface ohne deklarierte Methoden. Daher garantiert `implements` allein weder die Existenz noch die Signatur eines Resolvers. Wenn Sie wie oben `resolve()` bereitstellen, müssen Sie für dieses Attribut kein normales `bind()` registrieren.

> [!WARNING]
>
> Verwenden Sie den Konstruktor des Attributs nur zum Speichern von Metadaten. Der Container ruft `newInstance()` bei der Auflösung und bei der Nachbearbeitung des Attributs getrennt auf, daher ist er kein Ort für eine „einmalige Initialisierung“. Führen Sie dort keine externe Kommunikation aus und halten Sie keinen veränderlichen Zustand.

### Die nutzende Klasse über den Container erzeugen

Versehen Sie `src/CourierOptions.php` mit den Attributen.

```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,
    ) {}
}
```

Wenn die nutzende Anwendung die Konfiguration überschreibt, wird bei der nächsten Auflösung dieser Wert injiziert.

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

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

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

In diesem Beispiel ist `$options->timeout` gleich `45` und `$options->retries` gleich `3`. Das Attribut liest nur die Konfiguration und führt keine fachliche Validierung durch, etwa ob es sich um eine positive Ganzzahl handelt oder ob ein zulässiger Höchstwert eingehalten wird. Auch Typdeklarationen ersetzen keine Validierung von Konfigurationswerten.

## Die Auflösungsreihenfolge im Konstruktor nachvollziehen

`Container::resolveDependencies()` bestimmt den Wert jedes Parameters in der folgenden Reihenfolge.

1. Einen Überschreibungswert verwenden, der z. B. über `makeWith()` übergeben wurde und zum Parameternamen passt.
2. Mit `Util::getContextualAttributeFromDependency()` nach einem kontextuellen Attribut suchen und es auflösen.
3. Wenn das Ergebnis des Attributs `null` ist, zur normalen Auflösung von primitiven Werten oder Klassen übergehen.
4. Nach der Nachbearbeitung des Attributs den ermittelten Wert an den Konstruktor übergeben.

Der Überschreibungsweg aus Schritt 1 überspringt die Attributauflösung und die Nachbearbeitung für diesen Parameter.

```mermaid theme={null}
flowchart TD
    A["Konstruktorparameter"] --> B{"Per Parametername überschrieben?"}
    B -->|Yes| C["Überschreibungswert verwenden"]
    B -->|No| D{"ContextualAttribute vorhanden?"}
    D -->|Yes| E["Resolver des Attributs aufrufen"]
    E --> F{"Ergebnis ist null?"}
    F -->|No| G["Wert des Attributs verwenden"]
    F -->|Yes| H["Normale Typ- bzw. Primitivauflösung"]
    D -->|No| H
    G --> I["Nachbearbeitung des Attributs"]
    H --> I
    C --> J["An den Konstruktor übergeben"]
    I --> J
```

Die Schlüssel von `makeWith()` sind keine Konfigurationsschlüssel, sondern Parameternamen der nutzenden Klasse.

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

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

In diesem Fall ist `timeout` unabhängig vom Konfigurationswert `5`. Gibt das Attribut dagegen einen Wert ungleich `null` zurück, hat das Ergebnis des Attributs Vorrang vor einem normalen `when()->needs('$timeout')->give(...)`. Testen Sie daher auch, ob das Hinzufügen eines Attributs im Paket bestehende kontextuelle Bindings der Nutzer verändern kann.

`Util` ermittelt per Reflection die Attribute, die `ContextualAttribute` implementieren, und wählt das erste davon für die Auflösung aus. Wenn Sie mehrere verschiedene kontextuelle Attribute nebeneinander angeben, werden die Resolver nicht nacheinander kombiniert.

### Registrierte Handler haben Vorrang vor `resolve()`

`Container::resolveFromAttribute()` sucht zuerst mit dem Klassennamen des Attributs als Schlüssel nach einem Handler, der mit `whenHasAttribute()` registriert wurde. Gibt es keinen, wird `resolve()` des Attributs verwendet. Fehlt beides, wird eine `BindingResolutionException` geworfen.

`whenHasAttribute()` ist keine Einstellung pro nutzender Klasse, sondern pro Attributtyp. Eine erneute Registrierung für dieselbe Attributklasse überschreibt den vorherigen Handler. Wenn ein Paket die Handler anderer Pakete oder der eingebauten Laravel-Attribute bedingungslos ändert, wirkt sich das auf die gesamte nutzende Anwendung aus. Legen Sie das Standardverhalten eigener Attribute in `resolve()` ab und behandeln Sie Änderungen über Handler als expliziten Erweiterungspunkt.

## `make()`, `call()` und direkte Aufrufe verhalten sich nicht gleich

Auch `app()->call()` löst Attribute auf, aber der normale `BoundMethod`-Pfad fügt das Ergebnis eines gefundenen Attributs unverändert den Argumenten hinzu. Anders als bei der Konstruktorauflösung wird bei einem Ergebnis von `null` nicht auf die Auflösung des Standardwerts zurückgegriffen.

Ein Beispiel, das ohne weitere kontextuelle Bindings den nicht existierenden Schlüssel `courier.missing` liest:

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

| Aufruf | Ergebnis | Grund |
| - | - | - |
| `app()->make(NullableTimeoutOptions::class)->timeout` | `99` | Das Attribut gibt `null` zurück, danach wird der primitive Standardwert verwendet |
| `app()->call($readTimeout)` | `null` | Das Ergebnis des Attributs wird unverändert übergeben |
| `$readTimeout()` | `99` | Normaler PHP-Aufruf, Attribute werden nicht aufgelöst |

Auch `new CourierOptions(timeout: 5, retries: 1)` ist eine normale Instanziierung durch PHP. Das bloße Anbringen eines Attributs fügt direkten `new`-Aufrufen oder Methodenaufrufen keine Dependency Injection hinzu.

> [!NOTE]
>
> Wenn Konstruktoren und Callables dasselbe Standardverhalten benötigen, verlassen Sie sich nicht nur auf den Standardwert des Parameters, sondern definieren Sie einen Standardwert im Attribut, etwa `#[CourierConfig('timeout', default: 30)]`. Dieses `default` greift, wenn der Konfigurationsschlüssel nicht existiert. Es ersetzt keinen vorhandenen Schlüssel, dessen Wert `null` ist. Bei APIs, in denen `null` selbst ein bedeutungsvoller Wert ist, prüfen Sie beide Auflösungswege getrennt.

## Erneute Injektion von Werten und Lebensdauer von Services trennen

Das Attribut liest die Konfiguration zum Zeitpunkt der Parameterauflösung. Es ist keine Funktion, die Eigenschaften eines bereits erzeugten `CourierOptions` überwacht oder aktualisiert.

Wenn Sie `CourierOptions` beispielsweise als Singleton registrieren, behält diese Instanz die zuerst injizierten Werte. Auch wenn Sie danach die Konfiguration ändern und ein normales `make()` aufrufen, ändern sich die Werte derselben Instanz nicht.

Auch bei Attributen, die anfragebezogene Informationen lesen, bestimmt das Attribut selbst nicht die Lebensdauer als Singleton oder Scoped. Wählen Sie die Lebensdauer der Services, die injizierte Werte oder Objekte halten, gemäß [Lebenszyklus-Design für Paket-Services](https://kawax.biz/de/advanced/package-service-lifetimes.md). Betrachten Sie das Neuerzeugen des Konfigurations-Caches getrennt von der Aktualisierung langlebiger Prozesse und bereits erzeugter Services.

## Auflösungswege testen

Tests für das Attribut allein lassen sich wie in den offiziellen Tests mit einem neuen `Container` und `Illuminate\Config\Repository` schreiben. Das folgende PHPUnit-Beispiel setzt eine Testumgebung voraus, in der die oben definierten Klassen `CourierConfig`, `CourierOptions` und `NullableTimeoutOptions` geladen sind.

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

Das Beispiel mit einem Handler, der immer `7` zurückgibt, dient nur zum Testen der Priorität. Innerhalb dieses Containers wirkt es sich auf jede Auflösung desselben Attributs aus, einschließlich `retries`.

Dieser Unit-Test allein deckt die Integration mit der Provider-Registrierung des Pakets, `mergeConfigFrom()` und dem Konfigurations-Cache nicht ab. Prüfen Sie zusätzlich in der Anwendungsumgebung aus [Laravel-Pakete mit Orchestra Testbench testen](https://kawax.biz/de/advanced/package-testing.md) die Zustände vor der Veröffentlichung, nach der Veröffentlichung und mit Konfigurations-Cache.

### Prüfpunkte vor dem Release

* Behandeln Sie den Klassennamen des Attributs, die Namen der Konstruktorparameter, die Konfigurationsschlüssel und den Rückgabetyp als öffentliche API. Achten Sie auch auf Änderungen an `key` und `default`, die als benannte Argumente verwendet werden.
* Wenn bei fehlendem Schlüssel der Parametername verwendet wird, prüfen Sie, ob eine Umbenennung des Parameters in der nutzenden Klasse die referenzierte Konfiguration verändert.
* Testen Sie bestehende Bindings ohne Attribut, die Auflösung mit Attribut und das Überschreiben per Parametername jeweils separat.
* Testen Sie `null`, fehlende Konfiguration und ungeeignete Typen sowohl mit `make()` als auch mit `call()`.
* Stellen Sie sicher, dass Singletons und langlebige Worker keine veralteten Werte halten.
* Prüfen Sie bei einer Aktualisierung der unterstützten Laravel-Version die Argumente des Resolvers, die Priorität und die Nachbearbeitung der Attribute erneut anhand des offiziellen Quellcodes und der Tests.

## Verwandte Seiten

> [card] [Paketkonfiguration zusammenführen und cachen](https://kawax.biz/de/advanced/package-config-merging.md)
>
> Prüfen Sie die Grenzen zwischen Standardkonfiguration, veröffentlichter Konfiguration und Cache-Aktualisierung.

> [card] [Lebenszyklus-Design für Paket-Services](https://kawax.biz/de/advanced/package-service-lifetimes.md)
>
> Wählen Sie die Lebensdauer von Services, die injizierte Werte halten.

> [card] [Container-Tags und Plugin-Aggregation in Paketen](https://kawax.biz/de/advanced/package-container-tags.md)
>
> Lernen Sie das eingebaute Tag-Attribut und die Aggregation registrierter Implementierungen kennen.

> [card] [PHP Reflection API](https://kawax.biz/de/advanced/php-reflection.md)
>
> Verstehen Sie, wie Metadaten von Attributen und Parametern gelesen werden.

## Herangezogene Primärquellen

* [Offizielle Laravel-Dokumentation: Defining custom attributes](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/container.md#defining-custom-attributes)
* [Offizielle Laravel-Dokumentation: Paketkonfiguration](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/packages.md#configuration)
* [Container: resolveDependencies(), resolveFromAttribute(), whenHasAttribute() und Nachbearbeitung von Attributen](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Container/Container.php)
* [BoundMethod: Parameterauflösung für Callables](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Container/BoundMethod.php)
* [Util: Erkennung von ContextualAttribute per 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: Eingebautes Konfigurationsattribut](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Container/Attributes/Config.php)
* [Config Repository: get() und das Abrufen von Konfigurationswerten](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Config/Repository.php)
* [Arr: Standardwert von get() und Umgang mit vorhandenem null](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Collections/Arr.php)
* [Offizielle Tests: Resolver von Attributen, registrierte Handler, ReflectionParameter und Auflösung mit call](https://github.com/laravel/framework/blob/v13.39.0/tests/Container/ContextualAttributeBindingTest.php)


## Related topics

- [Service-Container](https://kawax.biz/de/service-container.md)
- [Container-Tags und Plugin-Aggregation in Paketen](https://kawax.biz/de/advanced/package-container-tags.md)
- [Event-Listener in Paketen registrieren und cachen](https://kawax.biz/de/advanced/package-events.md)
- [Blade-Erweiterungen und Kompilierungscache in Paketen](https://kawax.biz/de/advanced/package-blade-extensions.md)
- [Middleware-Registrierung und Geltungsbereich in Paketen](https://kawax.biz/de/advanced/package-middleware.md)


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