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

# Atributos contextuales y resolución de dependencias en paquetes

> Analiza ContextualAttribute de Laravel 13 y explica los atributos que inyectan la configuración de un paquete, el orden de prioridad de la resolución, las diferencias entre make y call, y las pruebas y el mantenimiento de servicios de larga duración.

A veces quieres que quienes usan tu paquete puedan inyectar los valores que necesitan sin enumerar claves de configuración en el service provider. Si expones atributos contextuales, la configuración y los servicios propios del paquete se pueden resolver a partir de los metadatos añadidos a los argumentos.

En esta página se revisa el código fuente oficial de Laravel Framework `v13.39.0` y se implementa un atributo propio que lee la configuración `courier`. Si el atributo integrado `#[Config('courier.timeout')]` basta para tu objetivo, no hace falta añadir atributos propios. Considéralos cuando quieras ofrecer, como parte de la API del paquete, una convención de claves o una política de resolución propias.

## La función del atributo es decidir qué valor se inyecta

El contenedor de Laravel lee los atributos que implementan `Illuminate\Contracts\Container\ContextualAttribute` al resolver los argumentos. A la clase consumidora no se le pasa el objeto del atributo, sino el valor que devuelve el resolver.

| Método | Dónde se configura | Uso adecuado |
| - | - | - |
| `when()->needs()->give()` | Service provider | Cambiar dependencias desde la aplicación sin modificar la clase consumidora |
| Atributos integrados `Config`, `Storage` y `Tag` | Argumentos de la clase consumidora | Declarar los métodos de resolución que ofrece Laravel |
| Atributo contextual propio del paquete | Argumentos de la clase consumidora y la clase del atributo | Resolver valores según las convenciones propias del paquete |

Para los bindings normales y los atributos integrados, consulta [Service container](https://kawax.biz/es/service-container.md). Los atributos contextuales son un mecanismo distinto de los atributos que declaran bindings de clases o interfaces, como `Bind`, `Singleton` y `Scoped`.

## Crear un atributo que lee la configuración del paquete

### Registrar la configuración predeterminada

Primero, prepara los valores predeterminados en el `config/courier.php` del paquete.

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

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

Fusiona la configuración en el `register()` de `src/CourierServiceProvider.php`. Para el registro automático del provider, consulta [Estructura interna de la detección automática de paquetes](https://kawax.biz/es/advanced/package-discovery.md), y para la publicación y la caché de configuración, [Fusión y caché de la configuración de paquetes](https://kawax.biz/es/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'
        );
    }
}
```

### Implementar `ContextualAttribute`

En `src/Attributes/CourierConfig.php`, define un atributo que añade `courier.` a la clave de configuración.

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

En la implementación revisada, el resolver recibe estos tres argumentos:

1. La instancia del atributo añadido al argumento.
2. El contenedor que está resolviendo la dependencia.
3. El `ReflectionParameter` que representa el objetivo de la resolución.

En este ejemplo, si se omite la clave se usa el nombre del argumento. `#[CourierConfig(default: 30)] int $timeout` hace referencia a `courier.timeout`. Si quieres leer la misma configuración aunque cambie el nombre del argumento en el lado consumidor, indica la clave de forma explícita, como en `#[CourierConfig('timeout', default: 30)]`.

En el momento de la revisión, `ContextualAttribute` es una interfaz marcadora que no declara métodos. Por eso, `implements` por sí solo no garantiza la existencia del resolver ni su firma. Si defines `resolve()` como arriba, no necesitas registrar un `bind()` normal para este atributo.

> [!WARNING]
>
> Usa el constructor del atributo solo para guardar metadatos. El contenedor llama a `newInstance()` por separado durante la resolución y durante el posprocesamiento del atributo, así que no es el lugar para una "inicialización que se ejecuta una sola vez". No le des comunicaciones externas ni estado mutable.

### Crear la clase consumidora desde el contenedor

Añade los atributos en `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,
    ) {}
}
```

Si la aplicación consumidora sobrescribe la configuración, ese valor se inyecta en la siguiente resolución.

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

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

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

En este ejemplo, `$options->timeout` es `45` y `$options->retries` es `3`. El atributo solo obtiene la configuración; no realiza validaciones de negocio, como comprobar si es un entero positivo o si respeta el máximo permitido. La declaración de tipo tampoco sustituye a la validación de los valores de configuración.

## Leer el orden de resolución en el constructor

`Container::resolveDependencies()` decide el valor de cada argumento en este orden:

