Search by

thekaziomar / laravel-atomic-cache

thekaziomar

A simple, stampede-proof, and tag-aware caching package for Laravel.

Package info

github.com/kaziomarsh24/laravel-atomic-cache

pkg:composer/thekaziomar/laravel-atomic-cache

Statistics

Installs: 7

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.1 2026-10-24 18:41 UTC

This package is auto-updated.

Last update: 2026-10-24 19:11:15 UTC


README

Run Tests Latest Version on Packagist Total Downloads PHP Version License

A high-performance, stampede-proof caching package for Laravel. Designed for modern web applications handling concurrent bursts, large payloads, multi-table model invalidations, and non-Redis environments.

use KaziOmar\AtomicCache\Facades\AtomicCache;

// Standard caching with auto-compression (>50KB) & self-healing
$users = AtomicCache::remember('users.active', 3600, fn() => User::where('active', 1)->get());

// Cache stampede protection (atomic lock so 1000 concurrent requests only run 1 query)
$report = AtomicCache::rememberWithLock('monthly_report', fn() => Report::generate());

๐Ÿ“‘ Table of Contents

๐Ÿ’ก Why Laravel Atomic Cache?

Feature Standard Laravel Cache Laravel Atomic Cache
Stampede Protection โŒ All concurrent requests hit DB โœ… Atomic Lock (1 worker computes, others wait & read)
Large Payload Size โŒ Raw uncompressed serialization โœ… Auto Gzip (>50KB cuts Redis RAM by ~70%)
Driver Tag Support โŒ Crashes on File/Database stores โœ… Virtual Tagging (works across all stores without Redis)
Corrupted Data โŒ Throws crash exception to user โœ… Self-Healing (cleans key, regenerates silently)
Simultaneous Expiry โŒ All keys expire in same second โœ… TTL Jitter (random variance prevents herd spikes)
Model Invalidation โš ๏ธ Fragile observers before commit โœ… DB::afterCommit (prevents transaction race conditions)
Pagination Caching โš ๏ธ Page 1 and 2 collide unless manual โœ… Auto Query Hash (page/filter automatically isolated)

โš™๏ธ Requirements

  • PHP: 8.2, 8.3, 8.4, 8.5, or 8.6
  • Laravel Framework: 10.x, 11.x, 12.x, or 13.x
  • PHP Extensions: ext-zlib (optional for compression, built-in on 99% of PHP installs)

๐Ÿ“ฆ Installation

Install via Composer:

composer require thekaziomar/laravel-atomic-cache

Publish the optional configuration file:

php artisan vendor:publish --tag="atomic-cache-config"

๐Ÿš€ Core Features & Usage

1. Basic Caching

Works seamlessly like native Laravel cache, supporting flexible parameter ordering:

use KaziOmar\AtomicCache\Facades\AtomicCache;

// Standard signature: remember($key, $ttl, $callback)
$posts = AtomicCache::remember('posts.all', 3600, function () {
    return Post::where('published', true)->get();
});

// Alternative signature: remember($key, $callback, $ttl)
$posts = AtomicCache::remember('posts.all', fn() => Post::all(), 3600);

// Global helper shortcut
$posts = atomic_cache('posts.all', fn() => Post::all(), 3600);

Understanding the Parameters:

Parameter Type Description
$key string Unique Cache Identifier in your cache store (e.g. 'posts.all', 'home.banners'). Using {resource}.{action} or {module}:{query} is the industry standard convention to prevent key collisions and enable easy searching in Redis (redis-cli KEYS "atomic:posts.*").
$ttl int Time-To-Live in seconds (e.g. 3600 = 1 hour, 86400 = 1 day). Specifies how long the value remains cached. If omitted, uses config('atomic-cache.default_ttl', 3600).
$callback Closure Data Producer Callback. Executes only when the cache is empty or expired. The returned value is automatically compressed (if >50KB), stored in cache, and returned.
// Standard CRUD methods:
AtomicCache::put('key', $value, 3600);       // Store an item directly
$val = AtomicCache::get('key', default: null); // Retrieve an item
$exists = AtomicCache::has('key');             // Check existence
$val = AtomicCache::pull('key');              // Retrieve and immediately delete
AtomicCache::clear('key');                    // Delete item (alias of forget)

2. Stampede Protection (rememberWithLock)

During sudden traffic surges (e.g. after a cache flush or key expiry), hundreds of requests hit your database simultaneously trying to recalculate the same data.

rememberWithLock acquires an atomic distributed lock. Only one process executes the query, while concurrent requests wait up to 5 seconds and read the freshly cached result:

$analytics = AtomicCache::rememberWithLock('dashboard.stats', function () {
    return HeavyAnalyticsService::calculate();
}, ttl: 3600, lockSeconds: 10, waitSeconds: 5);

3. Auto Payload Compression (Gzip)

Large Eloquent collections or API responses can consume gigabytes of Redis/Memcached RAM.

