dialloibrahima/smart-cache

Smart caching utilities for Laravel applications

Installs: 8

Dependents: 0

Suggesters: 0

Security: 0

Stars: 1

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/dialloibrahima/smart-cache

v2.0.1 2025-12-12 14:43 UTC

This package is auto-updated.

Last update: 2025-12-12 14:45:03 UTC


README

Latest Version on Packagist GitHub Tests Action Status Total Downloads PHP Version Laravel Version

SmartCache adds automatic and intelligent caching to Eloquent queries, with automatic invalidation when data changes. Zero configuration, maximum performance.

πŸ†• What's New in v2.0

Feature Description
πŸ” smartFind() Granular cache invalidation - cache by record ID
πŸ“Š Web Dashboard Real-time monitoring at /smart-cache
πŸ› οΈ Artisan Command php artisan smart-cache:clear
πŸ“ˆ Stats Tracking Track hits, misses, and query logs
βœ… 49 Tests 98 assertions for robust reliability
composer require dialloibrahima/smart-cache:^2.0

πŸ“‹ Table of Contents

😰 The Problem

Manually managing Eloquent query caching is tedious and error-prone:

// ❌ BEFORE: Manual cache everywhere
$users = Cache::remember('active_users', 3600, function () {
    return User::where('active', true)->get();
});

// And remember to invalidate... always!
User::created(function ($user) {
    Cache::forget('active_users');  // 😱 Easy to forget!
});

User::updated(function ($user) {
    Cache::forget('active_users');  // 😱 You have to do this for every event!
});

User::deleted(function ($user) {
    Cache::forget('active_users');  // 😱 And for every query!
});

Common Problems:

Problem Impact
πŸ”„ Manual cache everywhere Duplicated and verbose code
🧠 Easy to forget invalidation Stale (old) data shown to users
πŸ› Hardcoded cache keys Collisions and hard-to-debug bugs
πŸ“Š No centralized management Impossible to monitor cache hits/misses

✨ The Solution

With SmartCache, everything becomes automatic:

// βœ… AFTER: One line, everything handled automatically
$users = User::smartCache()->where('active', true)->smartGet();

// βœ… Automatically cached with a unique key based on the query
// βœ… Invalidated when User is created/updated/deleted
// βœ… Zero configuration needed

πŸ“¦ Installation

composer require dialloibrahima/smart-cache

Publish the configuration file (optional):

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

⚑ Quick Start

1. Add the Trait to Your Model

<?php

namespace App\Models;

use DialloIbrahima\SmartCache\HasSmartCache;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    use HasSmartCache;
    
    // Rest of your model...
}

2. Use Smart Caching in Your Queries

// Retrieve active users with caching
$users = User::smartCache()->where('active', true)->smartGet();

// Next time, data comes from cache! ⚑

3. Everything Else is Automatic! πŸŽ‰

When you create, update, or delete a user, the cache is automatically invalidated.

πŸ”§ How It Works

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      Eloquent Query                         β”‚
β”‚  User::smartCache()->where('active', true)->smartGet()      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   SmartCacheManager                         β”‚
β”‚  1. Generates unique cache key based on:                    β”‚
β”‚     - SQL Query                                             β”‚
β”‚     - Bindings (parameters)                                 β”‚
β”‚     - Eager Loads (relationships)                           β”‚
β”‚  2. Checks if result exists in cache                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
           β”‚                               β”‚
           β–Ό                               β–Ό
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ Cache HIT   β”‚                β”‚ Cache MISS   β”‚
    β”‚ ⚑ Instant  β”‚                β”‚ Execute SQL  β”‚
    β”‚ return data β”‚                β”‚ Store result β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β”‚ Return data  β”‚
                                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Automatic Invalidation

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Model Event (created/updated/deleted)               β”‚
β”‚                    User::create([...])                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  SmartCacheObserver                         β”‚
β”‚  1. Intercepts the event automatically                      β”‚
β”‚  2. Identifies the cache tag for this Model                 β”‚
β”‚  3. Invalidates (flushes) all related caches                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                           β–Ό
              βœ… Next query = fresh data!

