Caching

Introduction

Every production application reaches a point where the same data gets fetched over and over — the same database query, the same API response, the same computed result. Caching breaks that cycle. Instead of re-executing expensive operations on every request, you compute the result once, store it, and serve it instantly until it expires or is invalidated.

Doppar's cache system is built on top of Symfony's Cache component and implements the PSR-16 SimpleCache standard. This means the entire system is driven by a clean, well-defined interface — Psr\SimpleCache\CacheInterface — so your application code stays decoupled from the underlying storage mechanism. Swap from file to redis to apc in a single config line, with zero changes to your application logic.

Configuration

The cache system is configured in config/caching.php. This file controls the default driver, all available store definitions, and the global key prefix used to prevent collisions between applications or environments sharing the same cache backend.

Set your preferred driver in .env:

CACHE_DRIVER=redis
CACHE_PREFIX=myapp_

Supported Drivers

Doppar ships with four first-class cache backends out of the box:

Driver Backed By Best For
file Filesystem Simple deployments, local development
redis Redis (in-memory) Production — high throughput, shared state
array PHP array (request-scoped) Testing — no persistence, no side effects
apc APCu PHP extension Single-server deployments with shared memory

All drivers expose the same API. Your application never needs to know which driver is active. You can also register a completely custom driver — see Custom Cache Drivers below.


Clearing the Cache via CLI

To purge the entire cache store from the command line:

php pool cache:clear

This is useful during deployments, after configuration changes, or whenever stale cached data needs to be discarded across the board.

Basic Operations

Doppar exposes two equally valid ways to interact with the cache: the Cache facade and the cache() global helper. Both resolve the same underlying CacheStore instance.

Storing an Item

Store a value for 60 seconds

use Phaseolies\Support\Facades\Cache;

Cache::set('username', 'Alice', 60);

The third argument is the TTL — time-to-live in seconds. Pass a \DateInterval instance instead of an integer if you prefer that style. Omit the TTL entirely to store without expiration (equivalent to forever).

Retrieving an Item

$username = Cache::get('username');

Provide a default value returned when the key is missing

$username = Cache::get('username', 'Guest');

If the key does not exist or has expired, the default is returned. No exception is thrown.

Checking Existence

You can check item exists or not using has function like this way.

if (Cache::has('username')) {
    // Key exists and has not expired
}

Deleting an Item

You can delete item with delete function

Cache::delete('username');

forget() is an alias — returns false if key was not present

Cache::forget('username');

Working with Multiple Items

When you need to read or write several keys in one logical operation, the batch methods avoid the overhead of multiple round-trips to the cache backend.

Storing Multiple Items

Cache::setMultiple([
    'config.theme'    => 'dark',
    'config.language' => 'en',
    'config.timezone' => 'Asia/Dhaka',
], ttl: 3600);

All keys share the same TTL. Pass null to store without expiration.

Retrieving Multiple Items

$values = Cache::getMultiple(
    ['config.theme', 'config.language', 'config.timezone'],
    default: 'unknown'
);

Returns an associative array:

['config.theme' => 'dark', 'config.language' => 'en', ...]

Missing keys receive the default value

Deleting Multiple Items

Delete multiple item using deleteMultiple function

Cache::deleteMultiple(['config.theme', 'config.language']);

Numeric Operations

For keys that hold integer values, Doppar provides atomic increment and decrement operations. These are particularly useful for counters, hit tracking, and lightweight rate-limiting logic.

Increment by 1 (default)

Cache::increment('page_views');

Increment by a custom step

Cache::increment('api_calls', 5);

Decrement by 1

Cache::decrement('credits');

Decrement by a custom step

Cache::decrement('credits', 10);

Both methods return the new value after the operation, or false if the key does not exist. They preserve the original TTL of the item — incrementing a counter does not reset its expiration.

Permanent & Conditional Storage

Store if Absent

add() writes the value only when the key does not already exist in the cache. If the key is present and unexpired, the call is a no-op and returns false.

Set only if 'lock_key' is not already cached

$stored = Cache::add('lock_key', 'processing', ttl: 30);

if ($stored) {
    // We set it — we own this window
}

This is useful for lightweight advisory locks or one-time initialization flags without the overhead of a full AtomicLock.

Store Forever

forever() stores a value with no expiration. It will remain in the cache until explicitly deleted or the store is cleared.

Cache::forever('feature_flags', $flags);

Use this for data that is invalidated by an explicit event (a deploy, an admin action) rather than by the passage of time.

Stash Helpers

The stash family of methods encapsulates the most common caching pattern — "give me the cached value, or compute and store it if it doesn't exist" — in a single, expressive call. They eliminate the check-get-set boilerplate that clutters most caching code.

