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
Requires
- php: ^8.2
- illuminate/cache: ^10.0||^11.0||^12.0
- illuminate/contracts: ^10.0||^11.0||^12.0
- illuminate/support: ^10.0||^11.0||^12.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^10.0.0||^9.0.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- spatie/laravel-ray: ^1.35
README
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
- The Solution
- Installation
- Quick Start
- How It Works
- API Reference
- Web Dashboard
- Artisan Commands
- Configuration
- Use Cases
- Best Practices
- Testing
- Requirements
- Roadmap
- Comparison with Alternatives
- Contributing
- License
π° 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:
- Enable the dashboard in your
.env:
SMART_CACHE_DASHBOARD=true
- 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
filedriver 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)
-
HasSmartCachetrait 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:
- Fork the repository
- Create a branch for your feature (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push the branch (
git push origin feature/amazing-feature) - 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! β