edemsky.m / laravel-tag-aware-cache
Tag-aware cache for Laravel powered by Symfony TagAwareAdapter (Redis and Memcached).
Package info
gitlab.com/edemsky.m/laravel-tag-aware-cache
pkg:composer/edemsky.m/laravel-tag-aware-cache
Requires
- php: ^8.2
- illuminate/support: ^12.0|^13.0
- symfony/cache: ^7.4|^8.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
- predis/predis: ^2.2|^3.0
Suggests
- ext-igbinary: Allows igbinary serialization for cached values.
- ext-memcached: Required when using the memcached driver.
- ext-redis: Required when using the redis driver with phpredis.
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-29 10:58:00 UTC
README
Tag-aware cache for Laravel powered by Symfony's TagAwareAdapter.
The package provides a standalone TagAwareCache facade and a small, explicit API for caching values
with tags. Tag invalidation is O(number of tags), not O(number of cached items), and works with Redis
and Memcached.
Features
- Symfony
TagAwareAdapterunder the hood. - Redis via Laravel's existing Redis connection (phpredis or Predis).
- Memcached via Laravel's
cache.stores.memcachedconfiguration. - Explicit tags on write and
invalidateTags()for invalidation. - Safe
flush()through a global tag — noFLUSHDB, noSCANof every key. remember()with Symfony's probabilistic early-expiration (beta) stampede protection.- Deterministic key/tag normalization for FQCNs, colons and other PSR-6 reserved characters.
- Configurable fallback when the cache backend is unavailable.
- Laravel 12–13, PHP 8.2+.
- Pest/Testbench tests, Larastan level 8 and GitLab CI.
Requirements
- PHP 8.2+
- Laravel 12 or 13
symfony/cache7.4 or 8.x- Redis (
ext-redisorpredis/predis) and/orext-memcached - Optional:
ext-igbinary
Installation
composer require edemsky.m/laravel-tag-aware-cache
The service provider is auto-discovered. Publish the configuration:
php artisan vendor:publish --tag=tag-aware-cache-config
Configuration
// config/tag-aware-cache.php
return [
'enabled' => env('TAG_AWARE_CACHE_ENABLED', true),
'driver' => env('TAG_AWARE_CACHE_DRIVER', 'redis'),
'redis' => [
'connection' => env('TAG_AWARE_CACHE_REDIS_CONNECTION', 'cache'),
'dsn' => env('TAG_AWARE_CACHE_REDIS_DSN'),
],
'memcached' => [
'store' => env('TAG_AWARE_CACHE_MEMCACHED_STORE', 'memcached'),
'persistent_id' => env('TAG_AWARE_CACHE_MEMCACHED_PERSISTENT_ID', 'tag-aware-cache'),
],
'prefix' => env('TAG_AWARE_CACHE_PREFIX', 'tag_aware_cache'),
'default_ttl' => env('TAG_AWARE_CACHE_TTL', 3600),
'flush_tag' => '__all__',
'known_tag_versions_ttl' => (float) env('TAG_AWARE_CACHE_KNOWN_TAG_VERSIONS_TTL', 0.15),
'fallback' => env('TAG_AWARE_CACHE_FALLBACK', 'array'),
'logging' => [
'enabled' => env('TAG_AWARE_CACHE_LOG', false),
],
'marshaller' => [
'igbinary' => env('TAG_AWARE_CACHE_IGBINARY', false),
'throw_on_serialization_failure' => env('TAG_AWARE_CACHE_THROW_ON_SERIALIZATION_FAILURE', true),
],
'keys' => [
'hash' => env('TAG_AWARE_CACHE_HASH_KEYS', false),
'max_length' => 200,
],
'tags' => [
'hash' => env('TAG_AWARE_CACHE_HASH_TAGS', false),
],
];
Keep prefix unique per application and environment. It isolates this package's keys from other
applications sharing the same Redis database.
Failure modes
fallback | Behavior |
|---|---|
array (default) | Switch to an in-process array cache for the rest of the request; the application keeps working |
throw | Throw CacheBackendException |
none | Disable storage for the rest of the request; remember() computes the value every time |
Cache failures never swallow exceptions thrown by your own remember() callback.
Usage
use EdemskyM\LaravelTagAwareCache\Facades\TagAwareCache;
$products = TagAwareCache::remember(
key: 'products:list:page:1',
ttl: 600,
callback: fn () => Product::query()->paginate(20),
tags: ['products', 'products:list'],
);
TagAwareCache::invalidateTags(['products']);
The facade is not registered as a global alias. Import its FQCN in each file (or add
TagAwareCache to config/app.php aliases yourself).
API
| Method | Description |
|---|---|
get(string $key, mixed $default = null) | Read a value |
getMany(array $keys) | Read multiple values; missing keys are null |
has(string $key) | Check whether a key is a hit |
put(string $key, mixed $value, $ttl = null, $tags = []) | Store a value |
putMany(array $values, $ttl = null, $tags = []) | Store multiple values |
forever(string $key, mixed $value, $tags = []) | Store without expiration |
remember(string $key, $ttl, callable $callback, $tags = [], float $beta = 1.0) | Read or compute and store |
rememberForever(string $key, callable $callback, $tags = []) | Read or compute and store without expiration |
forget(string $key) | Delete one key |
forgetMany(array $keys) | Delete multiple keys |
invalidateTags(array\|string $tags) | Invalidate all values with any of the tags |
flush() | Invalidate all values written by this package |
pool() | Access the underlying TagAwareAdapter |
$ttl accepts seconds (int), DateInterval, DateTimeInterface or null (use the configured
default). A non-positive TTL deletes the value instead of storing it.
null, false, 0 and '' are valid cached values. get() returns null both for a miss and for a
cached null; use has() when you need to distinguish them.
Dependency injection
use EdemskyM\LaravelTagAwareCache\Contracts\TagAwareCache as TagAwareCacheContract;
final class ProductController
{
public function __construct(private readonly TagAwareCacheContract $cache) {}
}
Tags
Tags are plain strings. A value can have multiple tags; invalidating any one of them invalidates the value. Tags are only needed when writing and invalidating — reads are by key.
Tags containing FQCNs, colons, slashes or other PSR-6 reserved characters are normalized
deterministically. The normalized tag includes a short hash when the original string changes, so
App\Models\Product:1 and App\Models\Product-1 do not collide.
Use the Tag helper to build model tags:
use EdemskyM\LaravelTagAwareCache\Helpers\Key;
use EdemskyM\LaravelTagAwareCache\Helpers\Tag;
TagAwareCache::put(
key: Key::model($product, 'price'),
value: $price,
ttl: 600,
tags: [
Tag::entity(Product::class),
Tag::model($product),
Tag::list(Product::class),
],
);
Keys
use EdemskyM\LaravelTagAwareCache\Helpers\Key;
Key::make('products', ['page' => 1, 'sort' => 'name']);
// products:{"page":1,"sort":"name"}
Key::model($product, 'price');
// App\Models\Product:42:price
Keys are normalized by the manager; you can also pass a valid key directly.
Redis
By default the package reuses the Laravel Redis connection from config/database.php:
// config/database.php
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'cache' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
],
],
TAG_AWARE_CACHE_DRIVER=redis
TAG_AWARE_CACHE_REDIS_CONNECTION=cache
If you use a dedicated connection, leave Laravel's serializer and compression options empty to
avoid double serialization by both Laravel and Symfony.
For a standalone connection, set TAG_AWARE_CACHE_REDIS_DSN. Cluster and sentinel configuration is
inherited from Laravel when using a connection name.
Memcached
The package reads the servers, options and sasl settings from an existing Memcached store:
// config/cache.php
'stores' => [
'memcached' => [
'driver' => 'memcached',
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
'sasl' => [
env('MEMCACHED_USERNAME'),
env('MEMCACHED_PASSWORD'),
],
'options' => [],
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
'port' => env('MEMCACHED_PORT', '11211'),
'weight' => 100,
],
],
],
],
TAG_AWARE_CACHE_DRIVER=memcached
TAG_AWARE_CACHE_MEMCACHED_STORE=memcached
The package uses a dedicated persistent_id by default (tag-aware-cache) so its binary protocol and
serializer settings do not interfere with Laravel's own MemcachedStore.
Memcached caveat: Symfony recommends Redis for tag-aware caches. Memcached works, but tag-version keys can be evicted by LRU, which causes mass cache misses (never stale reads). Make sure Memcached has enough memory and monitor evictions.
Flush semantics
flush() invalidates an internal global tag (__all__ by default), not the whole Redis database.
This is O(1), works the same on Redis and Memcached, and does not touch other applications.
Values written directly through pool() bypass the manager's automatic global tag, so flush() will
not affect them. Use the manager API (or add the global tag yourself) for values that must be flushed.
If you change flush_tag after deployment, values written under the old tag will not be affected by
flush() until they expire. Keep it stable.
Octane and long-running workers
The adapter and manager are registered as scoped bindings, so Octane gets fresh instances per request.
known_tag_versions_ttl controls the in-process tag-version cache:
0.15(default) — fewer MGETs, but up to 150 ms of staleness after invalidation in another process;0.0— strict freshness, at the cost of an extra tag lookup per read.
For queue workers, keep the default unless strict freshness is more important than the extra reads.
Cache::tags()
Cache::tags() compatibility is not part of v1. It is planned for v2.0 as an opt-in Laravel cache
driver and will not change the default store of the application.
Testing
composer install
composer validate --strict
vendor/bin/pint --test
vendor/bin/phpstan analyse
vendor/bin/pest
Redis integration tests:
TAG_AWARE_CACHE_TEST_REDIS=1 vendor/bin/pest tests/Integration/RedisTest.php
Memcached integration tests require a running Memcached server:
TAG_AWARE_CACHE_TEST_MEMCACHED=1 vendor/bin/pest tests/Integration/MemcachedTest.php
Security
- Keep Redis/Memcached on a private network and use authentication/TLS where possible.
- Do not cache secrets or personal data without encryption.
- Symfony's
DefaultMarshallerusesunserialize()withallowed_classes => true; a compromised cache backend can inject objects. Treat the cache as trusted infrastructure. - The package never logs DSNs or passwords.
Documentation
Changelog
See CHANGELOG.md.
Contributing
See CONTRIBUTING.md.
License
MIT. See LICENSE.