Skip to content

An aggressive Laravel cache busting scheme for infrequently updated content.

License

Notifications You must be signed in to change notification settings

plank/model-cache

Repository files navigation

PHP Version Support Laravel Version Support GitHub Workflow Status

Laravel Model Cache

A Laravel caching package that automatically invalidates when your models change.

Laravel Model Cache provides an elegant, declarative way to cache expensive operations at the model level with intelligent auto-invalidation. Perfect for read-heavy applications where data doesn't change very frequently but performance is critical.

  • ✨ Smart Caching: Cache any expensive operation with automatic invalidation when models change
  • 🏎️ High Performance: Supports tags for cache invalidation
  • 🧠 Intelligent Keys: Automatic cache key generation from closures and callables
  • 🔧 Zero Configuration: Works out of the box with Laravel's cache system

Table of Contents

 

Installation

Install the package via Composer:

composer require plank/model-cache
  1. Use the package's install command to complete the installation:
php artisan model-cache:install

 

Quick Start

Make your model cacheable by implementing the Cachable contract and using the IsCachable trait:

<?php

use Illuminate\Database\Eloquent\Model;
use Plank\ModelCache\Contracts\Cachable;
use Plank\ModelCache\Traits\IsCachable;

class User extends Model implements Cachable
{
    use IsCachable;
    
    // ...
}

Model-level cache

The following example invalidates when:

  1. Any User model is created, updated, or deleted
  2. Any Post or Comment model is created, updated, or deleted, due to their tags being added.
    • This means that Post and Comment would need to implement Flushable or Cachable
$result = User::remember(function () {
    $users = User::with(['posts', 'comments'])
        ->where('active', true)
        ->get();

    return $this->doSomethingExpensive($users);
}, [
    Post::class,
    Comment::class,
]);

Instance-level cache

The following example invalidates when:

  1. The specific User instance is updated or deleted
protected function getExpensiveAttribute(): mixed
{
    return $this->rememberOnSelf(function () {
        return ExpensiveApi::get(email: $this->email, name: $this->name);
    }, ttl: ExpireAfter::Forever);
}

 

Core Concepts

Model-Level Caching (remember())

Use remember() for data that should be invalidated when any model of that type changes:

// This cache invalidates when ANY user changes
$data = User::remember(fn() => User::all()->pluck('email'));

Instance-Level Caching (rememberOnSelf())

Use rememberOnSelf() for data that should only invalidate when that specific model instance changes:

$user = User::find(1);

// This cache only invalidates when THIS user changes
$data = $user->rememberOnSelf(fn() => $user->posts()->count());

Using Flushable Tags

A "Flushable tag" is the class string of a Model which implements the Flushable interface. (Cachable extends Flushable).

By passing the class string of a Flushable tag to the remember and rememberOnSelf methods, those cache entries will also be invalidated when any model of that type changes. By passing an instance of a Flushable as a tag, the entry will be invalidated only when that specific instance changes.

$user = User::find(1);

// This cache only invalidates when THIS user changes or any Post changes
$data = $user->rememberOnSelf(fn() => $user->posts()->count(), [Post::class]);

Automatic Cache Key Generation

The package intelligently manages cache keys by generating them from the passed callable. Calling the remember methods with the same callable – regardless of where you are calling it from (with the same tags) – will return the same result.

$cached = fn () => 'Expensive text';

User::remember($cached); // Cache miss
User::remember($cached); // Cache hit

class ExpensiveInvokeable
{
    public function __invoke()
    {
        return 'expensive';
    }
}

User::remember(ExpensiveInvokeable::class); // Cache miss
User::remember(ExpensiveInvokeable::class); // Cache hit

 

Model-Specific Configuration

Customize caching behavior per model:

class User extends Model implements Cachable
{
    use IsCachable;
    
    // Custom cache prefix for this model
    public static function modelCachePrefix(): string
    {
        return 'user_v2'; // Useful for cache versioning
    }
    
    // Default tags applied to all cache operations
    public static function defaultTags(): array
    {
        return ['users', 'auth'];
    }
    