stash() — Cache with TTL

Retrieve the value for key. If it does not exist, execute the callback, cache the result for the given TTL, and return it.

$users = Cache::stash('users.all', ttl: 300, callback: fn() => User::all());
Parameter Type Description
key string The cache key
ttl int|DateInterval How long to cache the result
callback Closure Executed only on a cache miss; its return value is stored

The callback is never executed on a cache hit — only the stored value is returned. This is the method you will reach for in the vast majority of caching scenarios.

#[Route(uri: 'products', methods: ['GET'])]
public function index(): mixed
{
    $products = Cache::stash(
        'products.active',
        ttl: 600,
        callback: fn() => Product::where('active', true)->get()
    );

    return response()->json($products);
}

stashForever() — Cache Indefinitely

Same as stash(), but the result is stored without an expiration time. Useful for static reference data that only changes on a deliberate admin action.

$countries = Cache::stashForever(
    'ref.countries',
    fn() => Country::orderBy('name')->get()
);

The cache entry persists until Cache::forget('ref.countries') or Cache::clear() is called.

stashWhen() — Conditional Caching

Cache the result only when a runtime condition is true. When the condition is false, the callback is executed and its result returned directly — nothing is written to the cache.

$results = Cache::stashWhen(
    key: 'search.' . md5($query),
    callback:  fn() => Search::run($query),
    condition: $request->wantsJson() && !$request->has('nocache'),
    ttl:       120
);
Parameter Type Description
key string The cache key
callback Closure The computation to run
condition bool If false, callback runs but result is not cached
ttl int|DateInterval|null TTL when caching; null stores forever when condition is true

This is ideal for scenarios where caching should be skipped for authenticated users, debug requests, or specific runtime flags.

Inspecting the Active Adapter

To confirm which cache adapter is currently running — useful in debugging or environment verification:

use Phaseolies\Support\Facades\Cache;

ddd(Cache::getAdapter());

This returns the raw Symfony AdapterInterface instance. For example, when redis is the active driver you will see:

Symfony\Component\Cache\Adapter\RedisAdapter {#...}

Custom Cache Drivers

Doppar's CacheServiceProvider exposes an extend() method that lets you register a factory closure for any custom driver name. The factory receives the store config array and must return a Symfony AdapterInterface instance.

In a ServiceProvider boot() method

$cacheProvider = $this->app->make(\Phaseolies\Providers\CacheServiceProvider::class);

$cacheProvider->extend('dynamodb', function (array $config) {
    return new DynamoDbCacheAdapter(
        client: new DynamoDbClient($config),
        table:  $config['table'],
    );
});

Then reference it in config/caching.php:

'stores' => [
    'dynamodb' => [
        'driver' => 'dynamodb',
        'table'  => env('DYNAMODB_CACHE_TABLE', 'cache'),
        'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
    ],
],

Direct Store Instantiation

In rare cases — custom scripts, standalone tools, or package internals — you may want to instantiate a CacheStore directly without going through the service container:

use Phaseolies\Cache\CacheStore;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;

$adapter = new FilesystemAdapter('my_prefix_', 0, '/tmp/cache');
$store   = new CacheStore($adapter, 'my_prefix_');

$store->set('build_id', 'abc123', 3600);
$buildId = $store->get('build_id');

The CacheStore constructor accepts any AdapterInterface-compatible adapter, making it trivially portable to any Symfony Cache-backed infrastructure.


Method Reference

Method Returns Description
set(key, value, ttl?) bool Store a value with optional TTL
get(key, default?) mixed Retrieve a value; return default on miss
has(key) bool Check if a non-expired key exists
delete(key) bool Remove a key
forget(key) bool Alias for delete(); returns false if key was absent
clear() bool Purge all keys from the store
setMultiple(values, ttl?) bool Store multiple key-value pairs
getMultiple(keys, default?) iterable Retrieve multiple keys
deleteMultiple(keys) bool Remove multiple keys
increment(key, value?) int|false Increment a numeric value; preserves TTL
decrement(key, value?) int|false Decrement a numeric value; preserves TTL
add(key, value, ttl?) bool Store only if key does not already exist
forever(key, value) bool Store without expiration
stash(key, ttl, callback) mixed Get or compute-and-cache with TTL
stashForever(key, callback) mixed Get or compute-and-cache indefinitely
stashWhen(key, callback, condition, ttl?) mixed Conditionally get or compute-and-cache
locked(name, seconds, owner?) AtomicLock Acquire an atomic lock
restoreLock(name, owner) AtomicLock Restore a lock by owner token
getAdapter() AdapterInterface Return the underlying Symfony adapter instance
v3.x Last updated: Mar 19, 2026