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 implementIlluminate\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’sconfig/courier.php.
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..
- The instance of the attribute attached to the parameter.
- The container resolving the dependency.
- The
ReflectionParameterrepresenting the target being resolved.
#[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.
Create the consuming class from the container
Attach the attribute insrc/CourierOptions.php.
$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.
- Use an override value passed through
makeWith()or similar whose key matches the parameter name. - Find and resolve a contextual attribute with
Util::getContextualAttributeFromDependency(). - If the attribute’s result is
null, proceed to regular primitive or class resolution. - After attribute post-processing, pass the determined value to the constructor.
makeWith() are the consuming class’s parameter names, not configuration keys.
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-createdCourierOptions.
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 newContainer and Illuminate\Config\Repository. The following PHPUnit example assumes a test environment that loads the CourierConfig, CourierOptions, and NullableTimeoutOptions defined above.
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
keyanddefault, 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 bothmake()andcall(). - 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
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
- Laravel official documentation: Defining custom attributes
- Laravel official documentation: Package configuration
- Container: resolveDependencies(), resolveFromAttribute(), whenHasAttribute(), and attribute post-processing
- BoundMethod: callable argument resolution
- Util: detecting ContextualAttribute through Reflection
- ContextualAttribute: marker interface
- Config: built-in configuration attribute
- Config Repository: get() and retrieving configuration values
- Arr: get() default values and handling of existing nulls
- Official tests: attribute resolvers, registered handlers, ReflectionParameter, and resolution via call