    // Skip cache invalidation under certain conditions
    public function shouldSkipFlushing(): bool
    {
        // Don't invalidate cache for minor updates
        return $this->wasChanged(['last_seen_at', 'login_count']);
    }
}

 

Using ExpireAfter Enum

The package provides a convenient enum for common TTL values:

use Plank\ModelCache\Enums\ExpireAfter;

// Available values
ExpireAfter::Forever;           // null (never expires)
ExpireAfter::OneMinute;         // 60 seconds
ExpireAfter::FiveMinutes;       // 300 seconds
ExpireAfter::TenMinutes;        // 600 seconds
ExpireAfter::FifteenMinutes;    // 900 seconds
ExpireAfter::ThirtyMinutes;     // 1800 seconds
ExpireAfter::FortyFiveMinutes;  // 2700 seconds
ExpireAfter::OneHour;           // 3600 seconds
ExpireAfter::OneDay;            // 86400 seconds
ExpireAfter::OneWeek;           // 604800 seconds
ExpireAfter::OneMonth;          // 2592000 seconds
ExpireAfter::OneYear;           // 31536000 seconds

// Usage
User::remember(fn() => User::all(), ttl: ExpireAfter::OneHour);
User::remember(fn() => User::count(), ttl: ExpireAfter::FiveMinutes);

Custom TTL Values

// Using integer seconds
User::remember(fn() => User::all(), ttl: 3600); // 1 hour

// Using null for forever
User::remember(fn() => User::all(), ttl: null); // Never expires

// Default TTL from config
User::remember(fn() => User::all()); // Uses config('model-cache.ttl')

 

Cache Invalidation

Automatic Invalidation

Cache entries are automatically invalidated when models change:

// Cache some user data
$userData = User::remember(fn() => User::with('posts')->get());

// This will automatically invalidate the above cache
User::create(['name' => 'John', 'email' => 'john@example.com']);

// Next call will regenerate the cache
$freshData = User::remember(fn() => User::with('posts')->get()); // Cache miss, regenerated

Model Events That Trigger Invalidation

  • created: When a new model is created
  • updated: When a model is updated
  • deleted: When a model is deleted
  • restored: When a soft-deleted model is restored (if using SoftDeletes)

Conditional Invalidation

Skip invalidation for specific scenarios:

class User extends Model implements Cachable
{
    use IsCachable;
    
    public function shouldSkipFlushing(): bool
    {
        // Don't invalidate cache for timestamp-only updates
        if ($this->wasChanged(['updated_at']) && count($this->getChanges()) === 1) {
            return true;
        }
        
        // Don't invalidate for tracking fields
        if ($this->wasChanged(['last_seen_at', 'login_count'])) {
            return true;
        }
        
        return false;
    }
}

 

Performance Considerations

Cache Store Compatibility

Redis (Recommended)

  • Supports cache tags for invalidation
  • Best performance for tagged cache operations
  • Recommended for production use

Memcached

  • Supports cache tags
  • Good performance
  • Alternative to Redis

File/Database Cache

  • No tag support - uses Cache::flush() for invalidation
  • ⚠️ Warning: Invalidation flushes the entire cache
  • Not recommended for production use

 

Testing

Disable Caching in Tests

// In your test setup
config(['model-cache.enabled' => false]);

 

Troubleshooting

Stale data

If your caching the results of a query that depends on or involves other models, be sure to make those models Cachable or Flushable and tag the entry with those classes/instances.

 

Credits

Inspired by the need for intelligent, automatic cache invalidation in Laravel applications.

 

License

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

 

Security Vulnerabilities

If you discover a security vulnerability within the package, please send an e-mail to security@plank.co. All security vulnerabilities will be promptly addressed.

 

Check Us Out!

 

Plank focuses on impactful solutions that deliver engaging experiences to our clients and their users. We're committed to innovation, inclusivity, and sustainability in the digital space. Learn more about our mission to improve the web.

About

An aggressive Laravel cache busting scheme for infrequently updated content.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

Packages

No packages published

Contributors 3

  •  
  •  
  •  

Languages