Cache Key Generation

Each query generates a unique and deterministic cache key:

// These two queries have DIFFERENT keys (different bindings)
User::smartCache()->where('role', 'admin')->smartGet();  // key: abc123...
User::smartCache()->where('role', 'user')->smartGet();   // key: def456...

// These two queries have the SAME key
User::smartCache()->where('active', true)->smartGet();   // key: xyz789...
User::smartCache()->where('active', true)->smartGet();   // key: xyz789... βœ… Cache HIT!

Normal Queries (No Breaking Changes)

SmartCache is opt-in only. Your existing Eloquent queries continue to work exactly as before - no caching, no changes:

// ❌ NORMAL QUERIES - No caching (standard Laravel behavior)
$users = User::where('active', true)->get();    // Always executes SQL
$user = User::find(1);                           // Always executes SQL
$count = User::count();                          // Always executes SQL
$first = User::where('role', 'admin')->first(); // Always executes SQL

// βœ… SMART CACHE QUERIES - Automatic caching
$users = User::smartCache()->where('active', true)->smartGet();  // Cached!
$user = User::smartCache()->smartFirst();                         // Cached!
$count = User::smartCache()->smartCount();                        // Cached!

This design is intentional:

  • βœ… No breaking changes - Existing code works without modification
  • βœ… Explicit caching - You choose exactly which queries to cache
  • βœ… Safe by default - Avoids caching queries that shouldn't be cached

Note: The observer is always registered when using the trait, so cache invalidation happens automatically on model events. However, this has no effect on normal queries - it only affects SmartCache queries.

πŸ“– API Reference

Available Methods

Query Methods

Method Description Returns
smartGet($columns = ['*']) Retrieve collection with cache Collection
smartFirst($columns = ['*']) Retrieve first result with cache Model|null
smartFind($id, $columns = ['*']) Find by ID with granular cache (record-level invalidation) Model|null
smartCount($column = '*') Count results with cache int
smartSum($column) Sum values with cache float|int
smartAvg($column) Average values with cache float|int|null
smartMax($column) Maximum value with cache mixed
smartMin($column) Minimum value with cache mixed

Scope Methods

Method Description
smartCache(?int $ttl = null) Enable caching for the query (optional TTL in minutes)
withoutSmartCache() Disable caching for this query

Static Methods

Method Description
Model::disableSmartCache() Disable caching for the entire model
Model::enableSmartCache() Re-enable caching for the model
Model::clearSmartCache() Clear all cache for the model
Model::invalidatesSmartCacheOf() Define related models to invalidate (override in model)

Artisan Commands

Clear cache from the command line:

# Clear cache for a specific model
php artisan smart-cache:clear App\\Models\\User

# Clear all SmartCache entries
php artisan smart-cache:clear --all

Granular Cache Invalidation (smartFind)

Use smartFind() for record-level caching. When a specific record is updated, only its cache is invalidated:

// Cache individual records with record-level tags
$user1 = User::smartCache()->smartFind(1);  // Tag: smart_cache.users.1
$user2 = User::smartCache()->smartFind(2);  // Tag: smart_cache.users.2

// When User 1 is updated, only User 1's cache is invalidated
$user1->update(['name' => 'New Name']);  // Invalidates: smart_cache.users.1
// User 2's cache remains valid!

Web Dashboard

SmartCache includes an optional monitoring dashboard:

  1. Enable the dashboard in your .env:
SMART_CACHE_DASHBOARD=true
  1. Access the dashboard at /smart-cache

Features:

  • πŸ“Š View cache hits/misses and hit ratio
  • πŸ“‹ See recent cached queries
  • πŸ—‘οΈ Clear cache from UI (all or by model)
  • βš™οΈ View current configuration

Configuration options in config/smart-cache.php:

'dashboard' => [
    'enabled' => env('SMART_CACHE_DASHBOARD', false),
    'middleware' => ['web', 'auth'],  // Add auth for production!
    'path' => 'smart-cache',
],

