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

# Package contextual attributes and dependency resolution

> Examine Laravel 13's ContextualAttribute to understand attributes that inject package configuration, resolution precedence, the differences between make and call, and testing and maintenance for long-lived services.

Sometimes you want package users to be able to inject the values they need without listing configuration keys in a service provider. By publishing a contextual attribute, you can resolve package-specific configuration or services from metadata attached to a parameter.

This page checks the official source of Laravel Framework `v13.39.0` and implements a custom attribute that reads the `courier` configuration. If the built-in `#[Config('courier.timeout')]` alone meets your needs, there is no need to add a custom attribute. Consider one when you want to offer your own key conventions or resolution policy as part of the package API.

## An attribute's role is to decide which value to inject

Laravel's container reads attributes that implement `Illuminate\Contracts\Container\ContextualAttribute` when it resolves parameters. The consuming class receives the value returned by the resolver, not the attribute object itself.

| Approach | Where it is configured | Suitable use |
| - | - | - |
| `when()->needs()->give()` | Service provider | Switch dependencies on the application side without changing the consuming class |
| Built-in `Config`, `Storage`, and `Tag` attributes | Parameters of the consuming class | Declare a resolution method provided by Laravel |
| Package-specific contextual attribute | Parameters of the consuming class and the attribute class | Resolve values using package-specific conventions |

For regular bindings and built-in attributes, see [Service container](https://kawax.biz/en/service-container.md). Contextual attributes are a separate mechanism from attributes such as `Bind`, `Singleton`, and `Scoped` that declare bindings for classes or interfaces.

## Create an attribute that reads package configuration

### Register the default configuration

First, provide default values in the package's `config/courier.php`.

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

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

Merge the configuration in `register()` of `src/CourierServiceProvider.php`. For automatic provider registration, see [Package auto-discovery internals](https://kawax.biz/en/advanced/package-discovery.md). For publishing and handling configuration caching, see [Package config merging and caching](https://kawax.biz/en/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'
        );
    }
}
```

### Implement `ContextualAttribute`

In `src/Attributes/CourierConfig.php`, define an attribute that prefixes the configuration key with `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
        );
    }
}
```

In the implementation examined, the resolver receives the following three arguments.

1. The instance of the attribute attached to the parameter.
2. The container resolving the dependency.
3. The `ReflectionParameter` representing the target being resolved.

In this example, the parameter name is used when the key is omitted. `#[CourierConfig(default: 30)] int $timeout` refers to `courier.timeout`. If you want to read the same setting even when the consumer renames the parameter, specify the key explicitly, as in `#[CourierConfig('timeout', default: 30)]`.

As of the version examined, `ContextualAttribute` is a marker interface that declares no methods. Therefore, `implements` alone does not guarantee that a resolver exists or what its signature is. If you provide `resolve()` as shown above, you do not need to register a regular `bind()` for this attribute.

> [!WARNING]
>
> Use the attribute's constructor only to hold metadata. The container calls `newInstance()` separately during resolution and during attribute post-processing, so it is not a place for "run-once" initialization. Do not perform external calls or keep mutable state in it.

### Create the consuming class from the container

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

When the consuming application overrides the configuration, that value is injected on the next resolution.

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

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

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

In this example, `$options->timeout` is `45` and `$options->retries` is `3`. The attribute only retrieves configuration. It does not perform business validation, such as whether a value is a positive integer or within an allowed maximum. Type declarations are not a substitute for validating configuration values either.

## Read the resolution order for constructors

`Container::resolveDependencies()` determines the value of each parameter in the following order.

1. Use an override value passed through `makeWith()` or similar whose key matches the parameter name.
2. Find and resolve a contextual attribute with `Util::getContextualAttributeFromDependency()`.
3. If the attribute's result is `null`, proceed to regular primitive or class resolution.
4. After attribute post-processing, pass the determined value to the constructor.

The override path in step 1 skips attribute resolution and post-processing for that parameter.

```mermaid theme={null}
flowchart TD
    A["Constructor parameter"] --> B{"Overridden by parameter name?"}
    B -->|Yes| C["Use override value"]
    B -->|No| D{"Has a ContextualAttribute?"}
    D -->|Yes| E["Call the attribute's resolver"]
    E --> F{"Result is null?"}
    F -->|No| G["Use the attribute's value"]
    F -->|Yes| H["Regular type/primitive resolution"]
    D -->|No| H
    G --> I["Attribute post-processing"]
    H --> I
    C --> J["Pass to the constructor"]
    I --> J
```

The keys for `makeWith()` are the consuming class's parameter names, not configuration keys.

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

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

In this case, `timeout` is `5` regardless of the configuration value. On the other hand, when the attribute returns a non-`null` value, the attribute's result takes precedence over a regular `when()->needs('$timeout')->give(...)`. Also test whether adding an attribute to the package might change existing users' contextual bindings.

`Util` retrieves attributes that implement `ContextualAttribute` through Reflection and selects the first one for resolution. Listing multiple different contextual attributes does not compose their resolvers in sequence.

### Registered handlers take precedence over `resolve()`

`Container::resolveFromAttribute()` uses the attribute class name as the key and first looks for a handler registered with `whenHasAttribute()`. If there is none, it uses the attribute's `resolve()`. If neither exists, it throws a `BindingResolutionException`.