1. Usa el valor de sobrescritura que coincide con el nombre del argumento, pasado con `makeWith()` u otros métodos.
2. Busca un atributo contextual con `Util::getContextualAttributeFromDependency()` y lo resuelve.
3. Si el resultado del atributo es `null`, pasa a la resolución normal de valores primitivos o de clases.
4. Tras el posprocesamiento del atributo, pasa el valor decidido al constructor.

La vía de sobrescritura del paso 1 omite la resolución del atributo y el posprocesamiento de ese argumento.

```mermaid theme={null}
flowchart TD
    A["Argumento del constructor"] --> B{"¿Sobrescrito por nombre de argumento?"}
    B -->|Yes| C["Usar el valor de sobrescritura"]
    B -->|No| D{"¿Tiene ContextualAttribute?"}
    D -->|Yes| E["Llamar al resolver del atributo"]
    E --> F{"¿El resultado es null?"}
    F -->|No| G["Usar el valor del atributo"]
    F -->|Yes| H["Resolución normal de tipo o primitivo"]
    D -->|No| H
    G --> I["Posprocesamiento del atributo"]
    H --> I
    C --> J["Pasar al constructor"]
    I --> J
```

Las claves de `makeWith()` son los nombres de los argumentos de la clase consumidora, no las claves de configuración.

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

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

En este caso, `timeout` es `5` sin importar la configuración. En cambio, si el atributo devuelve algo distinto de `null`, su resultado tiene prioridad sobre el `when()->needs('$timeout')->give(...)` normal. Prueba también la posibilidad de que añadir un atributo al paquete cambie los bindings contextuales de los usuarios existentes.

`Util` obtiene mediante Reflection los atributos que implementan `ContextualAttribute` y elige el primero para la resolución. Aunque coloques varios atributos contextuales distintos, sus resolvers no se combinan en orden.

### Los handlers registrados tienen prioridad sobre `resolve()`

`Container::resolveFromAttribute()` usa el nombre de la clase del atributo como clave y busca primero un handler registrado con `whenHasAttribute()`. Si no lo hay, usa el `resolve()` del atributo, y si no existe ninguno de los dos, lanza una `BindingResolutionException`.

`whenHasAttribute()` es una configuración por tipo de atributo, no por clase consumidora. Volver a registrar la misma clase de atributo sobrescribe el handler anterior. Si un paquete modifica sin condiciones los handlers de otros paquetes o de los atributos integrados de Laravel, afecta a toda la aplicación consumidora. Coloca el comportamiento estándar de tus atributos propios en `resolve()` y trata los cambios mediante handlers como un punto de extensión explícito.

## `make()`, `call()` y la llamada directa no son lo mismo

`app()->call()` también resuelve atributos, pero la vía normal de `BoundMethod`, cuando encuentra un atributo, añade su resultado directamente a los argumentos. A diferencia de la resolución del constructor, aunque el resultado del atributo sea `null`, no vuelve a la resolución del valor predeterminado.

Este ejemplo lee la clave inexistente `courier.missing` sin ningún otro binding contextual.

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

| Llamada | Resultado | Motivo |
| - | - | - |
| `app()->make(NullableTimeoutOptions::class)->timeout` | `99` | El atributo devuelve `null` y se pasa al valor predeterminado del primitivo |
| `app()->call($readTimeout)` | `null` | Se pasa directamente el resultado del atributo |
| `$readTimeout()` | `99` | Es una llamada normal de PHP y no resuelve atributos |

`new CourierOptions(timeout: 5, retries: 1)` también es una creación normal de PHP. Añadir un atributo no agrega inyección de dependencias a un `new` directo ni a las llamadas a métodos.

> [!NOTE]
>
> Si necesitas el mismo comportamiento predeterminado tanto en el constructor como en los callables, no dependas solo del valor predeterminado del argumento y define el valor predeterminado en el atributo, como en `#[CourierConfig('timeout', default: 30)]`. Este `default` se usa cuando la clave de configuración no existe y no reemplaza el valor cuando una clave existente vale `null`. En las API que tratan el propio `null` como un valor con significado, verifica por separado las dos vías de resolución.

## Separar la reinyección de valores del ciclo de vida del servicio

El atributo lee la configuración en el momento en que se resuelve el argumento. No es una función que observe o actualice las propiedades de un `CourierOptions` ya creado.

Por ejemplo, si `CourierOptions` es un singleton, esa instancia conserva los valores inyectados la primera vez. Aunque después cambies la configuración y llames a `make()` normalmente, los valores de la misma instancia no cambian.