Related Cache Invalidation

When a related model changes, you might want to invalidate the cache of parent models automatically. Override invalidatesSmartCacheOf() in your model:

class Comment extends Model
{
    use HasSmartCache;

    /**
     * When a Comment changes, also invalidate Post and Notification cache.
     */
    public static function invalidatesSmartCacheOf(): array
    {
        return [Post::class, Notification::class];
    }
}

Now when you create, update, or delete a Comment, the cache for both Post and Notification will be automatically invalidated!

Complete Examples

use App\Models\Post;
use App\Models\User;

// ═══════════════════════════════════════════════════════════
// BASIC QUERIES
// ═══════════════════════════════════════════════════════════

// Cache with default TTL (60 minutes)
$posts = Post::smartCache()
    ->where('published', true)
    ->smartGet();

// Cache with custom TTL (30 minutes)
$users = User::smartCache(30)
    ->where('role', 'admin')
    ->smartGet();

// Infinite cache (invalidated only by model events)
$categories = Category::smartCache(0)->smartGet();

// ═══════════════════════════════════════════════════════════
// AGGREGATES
// ═══════════════════════════════════════════════════════════

// Count with cache
$activeCount = User::smartCache()
    ->where('active', true)
    ->smartCount();

// Sum, average, min, max
$totalRevenue = Order::smartCache()->smartSum('amount');
$avgOrderValue = Order::smartCache()->smartAvg('amount');
$highestOrder = Order::smartCache()->smartMax('amount');
$lowestOrder = Order::smartCache()->smartMin('amount');

// ═══════════════════════════════════════════════════════════
// COMPLEX QUERIES
// ═══════════════════════════════════════════════════════════

// With relationships (eager loading)
$posts = Post::smartCache()
    ->with(['author', 'comments.user'])
    ->where('published', true)
    ->orderBy('created_at', 'desc')
    ->take(10)
    ->smartGet();

// With multiple conditions
$users = User::smartCache()
    ->where('active', true)
    ->where('email_verified_at', '!=', null)
    ->whereHas('posts', function ($query) {
        $query->where('published', true);
    })
    ->smartGet();

// ═══════════════════════════════════════════════════════════
// CACHE CONTROL
// ═══════════════════════════════════════════════════════════

// Disable cache for a specific query
$freshUser = User::withoutSmartCache()->find($id);

// Disable cache for entire model
User::disableSmartCache();
$users = User::smartCache()->smartGet(); // Does not use cache
User::enableSmartCache();

// Manually clear cache
User::clearSmartCache(); // Invalidates all User caches

βš™οΈ Configuration

After publishing the config, you'll find config/smart-cache.php:

<?php

return [
    /*
    |--------------------------------------------------------------------------
    | Cache Driver
    |--------------------------------------------------------------------------
    |
    | The cache driver to use. Set to 'auto' to use Laravel's default driver.
    |
    | Supported: 'auto', 'redis', 'memcached', 'database', 'array'
    |
    | ⚠️  NOTE: 'file' does NOT support cache tags and won't work correctly!
    |
    */
    'driver' => env('SMART_CACHE_DRIVER', 'auto'),

    /*
    |--------------------------------------------------------------------------
    | Default TTL (Time To Live)
    |--------------------------------------------------------------------------
    |
    | Default cache duration in MINUTES.
    | Set to 0 for infinite cache (invalidated only by model events).
    |
    */
    'ttl' => env('SMART_CACHE_TTL', 60),

    /*
    |--------------------------------------------------------------------------
    | Cache Key Prefix
    |--------------------------------------------------------------------------
    |
    | Prefix for all SmartCache keys, to avoid collisions with other
    | cached data in your application.
    |
    */
    'prefix' => 'smart_cache',

    /*
    |--------------------------------------------------------------------------
    | Enable SmartCache
    |--------------------------------------------------------------------------
    |
    | Enable or disable SmartCache globally.
    | Useful for disabling in testing environments.
    |
    */
    'enabled' => env('SMART_CACHE_ENABLED', true),

    /*
    |--------------------------------------------------------------------------
    | Logging
    |--------------------------------------------------------------------------
    |
    | Enable logging of cache hits and misses for debugging.
    | Logs will appear in Laravel's default log channel.
    |
    */
    'logging' => env('SMART_CACHE_LOGGING', false),
];