`whenHasAttribute()` is configured per attribute type, not per consuming class. Registering again for the same attribute class overwrites the previous handler. If a package unconditionally changes the handler for another package's attribute or a Laravel built-in attribute, it affects the entire consuming application. Put the standard behavior of a custom attribute in `resolve()`, and treat handler-based changes as an explicit extension point.

## `make()`, `call()`, and direct invocation are not the same

`app()->call()` also resolves attributes, but the regular `BoundMethod` path adds the attribute's result directly to the arguments once an attribute is found. Unlike constructor resolution, it does not fall back to default value resolution even when the attribute's result is `null`.

Here is an example that reads the nonexistent `courier.missing` with no other contextual 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;
};
```

| Invocation | Result | Reason |
| - | - | - |
| `app()->make(NullableTimeoutOptions::class)->timeout` | `99` | The attribute returns `null`, so resolution proceeds to the primitive default value |
| `app()->call($readTimeout)` | `null` | The attribute's result is passed as is |
| `$readTimeout()` | `99` | This is a normal PHP call and does not resolve attributes |

`new CourierOptions(timeout: 5, retries: 1)` is also normal PHP instantiation. Attaching an attribute does not add dependency injection to direct `new` calls or method calls.

> [!NOTE]
>
> If you need the same default behavior for both constructors and callables, define the default on the attribute, as in `#[CourierConfig('timeout', default: 30)]`, instead of relying only on the parameter's default value. This `default` is used when the configuration key does not exist, and it does not replace the value when an existing key is `null`. For APIs that treat `null` itself as a meaningful value, verify the two resolution paths separately.

## Separate value reinjection from service lifetime

An attribute reads the configuration at the time the parameter is resolved. It is not a feature that watches or updates the properties of an already-created `CourierOptions`.

For example, if you make `CourierOptions` a singleton, the values injected first are retained on that instance. Even if you change the configuration afterward and call a regular `make()`, the values on the same instance do not change.

Even when you create an attribute that reads per-request information, the attribute itself does not determine a singleton or scoped lifetime. Choose the lifetime of services that hold injected values or objects according to [Package service lifetime design](https://kawax.biz/en/advanced/package-service-lifetimes.md). Also treat regenerating the configuration cache separately from refreshing long-running processes and already-created services.

## Test the resolution paths

As with the official tests, you can write tests for the attribute alone using a new `Container` and `Illuminate\Config\Repository`. The following PHPUnit example assumes a test environment that loads the `CourierConfig`, `CourierOptions`, and `NullableTimeoutOptions` defined above.

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

The example where the handler always returns `7` is only for testing precedence. Within this container, it affects every resolution of the same attribute, including `retries`.

This unit test alone cannot verify integration with the package's provider registration, `mergeConfigFrom()`, or the configuration cache. Also verify the unpublished, published, and configuration-cached states in the application environment described in [Testing Laravel packages with Orchestra Testbench](https://kawax.biz/en/advanced/package-testing.md).

### Pre-release checklist

* Treat the attribute's class name, constructor parameter names, configuration keys, and return type as public API. Be careful about changes to `key` and `default`, which are used as named arguments.
* If the parameter name is used when the key is omitted, check whether renaming a parameter in a consuming class changes which configuration it refers to.
* Test existing bindings without attributes, resolution with attributes, and overrides by parameter name separately.
* Test `null`, missing configuration, and invalid types with both `make()` and `call()`.
* Confirm that the design does not let singletons or long-running workers retain stale values.
* When updating supported Laravel versions, recheck the resolver arguments, precedence, and attribute post-processing against the official source and tests.

## Related pages

> [card] [Package config merging and caching](https://kawax.biz/en/advanced/package-config-merging.md)
>
> Review default configuration, published configuration, and the boundaries of cache updates.

> [card] [Package service lifetime design](https://kawax.biz/en/advanced/package-service-lifetimes.md)
>
> Choose the lifetime of services that hold injected values.

> [card] [Package container tags and plugin aggregation](https://kawax.biz/en/advanced/package-container-tags.md)
>
> Review the built-in Tag attribute and aggregation of registered implementations.

> [card] [PHP Reflection API](https://kawax.biz/en/advanced/php-reflection.md)
>
> Review the mechanism for reading attribute and parameter metadata.

## Primary sources consulted

* [Laravel official documentation: Defining custom attributes](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/container.md#defining-custom-attributes)
* [Laravel official documentation: Package configuration](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/packages.md#configuration)
* [Container: resolveDependencies(), resolveFromAttribute(), whenHasAttribute(), and attribute post-processing](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Container/Container.php)
* [BoundMethod: callable argument resolution](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Container/BoundMethod.php)
* [Util: detecting ContextualAttribute through 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: built-in configuration attribute](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Container/Attributes/Config.php)
* [Config Repository: get() and retrieving configuration values](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Config/Repository.php)
* [Arr: get() default values and handling of existing nulls](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Collections/Arr.php)
* [Official tests: attribute resolvers, registered handlers, ReflectionParameter, and resolution via call](https://github.com/laravel/framework/blob/v13.39.0/tests/Container/ContextualAttributeBindingTest.php)


## Related topics

- [Package container tags and plugin aggregation](https://kawax.biz/en/advanced/package-container-tags.md)
- [Service container](https://kawax.biz/en/service-container.md)
- [Package service resolution hooks](https://kawax.biz/en/advanced/package-service-resolution.md)
- [Registering package Artisan commands and resolving dependencies](https://kawax.biz/en/advanced/package-commands.md)
- [Package service lifetime design](https://kawax.biz/en/advanced/package-service-lifetimes.md)


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