Skip to main content
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. For regular bindings and built-in attributes, see Service container. 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.
Merge the configuration in register() of src/CourierServiceProvider.php. For automatic provider registration, see Package auto-discovery internals. For publishing and handling configuration caching, see Package config merging and caching.

Implement ContextualAttribute

In src/Attributes/CourierConfig.php, define an attribute that prefixes the configuration key with courier..
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.
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.
When the consuming application overrides the configuration, that value is injected on the next resolution.
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. The keys for makeWith() are the consuming class’s parameter names, not configuration keys.
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.
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.
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. 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.
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.

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.

Package config merging and caching

Review default configuration, published configuration, and the boundaries of cache updates.

Package service lifetime design

Choose the lifetime of services that hold injected values.

Package container tags and plugin aggregation

Review the built-in Tag attribute and aggregation of registered implementations.

PHP Reflection API

Review the mechanism for reading attribute and parameter metadata.

Primary sources consulted

Last modified on October 31, 2026