Environment Variables

Add to your .env:

# Use Redis for best performance
SMART_CACHE_DRIVER=redis

# Cache for 2 hours (120 minutes)
SMART_CACHE_TTL=120

# Enable logging for debugging
SMART_CACHE_LOGGING=true

# Disable in testing
SMART_CACHE_ENABLED=true

πŸ“š Use Cases

1. Homepage with Recent Posts

// Controller
public function index()
{
    // Cache for 5 minutes - frequently changing content
    $recentPosts = Post::smartCache(5)
        ->with('author:id,name,avatar')
        ->where('published', true)
        ->latest()
        ->take(10)
        ->smartGet();

    // Cache for 1 hour - rarely changes
    $categories = Category::smartCache(60)
        ->withCount('posts')
        ->orderBy('name')
        ->smartGet();

    // Cache for 24 hours - almost static
    $stats = [
        'total_posts' => Post::smartCache(1440)->smartCount(),
        'total_users' => User::smartCache(1440)->smartCount(),
    ];

    return view('home', compact('recentPosts', 'categories', 'stats'));
}

2. Admin Dashboard

public function dashboard()
{
    // Aggregated statistics - short cache for always up-to-date data
    return [
        'orders_today' => Order::smartCache(5)
            ->whereDate('created_at', today())
            ->smartCount(),
            
        'revenue_today' => Order::smartCache(5)
            ->whereDate('created_at', today())
            ->smartSum('total'),
            
        'new_users_week' => User::smartCache(15)
            ->where('created_at', '>=', now()->subWeek())
            ->smartCount(),
            
        'avg_order_value' => Order::smartCache(30)
            ->where('status', 'completed')
            ->smartAvg('total'),
    ];
}

3. API with Rate Limiting

// Perfect for APIs - avoids duplicate queries from repeated calls
public function show(string $slug)
{
    $post = Post::smartCache()
        ->with(['author', 'tags', 'comments.user'])
        ->where('slug', $slug)
        ->where('published', true)
        ->smartFirst();

    if (!$post) {
        abort(404);
    }

    return new PostResource($post);
}

4. Sidebar with Dynamic Data

// View Composer - executed on every page
View::composer('partials.sidebar', function ($view) {
    $view->with([
        'popularPosts' => Post::smartCache(30)
            ->orderByDesc('views')
            ->take(5)
            ->smartGet(),
            
        'recentComments' => Comment::smartCache(10)
            ->with('user:id,name')
            ->latest()
            ->take(5)
            ->smartGet(),
    ]);
});

βœ… Best Practices

βœ… DO - What to Do

// βœ… Queries that rarely change
$categories = Category::smartCache(120)->smartGet();

// βœ… Aggregates on large datasets
$totalSales = Order::smartCache()->smartSum('amount');

// βœ… Public data shared between users
$featuredProducts = Product::smartCache()
    ->where('featured', true)
    ->smartGet();

// βœ… Short TTL for frequently changing data
$latestNews = News::smartCache(5)->latest()->take(5)->smartGet();

// βœ… Eager loaded relationships
$posts = Post::smartCache()
    ->with('author', 'tags')  // Included in cache key
    ->smartGet();

❌ DON'T - What to Avoid

// ❌ User-specific queries - each user = new cache entry
$myPosts = Post::smartCache()
    ->where('user_id', auth()->id())  // Better without cache
    ->smartGet();

// ❌ Queries with highly variable parameters
$searchResults = Post::smartCache()
    ->where('title', 'like', "%{$searchTerm}%")  // Too many variations
    ->smartGet();