Whenever a cached payload exceeds 50 KB (configurable), Atomic Cache automatically compresses it with gzcompress:

  • Reduces Redis memory usage by ~70% to 80%.
  • Decompression is handled transparently on retrieval.
  • If payload is smaller than 50 KB, it stores without compression overhead.

4. Virtual Tagging Engine (File & Database Fallback)

In Laravel, calling Cache::tags(['users']) on file or database cache drivers crashes with BadMethodCallException: This cache store does not support tagging.

Atomic Cache introduces a Virtual Tag Versioning Engine:

  • On Redis / Memcached: Uses native high-performance driver tags.
  • On File / Database / SQLite / Array: Tracks atomic version counters per tag (e.g. _tag_v:users = 1).
  • When clearTags(['users']) is called, it increments the tag version to 2.
  • Subsequent reads automatically experience a cache miss! Tag invalidation works in local development without Redis!
// Clear a single model item: flushes tag "users:42"
AtomicCache::clearItem('users', 42);

// Clear an entire collection: flushes tag "users"
AtomicCache::clearCollection('users');

// Clear custom tags
AtomicCache::clearTags(['dashboard', 'reports']);

5. Cache TTL Jitter (Thundering Herd Prevention)

If you cache 1,000 keys for 3,600 seconds, they will all expire at the exact same microsecond, causing a sudden CPU and database spike.

withJitter adds a small random variance (e.g. ยฑ5%) to the TTL:

// TTL of 3600s will randomly vary between 3420s and 3780s
AtomicCache::withJitter()->remember('feed.popular', fn() => getFeed(), 3600);

// Or pass an explicit variance (e.g. 10%)
AtomicCache::withJitter(0.10)->remember('feed.popular', fn() => getFeed(), 3600);

6. User-Scoped Caching (rememberForUser)

Avoid manually concatenating Auth::id() into your cache keys:

// Automatically scopes key to "atomic:user:{auth_id}:dashboard_nav"
$menu = AtomicCache::rememberForUser('dashboard_nav', function () {
    return Auth::user()->navigationMenu();
});

// Or scope to a specific user ID
$profile = AtomicCache::rememberForUser('profile', fn() => User::find(99), userId: 99);

7. Fluent Chainable Builder

You can fluently combine any optimizations in a clean builder chain:

$data = AtomicCache::store('redis')
    ->tags(['finance', 'reports'])
    ->ttl(7200)
    ->withLock(lockSeconds: 15, waitSeconds: 5)
    ->withJitter(0.05)
    ->remember('annual_revenue', function () {
        return RevenueCalculator::compute();
    });

8. Blade Fragment Caching

Cache expensive HTML blocks, navigation bars, or sidebar widgets directly in Blade templates:

@atomicCache('sidebar_categories', 3600)
    <div class="sidebar">
        @foreach(\App\Models\Category::with('subcategories')->get() as $category)
            <h3>{{ $category->name }}</h3>
            @foreach($category->subcategories as $sub)
                <p>{{ $sub->title }}</p>
            @endforeach
        @endforeach
    </div>
@endAtomicCache

9. Artisan CLI Command

Manage and clear cache tags or keys from terminal, cron jobs, or CI/CD deployment scripts:

# Clear a specific cache tag or key
php artisan atomic-cache:clear users

# Clear multiple tags
php artisan atomic-cache:clear dashboard

# Clear all atomic cache entries
php artisan atomic-cache:clear --all

๐Ÿงฉ Eloquent Model Lifecycle (AutoClearsCache)

Add the AutoClearsCache trait to any Eloquent model. Whenever a record is created, updated, or deleted, its cache tags are automatically flushed only after the active database transaction successfully commits (DB::afterCommit):

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use KaziOmar\AtomicCache\Traits\AutoClearsCache;

class User extends Model
{
    use AutoClearsCache;

    // Optional: automatically invalidate related cache tags when a user changes
    protected array $relatedCacheTags = ['dashboard', 'admin_stats'];
}

When $user->save() or $user->delete() executes:

  1. Waits for DB::commit() (if transaction rolls back, cache is untouched).
  2. Automatically invalidates single item tag: users:{id}.
  3. Automatically invalidates collection tag: users.
  4. Automatically invalidates related tags: dashboard, admin_stats.

๐Ÿ—๏ธ Service & Repository Layer (HasAtomicCache)

Standardize your caching architecture across services with the HasAtomicCache trait:

namespace App\Services;

use App\Models\User;
use KaziOmar\AtomicCache\Traits\HasAtomicCache;

class UserService
{
    use HasAtomicCache;

    // Optional service-level configuration properties:
    protected bool $cachingEnabled = true; // Set to false to bypass cache (useful in tests/debugging)
    protected int $cacheTTL = 7200;        // Custom default TTL in seconds for this service
    protected bool $cachePerUser = false;  // Scope all operations to the authenticated user ID
    protected string $cacheResource = 'users'; // Base resource tag (defaults to table/plural class name)