Incluso al crear atributos que leen información por solicitud, el atributo no decide por sí mismo un ciclo de vida singleton o scoped. Elige el ciclo de vida de los servicios que conservan los valores u objetos inyectados según [Diseño del ciclo de vida de los servicios de paquetes](https://kawax.biz/es/advanced/package-service-lifetimes.md). Considera también por separado la regeneración de la caché de configuración y la actualización de procesos de larga duración y servicios ya creados.

## Probar las vías de resolución

Las pruebas del atributo por sí solo se pueden escribir, como en las pruebas oficiales, con un nuevo `Container` y un `Illuminate\Config\Repository`. El siguiente ejemplo de PHPUnit supone un entorno de pruebas que carga `CourierConfig`, `CourierOptions` y `NullableTimeoutOptions`, definidos arriba.

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

El ejemplo en el que el handler siempre devuelve `7` sirve solo para probar el orden de prioridad. Dentro de este contenedor, afecta a todas las resoluciones del mismo atributo, incluida `retries`.

Esta prueba unitaria por sí sola no comprueba la integración con el registro del provider del paquete, `mergeConfigFrom()` ni la caché de configuración. Verifica también los estados antes de publicar, después de publicar y con caché de configuración en el entorno de aplicación de [Probar paquetes Laravel con Orchestra Testbench](https://kawax.biz/es/advanced/package-testing.md).

### Comprobaciones antes de publicar una versión

* Trata como API pública el nombre de la clase del atributo, los nombres de los argumentos del constructor, las claves de configuración y el tipo de retorno. Presta atención también a los cambios en `key` y `default`, que se usan como argumentos con nombre.
* Si la especificación usa el nombre del argumento cuando se omite la clave, comprueba que cambiar el nombre del argumento en la clase consumidora no cambie la configuración a la que se hace referencia.
* Prueba por separado los bindings existentes sin atributo, la resolución con atributo y la sobrescritura por nombre de argumento.
* Prueba `null`, la configuración ausente y los tipos inadecuados tanto con `make()` como con `call()`.
* Comprueba que el diseño no haga que los singletons o los workers de larga duración conserven valores obsoletos.
* Al actualizar las versiones de Laravel compatibles, vuelve a revisar los argumentos del resolver, el orden de prioridad y el posprocesamiento de atributos en el código fuente y las pruebas oficiales.

## Páginas relacionadas

> [card] [Fusión y caché de la configuración de paquetes](https://kawax.biz/es/advanced/package-config-merging.md)
>
> Revisa la configuración predeterminada, la configuración publicada y los límites de actualización de la caché.

> [card] [Diseño del ciclo de vida de los servicios de paquetes](https://kawax.biz/es/advanced/package-service-lifetimes.md)
>
> Elige el ciclo de vida de los servicios que conservan los valores inyectados.

> [card] [Etiquetas del contenedor en paquetes y agregación de plugins](https://kawax.biz/es/advanced/package-container-tags.md)
>
> Revisa el atributo integrado Tag y la agregación de implementaciones registradas.

> [card] [PHP Reflection API](https://kawax.biz/es/advanced/php-reflection.md)
>
> Revisa el mecanismo para leer los metadatos de atributos y argumentos.

## Fuentes primarias consultadas

* [Documentación oficial de Laravel: Defining custom attributes](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/container.md#defining-custom-attributes)
* [Documentación oficial de Laravel: configuración de paquetes](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/packages.md#configuration)
* [Container: resolveDependencies(), resolveFromAttribute(), whenHasAttribute() y posprocesamiento de atributos](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Container/Container.php)
* [BoundMethod: resolución de argumentos de callables](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Container/BoundMethod.php)
* [Util: detección de ContextualAttribute mediante Reflection](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Container/Util.php)
* [ContextualAttribute: interfaz marcadora](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Contracts/Container/ContextualAttribute.php)
* [Config: atributo de configuración integrado](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Container/Attributes/Config.php)
* [Config Repository: get() y obtención de valores de configuración](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Config/Repository.php)
* [Arr: valor predeterminado de get() y tratamiento de null existente](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Collections/Arr.php)
* [Pruebas oficiales: resolver del atributo, handlers registrados, ReflectionParameter y resolución con call](https://github.com/laravel/framework/blob/v13.39.0/tests/Container/ContextualAttributeBindingTest.php)


## Related topics

- [Registro de comandos Artisan de paquetes y resolución de dependencias](https://kawax.biz/es/advanced/package-commands.md)
- [Hooks de resolución de servicios en paquetes](https://kawax.biz/es/advanced/package-service-resolution.md)
- [Registro y nueva resolución de drivers de Storage en paquetes](https://kawax.biz/es/advanced/package-filesystem-drivers.md)
- [Extensión y nueva resolución de canales de log en paquetes](https://kawax.biz/es/advanced/package-logging.md)
- [Guía de actualización de Laravel 11 a 12](https://kawax.biz/es/blog/upgrade-11-to-12.md)


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