Skip to main content
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. For preparing a provider, see Package development.

Separate the driver name from the store name

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.

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.
Load the provider through a mechanism such as package discovery. 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.
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.
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. 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.
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.
$same is the same object as $before. After forgetDriver(), the next retrieval creates $after from the new configuration. 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, 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, 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).
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

Last modified on October 29, 2026