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

# Custom cache stores and contracts in packages

> Examine Laravel 13's CacheManager and Store contract to understand when to register drivers, how the Repository converts TTLs, how instances are retained, and what support for features such as locks requires.

To provide your own cache backend from a package, separate **the Store, which implements storage**, from **the Repository, which provides Laravel's API**. What you register with `Cache::extend()` is a creation method; Laravel does not automatically wrap a returned Store in a Repository.

This page is based on the implementation of Laravel Framework `v13.39.0`. For basic usage, see [Cache](https://kawax.biz/en/cache.md). For preparing a provider, see [Package development](https://kawax.biz/en/advanced/package-development.md).

## Separate the driver name from the store name

| Element | Example on this page | Role |
| - | - | - |
| Driver name | `acme-memory` | The creation method registered with `extend()` |
| Store name | `catalog` | The configuration defined in `cache.stores`, and the identifier of the created Repository |
| Store | `Illuminate\Cache\ArrayStore` | Storing and retrieving keys, and managing expiration |
| Repository | `Illuminate\Cache\Repository` | `remember()`, TTL conversion, cache events, and more |

If a custom creator exists for the configured `driver`, `CacheManager::build()` calls it before any built-in driver. Do not reuse names such as `redis` or `array`; use a package-specific name.

```mermaid theme={null}
flowchart TD
    A["Cache::store('catalog')"] --> B{"Is catalog already resolved?"}
    B -->|Yes| C["Return the retained Repository"]
    B -->|No| D["Load cache.stores.catalog"]
    D --> E["Creator for driver: acme-memory"]
    E --> F["Create the Store and<br>wrap it with repository()"]
    F --> G["Retain it under the name catalog"]
```

## Register before other providers boot

If another package uses the cache in its `boot()`, registering your driver in your own `boot()` may be too late, depending on the order. Following the official documentation, register an **application `booting` callback** in `register()`.

The following minimal example uses the standard `ArrayStore` so you can verify the registration path without an external service. Place it in `src/MemoServiceProvider.php` of the `Acme\Memo` package.

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

Load the provider through a mechanism such as [package discovery](https://kawax.biz/en/advanced/package-discovery.md). Because this provider only registers an extension, do not make it deferred; make sure it is loaded for both HTTP and Artisan.

* During a normal boot, the application's `booting` callbacks run after every provider's `register()` has completed and before any `boot()`. This is different from the provider's own `$this->booting()`.
* The `extend()` callback receives the application and the store configuration array. Put Store creation and external connections in this creator callback.
* This registration timing does not guarantee **cache usage inside other providers' `register()`**. Keep service registration separate from actual usage.

> [!TIP]
>
> If you only need the standard in-memory cache, the `array` driver is enough. This example explains registration and contracts; it cannot be used for sharing across processes or for persistence. For your own backend, replace the `ArrayStore` creation with a class that implements `Illuminate\Contracts\Cache\Store`.

`CacheManager::extend()` rebinds the scope of the anonymous closure to the Manager. Do not assume `$this` in a non-static closure refers to the provider; use a static closure and the `$app` argument, as in the example.

### Add the store to the consuming application

Add it to the existing `stores` array in `config/cache.php`. Do not design the package to overwrite the consumer's `default` or the entire `stores` array.

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

To make it the default, set `CACHE_STORE` to the **store name `catalog`**, not the driver name. If you create multiple configurations for the same driver, a separate Repository is created for each store name.

## Build the Repository yourself

`CacheManager::callCustomCreator()` returns the callback's return value as is. If you return only a Store, `get()` and `put()` may work, but Repository methods such as `remember()` are unavailable and TTLs are interpreted differently.

In the example, `repository($store, $config)` is responsible for the following:

* Wrapping the Store in `Illuminate\Cache\Repository`.
* Passing the `store` name that the Manager added to the configuration to the Repository.
* Setting the event Dispatcher on the Repository if one is registered and the `events` setting is not disabled.

This does not apply all of your custom settings to the Store. Apply settings such as the connection target, prefix, and serialization in the Store implementation and the creator. The same goes for `serialize`, which the example reads from the configuration.

## Check the Store contract against the current implementation

Implement the methods required by Laravel 13's `Illuminate\Contracts\Cache\Store`. Do not distribute a list of empty methods from an old sample as a working driver.

| Method | Behavior to verify |
| - | - |
| `get()` / `many()` | Return `null` for missing or expired items. `many()` returns an array keyed by the requested keys, with `null` for missing items |
| `put()` / `putMany()` | Store with a TTL in seconds and return success or failure |
| `forever()` | Store without an expiration. This does not mean preventing eviction or deletion by the backend |
| `touch()` | Change the expiration of an existing key. Return `false` if the item is missing or expired |
| `increment()` / `decrement()` | Return the updated value or failure. Also verify concurrent updates and TTL preservation |
| `forget()` | Delete the key and return the result |
| `flush()` | Delete the entire cache. Make clear what is actually deleted |
| `getPrefix()` | Return the key prefix used by the Store |

The Repository treats `null` as a cache miss. Do not treat `false` or `0` as missing in your implementation. Storing `null` does not act as a marker that prevents `remember()` from recomputing.

### Verify TTLs through the Repository

`Repository::put()` processes the TTL before passing it to the Store.

| Call to the Repository | Action on the Store |
| - | - |
| `put('key', $value)` | `forever()` |
| `put('key', $value, 60)` | `put()` with the number of seconds |
| `put('key', $value, $expiresAt)` | Convert a date or similar value to seconds, then `put()` if positive |
| `put('key', $value, 0)` or a negative TTL | `forget()` |
| `touch('key', 60)` | `touch()` |
| `touch('key', 0)` or a negative TTL | `forget()` |

> [!WARNING]
>
> The example's `ArrayStore` stores an item indefinitely when you pass `0` seconds directly to `put()`. By contrast, the Repository's `put(..., 0)` deletes the item. Testing only the low-level Store retrieved with `getStore()` does not verify the behavior of the API that users call.

### Check support for add, locks, and tags separately

Implementing the Store contract alone does not guarantee support for every cache feature.

* **Atomic `add()`**: This is not part of the Store contract. When called with a positive TTL, the Repository uses the Store's `add()` if it exists. Otherwise, it falls back to `get()` and `put()`, so wrapping the Store in a Repository does not guarantee atomicity. For a shared backend, implement it with the backend's atomic operations and verify concurrent execution.
* **Locks**: `lock()` and `restoreLock()` in `Illuminate\Contracts\Cache\LockProvider` are a separate contract. Verify acquisition, owner matching, expiration, and that locks held by other owners are not released. The example's `ArrayStore` locks do not provide mutual exclusion across processes.
* **Tags**: The Repository checks whether the Store has `tags()`. This generally requires an implementation using something like `TaggableStore`; the Store contract alone does not enable it automatically.

Do not assume `flush()` is a partial deletion that respects the configured prefix. Check what is deleted on a shared backend and explain it to users, including `php artisan cache:clear`.

## Distinguish instance retention from data deletion

`CacheManager` is a singleton, and it also retains the result of `store('catalog')` per store name. Changing the configuration or re-registering with `extend()` does not update an already created Repository.

```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` is the same object as `$before`. After `forgetDriver()`, the next retrieval creates `$after` from the new configuration.

| Operation | Target |
| - | - |
| `Cache::forgetDriver('catalog')` | Removes the reference to the Repository retained by the Manager |
| `Cache::store('catalog')->forget('key')` | Deletes one stored key |
| `Cache::store('catalog')->flush()` | Deletes all cache data in the Store |

`forgetDriver()` does not delete everything in the backend. However, because the example's `ArrayStore` holds data inside the instance, the old data is not visible from the new Store. Also, references to the old Repository held by the consumer's `$before` or by an already resolved `cache.store` singleton are not replaced.

In production, refresh the configuration cache and restart long-running workers as part of your normal deployment. Also consider [service lifetimes](https://kawax.biz/en/advanced/package-service-lifetimes.md), and do not treat re-resolution as a bulk update across all processes.

## Test the creation path, not just facade mocks

In your [package test environment](https://kawax.biz/en/advanced/package-testing.md), go through the real Manager and Repository. The following Testbench example verifies that the driver is available even in the `boot()` of another provider registered earlier. The example stores an array without changing the `cache.serializable_classes` restoration restriction, and also checks the raw stored value with `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']);
    }
}
```

For your own backend, also verify expiration, missing keys in `many()`, round-tripping `false` and `0`, `touch()`, connection failures, concurrent updates, and the prefix and deletion scope. Do not treat a simple successful `Cache::shouldReceive('get')` as evidence of driver registration or compatibility with an external service.

## Long-term maintenance checklist

* Make the driver name, store configuration, serialization format, exceptions, and return values on failure part of the public specification.
* Check the Store contract methods and optional features such as tags and locks for each supported Laravel version.
* Verify that the driver registers on a fresh boot with a cached configuration and can be used from other providers' `boot()`.
* When announcing compatibility changes to stored data, include your policy for migrating or discarding existing keys.
* Distinguish in-process instance retention, backend data, and the configuration cache.

## Primary sources consulted

* [Laravel official documentation: Adding custom cache drivers](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/cache.md#adding-custom-cache-drivers)
* [CacheManager: build, repository, extend, forgetDriver](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/CacheManager.php)
* [Repository: TTL conversion, add, tags, events](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/Repository.php)
* [Store contract](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Contracts/Cache/Store.php) / [LockProvider contract](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Contracts/Cache/LockProvider.php)
* [ArrayStore: storage, expiration, serialization](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/ArrayStore.php)
* [CacheServiceProvider: singleton registration of the Manager and cache.store](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Cache/CacheServiceProvider.php)
* [Application: application booting and provider boot](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Foundation/Application.php)
* [Laravel official tests: 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

- [Advanced Topics](https://kawax.biz/en/advanced/index.md)
- [Cache](https://kawax.biz/en/cache.md)
- [Package Blade extensions and the compilation cache](https://kawax.biz/en/advanced/package-blade-extensions.md)
- [Integrating package caches with optimize](https://kawax.biz/en/advanced/package-optimization.md)
- [Contracts](https://kawax.biz/en/contracts.md)


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