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

# Stores de caché personalizados y contratos en paquetes

> Analiza el CacheManager y el contrato Store de Laravel 13, y verifica el momento de registro del driver, la conversión del TTL en el Repository, la retención de instancias y el alcance de funciones como los locks.

Para ofrecer un backend de caché propio desde un paquete, separa **el Store, que implementa el almacenamiento**, del **Repository, que ofrece la API de Laravel**. Lo que se registra con `Cache::extend()` es el método de creación, y Laravel no envuelve automáticamente en un Repository el Store que devuelvas.

Esta página se basa en la revisión de la implementación de Laravel Framework `v13.39.0`. Para las operaciones básicas, consulta [Caché](https://kawax.biz/es/cache.md), y para preparar el provider, [Desarrollo de paquetes](https://kawax.biz/es/advanced/package-development.md).

## Separa el nombre del driver del nombre del store

| Elemento | Ejemplo en esta página | Función |
| - | - | - |
| Nombre del driver | `acme-memory` | El método de creación que se registra con `extend()` |
| Nombre del store | `catalog` | La configuración definida en `cache.stores` y el identificador del Repository ya generado |
| Store | `Illuminate\Cache\ArrayStore` | Guarda y obtiene claves, y gestiona su expiración |
| Repository | `Illuminate\Cache\Repository` | `remember()`, conversión del TTL, eventos de caché, etc. |

Si existe un creador personalizado para el `driver` de la configuración, `CacheManager::build()` lo llama antes que a los drivers integrados. No reutilices nombres como `redis` o `array`; usa un nombre propio del paquete.

```mermaid theme={null}
flowchart TD
    A["Cache::store('catalog')"] --> B{"¿catalog ya está resuelto?"}
    B -->|Sí| C["Devuelve el Repository retenido"]
    B -->|No| D["Lee cache.stores.catalog"]
    D --> E["Creador de driver: acme-memory"]
    E --> F["Genera el Store y<br>lo envuelve con repository()"]
    F --> G["Lo retiene con el nombre catalog"]
```

## Regístralo antes del boot de otros providers

Si otro paquete usa la caché en su `boot()`, registrar el driver en tu propio `boot()` puede llegar tarde según el orden. Siguiendo la documentación oficial, registra en `register()` un **callback `booting` de la aplicación**.

El siguiente es un ejemplo mínimo que usa el `ArrayStore` estándar para comprobar el flujo de registro sin servicios externos. Colócalo en `src/MemoServiceProvider.php` del paquete `Acme\Memo`.

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

namespace Acme\Memo;

use Illuminate\Cache\ArrayStore;
use Illuminate\Cache\Repository;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\ServiceProvider;

class MemoServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->booting(static function (Application $app): void {
            $app['cache']->extend('acme-memory', static function (
                Application $app,
                array $config,
            ): Repository {
                $store = new ArrayStore(
                    $config['serialize'] ?? false,
                    $app['config']->get('cache.serializable_classes'),
                );

                return $app['cache']->repository($store, $config);
            });
        });
    }
}
```

El provider se carga, por ejemplo, mediante el [descubrimiento automático de paquetes](https://kawax.biz/es/advanced/package-discovery.md). Como este provider solo registra la extensión, no lo conviertas en un provider diferido, para que se cargue tanto en HTTP como en Artisan.

* El `booting` de la aplicación se ejecuta, en un arranque normal, después de que terminen los `register()` de todos los providers y antes de cada `boot()`. Es distinto del `$this->booting()` del propio provider.
* El callback de `extend()` recibe la aplicación y el array de configuración del store. Coloca la generación del Store y las conexiones externas en este callback de creación.
* Este momento de registro no garantiza el **uso de la caché dentro del `register()`** de otros providers. Separa el registro de servicios de su uso real.

> [!TIP]
>
> Si solo necesitas la caché en memoria estándar, basta con el driver `array`. Este ejemplo sirve para explicar el registro y los contratos, y no sirve para compartir ni persistir datos entre procesos. En un backend propio, sustituye la parte que genera el `ArrayStore` por una clase que implemente `Illuminate\Contracts\Cache\Store`.

`CacheManager::extend()` vuelve a enlazar el ámbito del closure anónimo al Manager. No asumas que `$this` en un closure no estático es el provider; usa, como en el ejemplo, un closure estático y el argumento `$app`.

### Añade el store a la aplicación que usa el paquete

Añádelo al array `stores` existente de `config/cache.php`. No diseñes el paquete para que sobrescriba el `default` ni todo el array `stores` de la aplicación.

```php theme={null}
'catalog' => [
    'driver' => 'acme-memory',
    'serialize' => true,
],
```

```php theme={null}
use Illuminate\Support\Facades\Cache;