// ❌ Queries on real-time data
$liveData = Metric::smartCache()  // Don't use cache for real-time
    ->where('timestamp', '>=', now()->subMinute())
    ->smartGet();

// ❌ TTL too long for changing data
$orders = Order::smartCache(1440)->smartGet();  // 24h is too much!

πŸ’‘ Recommended Patterns

// Pattern: Conditional caching
$posts = $request->has('search')
    ? Post::withoutSmartCache()->where('title', 'like', "%{$search}%")->get()
    : Post::smartCache()->where('published', true)->smartGet();

// Pattern: Cache by role
$users = auth()->user()->isAdmin()
    ? User::withoutSmartCache()->get()  // Admin always sees fresh data
    : User::smartCache()->where('public', true)->smartGet();

// Pattern: Manual invalidation on specific actions
public function importProducts(Request $request)
{
    // Bulk import...
    Product::clearSmartCache();  // Force refresh after import
}

πŸ§ͺ Testing

Disable Cache in Tests

// tests/TestCase.php
protected function setUp(): void
{
    parent::setUp();
    
    // Disable SmartCache in tests
    config(['smart-cache.enabled' => false]);
}

Testing with Active Cache

use DialloIbrahima\SmartCache\SmartCacheManager;

it('caches query results', function () {
    $post = Post::factory()->create(['published' => true]);
    
    // First call - cache miss
    $result1 = Post::smartCache()->where('published', true)->smartGet();
    
    // Second call - cache hit
    $result2 = Post::smartCache()->where('published', true)->smartGet();
    
    expect($result1)->toHaveCount(1);
    expect($result2)->toHaveCount(1);
});

it('invalidates cache on model update', function () {
    $post = Post::factory()->create(['title' => 'Original']);
    
    // Cache the query
    Post::smartCache()->smartGet();
    
    // Update the post - cache invalidated automatically
    $post->update(['title' => 'Updated']);
    
    // Next query reflects the change
    $posts = Post::smartCache()->smartGet();
    expect($posts->first()->title)->toBe('Updated');
});

πŸ“‹ Requirements

Requirement Version
PHP ^8.2
Laravel 10.x, 11.x, 12.x
Cache Driver Redis (recommended), Memcached, Database, Array

⚠️ Important: The file driver does not support cache tags and won't work correctly with SmartCache. Use Redis for best performance.

Configuring Redis

# Install predis
composer require predis/predis

# Or use phpredis (PHP extension)
# .env
CACHE_DRIVER=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

πŸ—ΊοΈ Roadmap

v1.0 βœ… (Current)

  • HasSmartCache trait for models
  • Automatic query caching (get, first, count, sum, avg, min, max)
  • Automatic invalidation on model events
  • Global and per-query TTL configuration
  • Cache tags support
  • Cache hits/misses logging
  • Complete test suite

v1.1 (Next)

  • php artisan smart-cache:clear - Command to clear cache
  • php artisan smart-cache:stats - Usage statistics
  • Cache warming (pre-populate cache)
  • Events for cache hit/miss

v2.0 (Future)

  • Granular invalidation (per record, not per table)
  • Cache relationships separately
  • Web dashboard for monitoring
  • Multi-server distributed cache support

βš–οΈ Comparison with Alternatives

Feature SmartCache Cache::remember() laravel-responsecache
Zero boilerplate βœ… ❌ βœ…
Automatic invalidation βœ… ❌ βœ…
Query-level cache βœ… βœ… ❌ (response)
Aggregates (count, sum...) βœ… ❌ ❌
Per-query TTL βœ… βœ… ❌
Works with JSON APIs βœ… βœ… ⚠️
Tag-based invalidation βœ… ❌ βœ…

🀝 Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a branch for your feature (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Development

# Install dependencies
composer install

# Run tests
composer test

# Static analysis
composer analyse

# Code formatting
composer format

πŸ“„ License

The MIT License (MIT). See License File for more information.

πŸ‘¨β€πŸ’» Credits

⭐ If this package is useful to you, leave a star on GitHub! ⭐