    public function find(int $id)
    {
        return $this->cacheItem($id, fn() => User::findOrFail($id));
    }

    public function getActiveUsers()
    {
        return $this->cacheCollection('active', fn() => User::where('active', 1)->get());
    }

    public function update(int $id, array $data)
    {
        $user = User::findOrFail($id);
        $user->update($data);

        // Clear both single item and list caches
        $this->clearItemCache($id);
        $this->clearCollectionCache();

        return $user;
    }
}

Service Configuration Properties

You can customize caching per service by declaring any of these optional properties:

Property Type Default Purpose
$cachingEnabled bool true Service Cache Toggle. Set to false during automated testing or debugging to bypass caching completely and always fetch fresh database records.
$cacheTTL int 3600 Service-Level Expiry. Defines the default cache lifetime in seconds for this specific service without overriding global config.
$cachePerUser bool false Automatic User Isolation. When set to true, all cache keys generated in this service are automatically prefixed with the logged-in user's ID (auth()->id()), preventing user data leaks.
$cacheResource string (auto) Resource Tag & Namespace. Specifies the cache tag and key prefix (e.g. 'users', 'orders'). Defaults automatically to the model's table or pluralized class name (UserService -> 'users').

Safe Argument Normalization

If a service method receives complex arguments (e.g. Closure, Eloquent Model, or Request), HasAtomicCache safely converts them to unique hashes/identifiers before serialization. You will never encounter Exception: Serialization of 'Closure' is not allowed.

Automatic Pagination & Filter Isolation

When calling cache() or cacheCollection(), the trait automatically inspects request()->query().

  • Request GET /users?page=1 and GET /users?page=2 are automatically stored under separate, isolated cache keys.
  • Search filters like ?filter=active&sort=desc automatically receive independent cache entries.

โš™๏ธ Configuration Reference

Publish config/atomic-cache.php to customize package behavior:

return [
    // Master switch to enable/disable caching (useful in local tests)
    'enabled' => env('ATOMIC_CACHE_ENABLED', true),

    // Default TTL in seconds (1 hour)
    'default_ttl' => env('ATOMIC_CACHE_DEFAULT_TTL', 3600),

    // Specific cache store (null = default Laravel cache store)
    'store' => env('ATOMIC_CACHE_STORE', null),

    // Global key namespace prefix
    'prefix' => env('ATOMIC_CACHE_PREFIX', 'atomic'),

    // Stampede distributed locking settings
    'stampede' => [
        'enabled' => env('ATOMIC_CACHE_STAMPEDE_ENABLED', true),
        'lock_seconds' => env('ATOMIC_CACHE_LOCK_SECONDS', 10),
        'wait_seconds' => env('ATOMIC_CACHE_WAIT_SECONDS', 5),
    ],

    // Automatic Gzip payload compression
    'compression' => [
        'enabled' => env('ATOMIC_CACHE_COMPRESSION_ENABLED', true),
        'threshold' => env('ATOMIC_CACHE_COMPRESSION_THRESHOLD', 51200), // 50 KB
        'level' => env('ATOMIC_CACHE_COMPRESSION_LEVEL', 6),
    ],

    // Self-healing from corrupted cache payloads
    'self_healing' => [
        'enabled' => env('ATOMIC_CACHE_SELF_HEALING', true),
        'log_corrupted' => env('ATOMIC_CACHE_LOG_CORRUPTED', true),
    ],

    // Random variance on expiry time
    'jitter' => [
        'enabled' => env('ATOMIC_CACHE_JITTER_ENABLED', false),
        'percentage' => env('ATOMIC_CACHE_JITTER_PERCENTAGE', 5), // ยฑ5%
    ],
];

Environment Variables (.env)

Key Default Description
ATOMIC_CACHE_ENABLED true Enable or disable atomic caching globally
ATOMIC_CACHE_DEFAULT_TTL 3600 Default Time-To-Live in seconds
ATOMIC_CACHE_STORE null Cache driver store to use (redis, file, etc.)
ATOMIC_CACHE_PREFIX atomic Key namespace prefix
ATOMIC_CACHE_STAMPEDE_ENABLED true Enable atomic locking by default
ATOMIC_CACHE_LOCK_SECONDS 10 Maximum lock duration during computation
ATOMIC_CACHE_WAIT_SECONDS 5 Maximum wait time for concurrent requests
ATOMIC_CACHE_COMPRESSION_ENABLED true Auto-compress payloads exceeding threshold
ATOMIC_CACHE_COMPRESSION_THRESHOLD 51200 Compression threshold in bytes (50 KB)
ATOMIC_CACHE_JITTER_ENABLED false Enable TTL variance globally
ATOMIC_CACHE_JITTER_PERCENTAGE 5 Jitter variance percentage (ยฑ5%)

๐Ÿงช Testing

Run the test suite using PHPUnit:

composer test

๐Ÿ‘จโ€๐Ÿ’ป Credits

๐Ÿ“„ License

The MIT License (MIT). Please see License File for more information.