$value = Cache::store('catalog')->remember('product:42', 60, function () {
    return ['id' => 42, 'name' => 'Sample'];
});
```

Si quieres usarlo como predeterminado, lo que se indica en `CACHE_STORE` es también **el nombre del store, `catalog`**, no el nombre del driver. Si creas varias configuraciones con el mismo driver, se genera un Repository distinto para cada nombre de store.

## Construye el Repository tú mismo

`CacheManager::callCustomCreator()` devuelve tal cual el valor de retorno del callback. Si devuelves solo el Store, aunque `get()` y `put()` funcionen, no podrás usar `remember()` y otros métodos del Repository, y la interpretación del TTL también cambia.

El `repository($store, $config)` del ejemplo se encarga de lo siguiente:

* Envuelve el Store con `Illuminate\Cache\Repository`.
* Pasa al Repository el nombre `store` que el Manager añadió a la configuración.
* Si hay un Dispatcher de eventos registrado y la opción `events` no está desactivada, asigna el Dispatcher al Repository.

No es un proceso que aplique toda tu configuración propia al Store. El destino de conexión, el prefix, la serialización, etc., se reflejan en la implementación del Store y en el proceso de creación. Lo mismo ocurre con `serialize`, que el ejemplo lee de la configuración.

## Contrasta el contrato Store con la implementación actual

Implementa los métodos que exige `Illuminate\Contracts\Cache\Store` en Laravel 13. No distribuyas como driver funcional una lista de métodos vacíos copiada de ejemplos antiguos.

| Método | Comportamiento que hay que verificar |
| - | - |
| `get()` / `many()` | Devuelve `null` si no existe o ha expirado. `many()` devuelve un array con las claves solicitadas como claves, y también `null` para las que no existen |
| `put()` / `putMany()` | Guarda con un TTL en segundos y devuelve éxito o fallo |
| `forever()` | Guarda sin fecha de expiración. No significa que impida la expulsión o eliminación por parte del backend |
| `touch()` | Cambia la expiración de una clave existente. Devuelve `false` si no existe o ha expirado |
| `increment()` / `decrement()` | Devuelve el valor actualizado o un fallo. Verifica también las actualizaciones concurrentes y la conservación del TTL |
| `forget()` | Elimina la clave y devuelve el resultado |
| `flush()` | Elimina toda la caché. Indica claramente el alcance real de lo que se borra |
| `getPrefix()` | Devuelve el prefijo de claves que usa el Store |

El Repository trata `null` como un fallo de caché. Implementa el Store de forma que `false` o `0` no se traten como inexistentes. Guardar `null` no sirve como marca para evitar que `remember()` vuelva a calcular el valor.

### Verifica el TTL a través del Repository

`Repository::put()` procesa el TTL antes de pasarlo al Store.

| Llamada al Repository | Operación en el Store |
| - | - |
| `put('key', $value)` | `forever()` |
| `put('key', $value, 60)` | `put()` con los segundos |
| `put('key', $value, $expiresAt)` | Convierte la fecha u otro valor a segundos y, si es positivo, `put()` |
| `put('key', $value, 0)` o TTL negativo | `forget()` |
| `touch('key', 60)` | `touch()` |
| `touch('key', 0)` o TTL negativo | `forget()` |

> [!WARNING]
>
> El `ArrayStore` del ejemplo guarda sin expiración si se pasan `0` segundos directamente a `put()`. En cambio, `put(..., 0)` del Repository elimina el elemento. Probar solo el Store de bajo nivel obtenido con `getStore()` no confirma el comportamiento de la API que llaman los usuarios.

### Comprueba por separado add, locks y etiquetas

Implementar el contrato Store no garantiza por sí solo la compatibilidad con todas las funciones de la caché.

* **`add()` atómico**: no forma parte del contrato Store. Si se llama con un TTL positivo y el Store tiene `add()`, el Repository lo usa. Si no, recurre a `get()` y `put()`, así que envolverlo en un Repository no garantiza la atomicidad. En backends compartidos, impleméntalo con las operaciones atómicas de ese backend y verifica también la ejecución concurrente.
* **Locks**: `lock()` y `restoreLock()` de `Illuminate\Contracts\Cache\LockProvider` son un contrato aparte. Verifica la adquisición, la coincidencia del propietario, la expiración y que no se liberen locks de otros propietarios. Los locks del `ArrayStore` del ejemplo no proporcionan exclusión mutua entre procesos distintos.
* **Etiquetas**: el Repository comprueba si el Store tiene `tags()`. Por lo general se necesita una implementación basada en `TaggableStore` u otra similar, y no se activan automáticamente solo con el contrato Store.

No asumas que `flush()` es un borrado parcial que respeta el prefix de la configuración. Comprueba el alcance del borrado en backends compartidos y explícalo a los usuarios, incluido `php artisan cache:clear`.

## Distingue la retención de instancias del borrado de datos

`CacheManager` es un singleton, y también retiene el resultado de `store('catalog')` por nombre de store. Cambiar la configuración o volver a registrar `extend()` no actualiza los Repository ya generados.

```php theme={null}
use Illuminate\Support\Facades\Cache;

