Looking to hire Laravel developers? Try LaraJobs

laravel-tag-aware-cache maintained by edemsky.m

Description
Tag-aware cache for Laravel powered by Symfony TagAwareAdapter (Redis and Memcached).
Last update
2026/10/29 14:33 (dev-main)
License
Downloads
0

Comments
comments powered by Disqus

Laravel Tag-Aware Cache

Latest Version Downloads Pipeline License

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 TagAwareAdapter under the hood.
  • Redis via Laravel's existing Redis connection (phpredis or Predis).
  • Memcached via Laravel's cache.stores.memcached configuration.
  • Explicit tags on write and invalidateTags() for invalidation.
  • Safe flush() through a global tag — no FLUSHDB, no SCAN of 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/cache 7.4 or 8.x
  • Redis (ext-redis or predis/predis) and/or ext-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 DefaultMarshaller uses unserialize() with allowed_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.