$before = Cache::store('catalog');

config(['cache.stores.catalog.serialize' => false]);

$same = Cache::store('catalog');

Cache::forgetDriver('catalog');

$after = Cache::store('catalog');
```

`$same` es el mismo objeto que `$before`. Al obtenerlo después de `forgetDriver()`, `$after` se genera a partir de la nueva configuración.

| Operación | Objetivo |
| - | - |
| `Cache::forgetDriver('catalog')` | Elimina la referencia al Repository que retiene el Manager |
| `Cache::store('catalog')->forget('key')` | Elimina una clave guardada |
| `Cache::store('catalog')->flush()` | Elimina todos los datos de caché del Store |

`forgetDriver()` no borra todo el backend. Sin embargo, como el `ArrayStore` del ejemplo guarda los datos dentro de la instancia, el nuevo Store no ve los datos antiguos. Además, no se sustituyen el `$before` del código que lo usa ni las referencias al Repository antiguo que retiene el singleton `cache.store` ya resuelto.

En producción, actualiza la caché de configuración siguiendo el despliegue habitual y reinicia los workers de larga duración. Ten en cuenta también el [ciclo de vida de los servicios](https://kawax.biz/es/advanced/package-service-lifetimes.md), y no trates la nueva resolución como una actualización simultánea de todos los procesos.

## Prueba el flujo de creación, no solo mocks de facades

En el [entorno de pruebas del paquete](https://kawax.biz/es/advanced/package-testing.md), pasa por el Manager y el Repository reales. El siguiente ejemplo de Testbench comprueba que el driver también puede usarse desde el `boot()` de otro provider registrado antes. En el ejemplo se guarda un array sin cambiar la restricción de restauración de `cache.serializable_classes`, y se comprueba también el valor guardado en bruto con `ArrayStore::all(false)`.

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

namespace Acme\Memo\Tests;

use Acme\Memo\MemoServiceProvider;
use Illuminate\Cache\ArrayStore;
use Illuminate\Cache\Repository;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\ServiceProvider;
use Orchestra\Testbench\TestCase;

class CacheConsumerProvider extends ServiceProvider
{
    public function boot(): void
    {
        Cache::store('catalog')->forever('boot-marker', 'ready');
    }
}

class PackageCacheTest extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [CacheConsumerProvider::class, MemoServiceProvider::class];
    }

    protected function defineEnvironment($app): void
    {
        $app['config']->set('cache.default', 'array');
        $app['config']->set('cache.stores.catalog', [
            'driver' => 'acme-memory',
            'serialize' => true,
        ]);
    }

    public function test_the_driver_is_available_during_another_providers_boot(): void
    {
        $cache = Cache::store('catalog');

        $this->assertInstanceOf(Repository::class, $cache);
        $this->assertInstanceOf(ArrayStore::class, $cache->getStore());
        $this->assertSame('catalog', $cache->getName());
        $this->assertSame('ready', $cache->get('boot-marker'));
        $this->assertNotNull($cache->getEventDispatcher());
    }

    public function test_serialization_is_applied_from_the_store_configuration(): void
    {
        $cache = Cache::store('catalog');
        $value = ['status' => 'queued'];

        $cache->put('job', $value, 60);

        $this->assertSame($value, $cache->get('job'));
        $this->assertSame(serialize($value), $cache->getStore()->all(false)['job']['value']);
    }

    public function test_a_zero_ttl_deletes_the_item_through_the_repository(): void
    {
        $cache = Cache::store('catalog');
        $cache->forever('product:42', 'old');

        $this->assertTrue($cache->put('product:42', 'new', 0));
        $this->assertNull($cache->get('product:42'));
    }

    public function test_forgetting_a_driver_rebuilds_only_the_manager_reference(): void
    {
        $before = Cache::store('catalog');

        config(['cache.stores.catalog.serialize' => false]);
        $this->assertSame($before, Cache::store('catalog'));

        Cache::forgetDriver('catalog');
        $after = Cache::store('catalog');

        $this->assertNotSame($before, $after);
        $this->assertSame('ready', $before->get('boot-marker'));
        $this->assertNull($after->get('boot-marker'));

        $value = ['status' => 'queued'];
        $after->forever('job', $value);

        $this->assertSame($value, $after->get('job'));
        $this->assertSame($value, $after->getStore()->all(false)['job']['value']);
    }
}
```

En un backend propio, verifica además la expiración, las claves inexistentes en `many()`, el ida y vuelta de `false` y `0`, `touch()`, los fallos de conexión, las actualizaciones concurrentes, y el prefix y el alcance del borrado. Que un simple `Cache::shouldReceive('get')` tenga éxito no es prueba del registro del driver ni de la compatibilidad con servicios externos.

## Lista de comprobación para el mantenimiento a largo plazo

* Haz que el nombre del driver, la configuración del store, el formato de serialización, las excepciones y los valores de retorno en caso de fallo formen parte de la especificación pública.
* Comprueba los métodos del contrato Store y las funciones opcionales, como etiquetas y locks, para cada versión de Laravel compatible.
* Verifica que el driver se registra incluso en un arranque nuevo con la caché de configuración activa, y que puede usarse desde el `boot()` de otros providers.
* Al anunciar cambios de compatibilidad en los datos guardados, incluye la política de migración o descarte de las claves existentes.
* Distingue la retención de instancias dentro del proceso, los datos del backend y la caché de configuración.

## Fuentes primarias consultadas

* [Documentación oficial de Laravel: drivers de caché personalizados](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/cache.md#adding-custom-cache-drivers)
* [CacheManager: build, repository, extend y forgetDriver](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/CacheManager.php)
* [Repository: conversión del TTL, add, tags y eventos](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/Repository.php)
* [Contrato Store](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Contracts/Cache/Store.php) / [Contrato LockProvider](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Contracts/Cache/LockProvider.php)
* [ArrayStore: almacenamiento, expiración y serialización](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/ArrayStore.php)
* [CacheServiceProvider: registro singleton del Manager y de cache.store](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/CacheServiceProvider.php)
* [Application: booting de la aplicación y boot de los providers](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Foundation/Application.php)
* [Pruebas oficiales de Laravel: CacheManagerTest](https://github.com/laravel/framework/blob/v13.39.0/tests/Cache/CacheManagerTest.php) / [CacheRepositoryTest](https://github.com/laravel/framework/blob/v13.39.0/tests/Cache/CacheRepositoryTest.php)


## Related topics

- [Temas avanzados](https://kawax.biz/es/advanced/index.md)
- [Registro de rutas y caché en paquetes](https://kawax.biz/es/advanced/package-routes.md)
- [Registro y caché de listeners de eventos en paquetes](https://kawax.biz/es/advanced/package-events.md)
- [Extensiones de Blade en paquetes y caché de compilación](https://kawax.biz/es/advanced/package-blade-extensions.md)
- [Canales de notificación personalizados en paquetes y verificación de la entrega](https://kawax.biz/es/advanced/package-notification-channels.md)


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