diff --git a/README.md b/README.md index ababf35..d1e6478 100644 --- a/README.md +++ b/README.md @@ -7,21 +7,13 @@ [![PHP Version](https://img.shields.io/packagist/php-v/iazaran/smart-cache.svg?style=flat-square)](https://packagist.org/packages/iazaran/smart-cache) [![Tests](https://img.shields.io/badge/tests-425%20passed-brightgreen?style=flat-square)](https://github.com/iazaran/smart-cache/actions) -A drop-in replacement for Laravel's `Cache` facade that automatically compresses, chunks, and optimizes cached data — with write deduplication, self-healing recovery, and cost-aware eviction built in. Implements `Illuminate\Contracts\Cache\Repository` and PSR-16 `SimpleCache`; your existing code works unchanged. +**Drop-in replacement for Laravel's `Cache` facade** that automatically compresses, chunks, and optimizes cached data — with write deduplication, self-healing recovery, and cost-aware eviction built in. -**PHP 8.1+ · Laravel 8–12 · All cache drivers** +Implements `Illuminate\Contracts\Cache\Repository` and PSR-16 `SimpleCache`. Your existing code works unchanged. -## Why SmartCache? +**PHP 8.1+ · Laravel 8–12 · Redis, File, Database, Memcached, Array** -| Concern | Without SmartCache | With SmartCache | -|---|---|---| -| Large payloads (100 KB+) | Stored as-is, slow reads | Auto-compressed & chunked | -| Redundant writes | Every `put()` hits the store | Skipped when content is unchanged (write deduplication) | -| Corrupted entries | Exception propagates to users | Auto-evicted and regenerated (self-healing) | -| Eviction strategy | LRU / random | Cost-aware scoring — keeps high-value keys | -| Cache stampede | Thundering herd on expiry | XFetch, jitter, and rate limiting | -| Conditional caching | Manual `if` checks around `put()` | `rememberIf()` — one-liner | -| Monitoring | DIY logging | Built-in dashboard, metrics, and health checks | +--- ## Installation @@ -29,18 +21,18 @@ A drop-in replacement for Laravel's `Cache` facade that automatically compresses composer require iazaran/smart-cache ``` -No configuration required. Works immediately with any cache driver (Redis, File, Database, Memcached, Array). +That's it. No configuration required — works immediately with your existing cache driver. ## Quick Start ```php use SmartCache\Facades\SmartCache; -// Same API as Laravel's Cache facade — with automatic optimization +// Same API you already know SmartCache::put('users', $users, 3600); $users = SmartCache::get('users'); -// Remember pattern +// Remember pattern — with automatic compression & cost tracking $users = SmartCache::remember('users', 3600, fn() => User::all()); // Helper function @@ -50,111 +42,114 @@ $products = smart_cache('products'); Large data is automatically compressed and chunked behind the scenes. No code changes needed. -### Multiple Cache Drivers - -```php -// Each store preserves all SmartCache optimizations -SmartCache::store('redis')->put('key', $value, 3600); -SmartCache::store('memcached')->remember('users', 3600, fn() => User::all()); - -// Bypass SmartCache when needed -SmartCache::repository('redis')->put('key', $value, 3600); -``` +## Why SmartCache? -SmartCache implements `Illuminate\Contracts\Cache\Repository`, so it works anywhere Laravel's Cache contract is expected. +| Problem | Without SmartCache | With SmartCache | +|---|---|---| +| Large payloads (100 KB+) | Stored as-is, slow reads | Auto-compressed & chunked | +| Redundant writes | Every `put()` hits the store | Skipped when unchanged (write deduplication) | +| Corrupted entries | Exception crashes the request | Auto-evicted and regenerated (self-healing) | +| Eviction decisions | LRU / random | Cost-aware scoring — keeps high-value keys | +| Cache stampede | Thundering herd on expiry | XFetch, jitter, and rate limiting | +| Conditional caching | Manual `if` around `put()` | `rememberIf()` — one-liner | +| Stale data serving | Not available | SWR, stale, refresh-ahead, async queue refresh | +| Observability | DIY logging | Built-in dashboard, metrics, and health checks | -## Automatic Optimization +### How Automatic Optimization Works -SmartCache selects the best strategy based on your data: +SmartCache selects the best strategy based on your data — zero configuration: -| Data Profile | Strategy | Effect | +| Data Profile | Strategy Applied | Effect | |---|---|---| | Arrays with 5 000+ items | Chunking | Lower memory, faster access | | Serialized data > 50 KB | Compression | Significant size reduction (gzip) | | API responses > 100 KB | Chunking + Compression | Best of both | | Data < 50 KB | None | Zero overhead | -All thresholds are configurable. See [Configuration](#configuration). +All thresholds are [configurable](#configuration). -## Advanced Features +## Features Every feature below is **opt-in** and backward-compatible. -### Atomic Locks +### Multiple Cache Drivers ```php -SmartCache::lock('expensive_operation', 10)->get(function () { - return regenerateExpensiveData(); -}); -``` - -### In-Request Memoization +// Each store preserves all SmartCache optimizations +SmartCache::store('redis')->put('key', $value, 3600); +SmartCache::store('memcached')->remember('users', 3600, fn() => User::all()); -```php -$memo = SmartCache::memo(); -$users = $memo->remember('users', 3600, fn() => User::all()); -$users = $memo->get('users'); // instant — served from memory +// Bypass SmartCache when needed +SmartCache::repository('redis')->put('key', $value, 3600); ``` -### Batch Operations +### SWR Patterns (Stale-While-Revalidate) ```php -$values = SmartCache::many(['key1', 'key2', 'key3']); -SmartCache::putMany(['key1' => $a, 'key2' => $b], 3600); -SmartCache::deleteMultiple(['key1', 'key2', 'key3']); -``` +// Serve stale data while refreshing in background +$data = SmartCache::swr('github_repos', fn() => Http::get('...')->json(), 300, 900); -### Adaptive Compression +// Extended stale serving (1 h fresh, 24 h stale) +$config = SmartCache::stale('site_config', fn() => Config::fromDatabase(), 3600, 86400); -```php -// Adjusts compression level per entry based on access frequency and compressibility -config(['smart-cache.strategies.compression.mode' => 'adaptive']); +// Proactive refresh before expiry +$analytics = SmartCache::refreshAhead('daily_analytics', fn() => Analytics::generateReport(), 1800, 300); + +// Queue-based background refresh — returns stale immediately +$data = SmartCache::asyncSwr('dashboard_stats', fn() => Stats::generate(), 300, 900, 'cache-refresh'); ``` -### Lazy Loading +### Stampede Protection ```php -config(['smart-cache.strategies.chunking.lazy_loading' => true]); +// XFetch algorithm — probabilistic early refresh +$data = SmartCache::rememberWithStampedeProtection('key', 3600, fn() => expensiveQuery()); -$dataset = SmartCache::get('100k_records'); // LazyChunkedCollection -foreach ($dataset as $record) { /* max 3 chunks in memory */ } +// Rate-limited regeneration +SmartCache::throttle('api_call', 10, 60, fn() => expensiveApiCall()); + +// TTL jitter — prevents thundering herd on expiry +SmartCache::withJitter(0.1)->put('popular_data', $data, 3600); +// Actual TTL: 3240–3960 s (±10 %) ``` -### Cache Events +### Write Deduplication (Cache DNA) -```php -config(['smart-cache.events.enabled' => true]); +Hashes every value before writing. Identical content → write skipped entirely. -Event::listen(CacheHit::class, fn($e) => Log::info("Hit: {$e->key}")); -Event::listen(CacheMissed::class, fn($e) => Log::warning("Miss: {$e->key}")); -Event::listen(OptimizationApplied::class, fn($e) => Log::info("Optimized: {$e->key}")); +```php +SmartCache::put('app_config', Config::all(), 3600); +SmartCache::put('app_config', Config::all(), 3600); // no I/O — data unchanged ``` -### Encryption at Rest +### Self-Healing Cache + +Corrupted entries are auto-evicted and regenerated on next read — zero downtime. ```php -// config/smart-cache.php -'encryption' => [ - 'enabled' => true, - 'keys' => ['user_*', 'payment_*'], -], +$report = SmartCache::remember('report', 3600, fn() => Analytics::generate()); ``` -### Namespacing +### Conditional Caching ```php -SmartCache::namespace('api_v2')->put('users', $users, 3600); -SmartCache::flushNamespace('api_v2'); +$data = SmartCache::rememberIf('external_api', 3600, + fn() => Http::get('https://api.example.com/data')->json(), + fn($value) => !empty($value) && isset($value['status']) +); ``` -### TTL Jitter +### Cost-Aware Eviction + +GreedyDual-Size–inspired scoring: `score = (cost × ln(1 + access_count) × decay) / size` ```php -SmartCache::withJitter(0.1)->put('popular_data', $data, 3600); -// Actual TTL: 3240–3960 s (±10 %) — prevents thundering herd +SmartCache::remember('analytics', 3600, fn() => AnalyticsService::generateReport()); +SmartCache::getCacheValueReport(); // all entries ranked by value +SmartCache::suggestEvictions(5); // lowest-value entries to remove ``` -### Circuit Breaker +### Circuit Breaker & Fallback ```php $data = SmartCache::withFallback( @@ -163,75 +158,42 @@ $data = SmartCache::withFallback( ); ``` -### Stampede Protection - -```php -// XFetch algorithm — probabilistic early refresh -$data = SmartCache::rememberWithStampedeProtection('key', 3600, fn() => expensiveQuery()); - -// Rate-limited regeneration -SmartCache::throttle('api_call', 10, 60, fn() => expensiveApiCall()); -``` - -### Cost-Aware Caching - -Implements a GreedyDual-Size–inspired scoring model. Every `remember()` call tracks regeneration cost, access frequency, and entry size to compute a value score: - -``` -score = (cost × ln(1 + access_count) × decay) / size -``` - -```php -// Works transparently — just use remember() -SmartCache::remember('analytics', 3600, fn() => AnalyticsService::generateReport()); - -// Inspect value scores -SmartCache::getCacheValueReport(); -SmartCache::cacheValue('analytics'); -SmartCache::suggestEvictions(5); // lowest-value entries to remove first -``` - -### Write Deduplication (Cache DNA) - -SmartCache hashes every value before writing. When the stored content is identical, the write is skipped entirely — eliminating redundant I/O for frequently refreshed but rarely changing data (configuration, feature flags, rate-limit counters). +### In-Request Memoization ```php -// Frequent cron refreshes? Only the first write hits the store. -SmartCache::put('app_config', Config::all(), 3600); -// Second call with the same data → no I/O, returns true immediately -SmartCache::put('app_config', Config::all(), 3600); +$memo = SmartCache::memo(); +$users = $memo->remember('users', 3600, fn() => User::all()); +$users = $memo->get('users'); // instant — served from memory ``` -Enabled by default. Disable per-environment: +### Atomic Locks ```php -'deduplication' => ['enabled' => false], +SmartCache::lock('expensive_operation', 10)->get(function () { + return regenerateExpensiveData(); +}); ``` -### Conditional Caching (`rememberIf`) - -Cache values only when a condition is met. The callback always executes, but the result is stored only if the condition returns `true` — useful for filtering out empty or invalid API responses. +### Namespacing ```php -$data = SmartCache::rememberIf('external_api', 3600, - fn() => Http::get('https://api.example.com/data')->json(), - fn($value) => !empty($value) && isset($value['status']) -); +SmartCache::namespace('api_v2')->put('users', $users, 3600); +SmartCache::flushNamespace('api_v2'); ``` -### Self-Healing Cache - -Corrupted or unrestorable cache entries are automatically evicted instead of propagating an exception. Combined with `remember()` or `rememberIf()`, the entry is transparently regenerated on the next read — zero downtime, zero manual intervention. +### Cache Invalidation ```php -// If 'report' is corrupted, SmartCache evicts it and the callback runs again -$report = SmartCache::remember('report', 3600, fn() => Analytics::generate()); -``` +// Pattern-based +SmartCache::flushPatterns(['user_*', 'api_v2_*', '/product_\d+/']); -Enabled by default. Disable per-environment: +// Dependency tracking +SmartCache::dependsOn('user_posts', 'user_profile'); +SmartCache::invalidate('user_profile'); // also clears user_posts -```php -'self_healing' => ['enabled' => false], +// Tag-based +SmartCache::tags(['users'])->put('user_1', $user, 3600); +SmartCache::flushTags(['users']); ``` ### Model Auto-Invalidation @@ -250,71 +212,66 @@ class User extends Model } ``` -### Cache Warming +### Encryption at Rest ```php -php artisan smart-cache:warm -php artisan smart-cache:warm --keys=products,categories +// config/smart-cache.php → strategies.encryption +'encryption' => [ + 'enabled' => true, + 'keys' => ['user_token_abc123'], // exact cache-key match + 'patterns' => ['/^user_token_/', '/^payment_/'], // regex match +], ``` -### Orphan Chunk Cleanup +### Adaptive Compression -```bash -php artisan smart-cache:cleanup-chunks +```php +config(['smart-cache.strategies.compression.mode' => 'adaptive']); +// Hot data → fast compression (level 3–4), cold data → high compression (level 7–9) ``` -### Statistics Dashboard +### Lazy Loading ```php -'dashboard' => ['enabled' => true, 'prefix' => 'smart-cache', 'middleware' => ['web', 'auth']], -// GET /smart-cache/dashboard | /smart-cache/stats | /smart-cache/health +config(['smart-cache.strategies.chunking.lazy_loading' => true]); +$dataset = SmartCache::get('100k_records'); // LazyChunkedCollection +foreach ($dataset as $record) { /* max 3 chunks in memory */ } ``` -## SWR Patterns (Laravel 12+) +### Batch Operations ```php -// Stale-While-Revalidate -$data = SmartCache::swr('github_repos', fn() => Http::get('...')->json(), 300, 900); - -// Extended stale serving -$config = SmartCache::stale('site_config', fn() => Config::fromDatabase(), 3600, 86400); - -// Refresh-ahead -$analytics = SmartCache::refreshAhead('daily_analytics', fn() => Analytics::generateReport(), 1800, 300); - -// Queue-based background refresh — returns stale data immediately, refreshes asynchronously -$data = SmartCache::asyncSwr('dashboard_stats', fn() => Stats::generate(), 300, 900, 'cache-refresh'); +$values = SmartCache::many(['key1', 'key2', 'key3']); +SmartCache::putMany(['key1' => $a, 'key2' => $b], 3600); +SmartCache::deleteMultiple(['key1', 'key2', 'key3']); ``` -## Invalidation +### Cache Events ```php -// Pattern-based -SmartCache::flushPatterns(['user_*', 'api_v2_*', '/product_\d+/']); - -// Dependency tracking -SmartCache::dependsOn('user_posts', 'user_profile'); -SmartCache::invalidate('user_profile'); // also clears user_posts - -// Tag-based -SmartCache::tags(['users'])->put('user_1', $user, 3600); -SmartCache::flushTags(['users']); +config(['smart-cache.events.enabled' => true]); +Event::listen(CacheHit::class, fn($e) => Log::info("Hit: {$e->key}")); +Event::listen(CacheMissed::class, fn($e) => Log::warning("Miss: {$e->key}")); ``` -## Monitoring +### Monitoring & Dashboard ```php SmartCache::getPerformanceMetrics(); // hit_ratio, compression_savings, timing -SmartCache::analyzePerformance(); // health score, recommendations +SmartCache::analyzePerformance(); // health score + recommendations +``` -SmartCache::executeCommand('status'); -SmartCache::executeCommand('clear', ['key' => 'api_data']); +```php +// Enable web dashboard +'dashboard' => ['enabled' => true, 'prefix' => 'smart-cache', 'middleware' => ['web', 'auth']], +// GET /smart-cache/dashboard | /smart-cache/stats | /smart-cache/health ``` ```bash php artisan smart-cache:status -php artisan smart-cache:status --force php artisan smart-cache:clear +php artisan smart-cache:warm --warmer=products --warmer=categories +php artisan smart-cache:cleanup-chunks ``` ## Configuration @@ -335,15 +292,16 @@ return [ 'strategies' => [ 'compression' => ['enabled' => true, 'mode' => 'fixed', 'level' => 6], 'chunking' => ['enabled' => true, 'chunk_size' => 1000], + 'encryption' => ['enabled' => false, 'keys' => []], ], 'monitoring' => ['enabled' => true, 'metrics_ttl' => 3600], - 'circuit_breaker' => ['enabled' => true, 'failure_threshold' => 5, 'timeout' => 30], - 'rate_limiter' => ['enabled' => true, 'default_limit' => 100, 'window' => 60], - 'encryption' => ['enabled' => false, 'keys' => []], + 'circuit_breaker' => ['enabled' => false, 'failure_threshold' => 5, 'recovery_timeout' => 30], + 'rate_limiter' => ['enabled' => true, 'window' => 60, 'max_attempts' => 10], 'jitter' => ['enabled' => false, 'percentage' => 0.1], 'deduplication' => ['enabled' => true], // Write deduplication (Cache DNA) 'self_healing' => ['enabled' => true], // Auto-evict corrupted entries 'dashboard' => ['enabled' => false, 'prefix' => 'smart-cache', 'middleware' => ['web']], + 'warmers' => [], // Cache warmer classes for smart-cache:warm ]; ``` @@ -361,7 +319,7 @@ $users = SmartCache::get('users'); ## Documentation -[Full documentation](https://iazaran.github.io/smart-cache/) — Installation, API reference, SWR patterns, and more. +[Full documentation →](https://iazaran.github.io/smart-cache/) — Installation, API reference, SWR patterns, and more. ## Testing diff --git a/composer.json b/composer.json index 78ed6bb..98f1d08 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,15 @@ "cache serialization", "cache strategies", "php caching", - "laravel optimization" + "laravel optimization", + "stale-while-revalidate", + "swr", + "cache stampede prevention", + "cost-aware eviction", + "self-healing cache", + "write deduplication", + "psr-16", + "laravel cache replacement" ], "homepage": "https://github.com/iazaran/smart-cache", "type": "library", diff --git a/config/smart-cache.php b/config/smart-cache.php index 2ea4585..6d0cfa6 100644 --- a/config/smart-cache.php +++ b/config/smart-cache.php @@ -55,6 +55,12 @@ 'patterns' => [], // Regex patterns for keys to encrypt (e.g., '/^user_token_/') 'encrypt_all' => false, // Encrypt all cached values ], + 'serialization' => [ + 'enabled' => false, // Disabled by default + 'preferred_method' => 'auto', // auto, json, igbinary, php + 'auto_detect' => true, // Auto-detect best method per value + 'size_threshold' => 1024, // Minimum size in bytes to apply optimization + ], ], /* @@ -66,6 +72,7 @@ | */ 'circuit_breaker' => [ + 'enabled' => false, // Disabled by default — enable with withCircuitBreaker() 'failure_threshold' => 5, // Number of failures before opening circuit 'recovery_timeout' => 30, // Seconds to wait before trying again 'success_threshold' => 3, // Successful calls needed to close circuit @@ -80,6 +87,7 @@ | */ 'rate_limiter' => [ + 'enabled' => true, // Enable rate limiting for stampede protection 'window' => 60, // Window in seconds 'max_attempts' => 10, // Maximum attempts per window ], @@ -239,4 +247,22 @@ 'prefix' => 'smart-cache', // URL prefix for dashboard routes 'middleware' => ['web'], // Middleware to apply to dashboard routes ], + + /* + |-------------------------------------------------------------------------- + | Cache Warmers + |-------------------------------------------------------------------------- + | + | Register cache warmer classes to pre-warm the cache on demand via + | the `smart-cache:warm` Artisan command. Each warmer must implement + | a `warm(): array` method returning ['keys' => int]. + | + | Example: + | 'users' => App\CacheWarmers\UserCacheWarmer::class, + | + */ + 'warmers' => [ + // 'users' => App\CacheWarmers\UserCacheWarmer::class, + // 'products' => App\CacheWarmers\ProductCacheWarmer::class, + ], ]; \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 42e2f8e..ef026a5 100644 --- a/docs/index.html +++ b/docs/index.html @@ -6,6 +6,7 @@ + @@ -954,34 +955,6 @@

Strategy Selection

-

🎯 Adaptive Compression

-

Auto-optimize compression levels based on data characteristics:

- -
// Enable adaptive compression
-config(['smart-cache.strategies.compression.mode' => 'adaptive']);
-
-// Automatically selects optimal compression level:
-// - Hot data (frequently accessed) = level 3-4 (faster)
-// - Cold data (rarely accessed) = level 7-9 (smaller)
-// - Based on compressibility analysis
-
-SmartCache::put('hot_data', $frequentlyAccessed, 3600);
-SmartCache::put('cold_data', $rarelyAccessed, 3600);
- -

💾 Lazy Loading

-

Load large datasets on-demand, keeping only the active chunk in memory:

- -
// Enable lazy loading
-config(['smart-cache.strategies.chunking.lazy_loading' => true]);
-
-// Returns LazyChunkedCollection - loads chunks on-demand
-$largeDataset = SmartCache::get('100k_records');
-
-foreach ($largeDataset as $record) {
-    processRecord($record);
-    // Only 3 chunks in memory at once
-}
-

🧠 Smart Serialization

Auto-select best serialization method:

@@ -1454,11 +1427,11 @@

✨ Extended Features

🔐 Encryption Strategy

Encrypt sensitive cached data automatically:

-
// Configure encryption in config/smart-cache.php
+                    
// Configure encryption in config/smart-cache.php → strategies.encryption
 'encryption' => [
     'enabled' => true,
-    'keys' => ['user_*', 'payment_*'],     // Keys to encrypt
-    'patterns' => ['/secret_.*/'],          // Regex patterns
+    'keys' => ['user_token_abc123'],              // Exact cache-key match
+    'patterns' => ['/^user_/', '/^payment_/'],    // Regex patterns
 ],
 
 // Data matching patterns is automatically encrypted
@@ -1516,8 +1489,8 @@ 

🔌 Circuit Breaker

'circuit_breaker' => [ 'enabled' => true, 'failure_threshold' => 5, // Open after 5 failures - 'success_threshold' => 2, // Close after 2 successes - 'timeout' => 30, // Try again after 30 seconds + 'recovery_timeout' => 30, // Try again after 30 seconds + 'success_threshold' => 3, // Close after 3 successes ], // Check if cache is available @@ -1556,8 +1529,8 @@

🚦 Rate Limiting & Stampede Protection

// Configure rate limiter 'rate_limiter' => [ 'enabled' => true, - 'default_limit' => 100, 'window' => 60, + 'max_attempts' => 10, ],

🔥 Cache Warming

@@ -1568,26 +1541,35 @@

🔥 Cache Warming

class ProductCacheWarmer implements CacheWarmer { - public function warm(): void + public function warm(): array { $products = Product::with('images')->get(); SmartCache::put('all_products', $products, 3600); + + return ['keys' => 1]; + } + + public function getName(): string + { + return 'products'; } - public function getKeys(): array + public function getDescription(): string { - return ['all_products']; + return 'Warms the product cache'; } } -// Register in service provider -$this->app->tag([ProductCacheWarmer::class], 'cache.warmers'); +// Register in config/smart-cache.php +'warmers' => [ + 'products' => App\CacheWarmers\ProductCacheWarmer::class, +], // Run cache warming command php artisan smart-cache:warm -// Or warm specific keys -php artisan smart-cache:warm --keys=all_products,categories
+// Or warm specific warmers +php artisan smart-cache:warm --warmer=products

🧹 Orphan Chunk Cleanup

Automatically clean up orphaned chunks from expired data:

@@ -1742,23 +1724,27 @@

Configuration Reference

return [ // ... existing options ... - 'encryption' => [ - 'enabled' => false, - 'keys' => [], - 'patterns' => [], + 'strategies' => [ + // ... compression, chunking ... + 'encryption' => [ + 'enabled' => false, + 'keys' => [], + 'patterns' => [], + 'encrypt_all' => false, + ], ], 'circuit_breaker' => [ - 'enabled' => true, + 'enabled' => false, 'failure_threshold' => 5, - 'success_threshold' => 2, - 'timeout' => 30, + 'recovery_timeout' => 30, + 'success_threshold' => 3, ], 'rate_limiter' => [ 'enabled' => true, - 'default_limit' => 100, 'window' => 60, + 'max_attempts' => 10, ], 'jitter' => [ @@ -1785,6 +1771,10 @@

Configuration Reference

'prefix' => 'smart-cache', 'middleware' => ['web'], ], + + 'warmers' => [ + // 'products' => App\CacheWarmers\ProductCacheWarmer::class, + ], ]; @@ -2448,13 +2438,13 @@

✨ Resilience & Organization API

SmartCache::withFallback()
-
SmartCache::withFallback(callable $operation, callable $fallback): mixed
+
SmartCache::withFallback(callable $callback, mixed $fallback = null): mixed

Execute cache operation with automatic fallback on failure.

- $operation (callable) - Primary cache operation + $callback (callable) - Primary cache operation
- $fallback (callable) - Fallback if cache fails + $fallback (mixed) - Fallback value or callable if cache fails (default: null)
Returns: mixed - Result from operation or fallback @@ -2744,18 +2734,24 @@

💻 CLI Commands

smart-cache:warm
-
php artisan smart-cache:warm [--keys=]
+
php artisan smart-cache:warm [--warmer=*] [--list]

Pre-populate cache with frequently accessed data using registered warmers.

- --keys (optional) - Comma-separated list of specific keys to warm + --warmer (optional, repeatable) - Specific warmer(s) to run +
+
+ --list (optional) - List available warmers
Examples:
# Warm all registered cache warmers
 php artisan smart-cache:warm
 
-# Warm specific keys only
-php artisan smart-cache:warm --keys=products,categories
+# Warm specific warmers only +php artisan smart-cache:warm --warmer=products --warmer=categories + +# List available warmers +php artisan smart-cache:warm --list
diff --git a/src/Contracts/SmartCache.php b/src/Contracts/SmartCache.php index 96143ce..681dc42 100644 --- a/src/Contracts/SmartCache.php +++ b/src/Contracts/SmartCache.php @@ -392,11 +392,11 @@ public function getCircuitBreakerStats(): array; /** * Execute with fallback on circuit breaker open. * - * @param callable $primary - * @param callable $fallback + * @param callable $callback + * @param mixed $fallback * @return mixed */ - public function withFallback(callable $primary, callable $fallback): mixed; + public function withFallback(callable $callback, mixed $fallback = null): mixed; /** * Throttle cache operations. diff --git a/src/Drivers/MemoizedCacheDriver.php b/src/Drivers/MemoizedCacheDriver.php index 85cfed2..035d482 100644 --- a/src/Drivers/MemoizedCacheDriver.php +++ b/src/Drivers/MemoizedCacheDriver.php @@ -30,10 +30,15 @@ class MemoizedCacheDriver implements Repository protected array $memoizedMissing = []; /** - * @var array LRU tracking - keys in order of last access + * @var array LRU tracking - maps key to access counter value */ protected array $accessOrder = []; + /** + * @var int Monotonically increasing counter for O(1) LRU tracking + */ + protected int $accessCounter = 0; + /** * @var int Maximum number of items to keep in memory */ @@ -65,39 +70,39 @@ public function setMaxSize(int $maxSize): void /** * Touch a key to mark it as recently used. + * O(1) — assigns a monotonically increasing counter value to the key. * * @param string $key * @return void */ protected function touchKey(string $key): void { - // Remove from current position - $index = array_search($key, $this->accessOrder, true); - if ($index !== false) { - unset($this->accessOrder[$index]); - } - // Add to end (most recently used) - $this->accessOrder[] = $key; + $this->accessOrder[$key] = ++$this->accessCounter; } /** * Evict least recently used items if over capacity. + * Sorts by counter value and removes the lowest (oldest) entries. * * @return void */ protected function evictIfNeeded(): void { - while (count($this->memoized) > $this->maxSize) { - // Re-index array to ensure we get the first element - $this->accessOrder = array_values($this->accessOrder); + if (count($this->memoized) <= $this->maxSize) { + return; + } + + // Sort by access counter ascending — lowest values are least recently used + asort($this->accessOrder); + while (count($this->memoized) > $this->maxSize) { if (empty($this->accessOrder)) { break; } - // Remove the least recently used (first in array) - $lruKey = array_shift($this->accessOrder); - unset($this->memoized[$lruKey]); + // Remove the least recently used (lowest counter value) + $lruKey = array_key_first($this->accessOrder); + unset($this->accessOrder[$lruKey], $this->memoized[$lruKey]); } } @@ -343,11 +348,7 @@ public function rememberForever($key, \Closure $callback): mixed */ public function forget($key): bool { - $index = array_search($key, $this->accessOrder, true); - if ($index !== false) { - unset($this->accessOrder[$index]); - } - unset($this->memoized[$key], $this->memoizedMissing[$key]); + unset($this->accessOrder[$key], $this->memoized[$key], $this->memoizedMissing[$key]); return $this->repository->forget($key); } @@ -361,6 +362,7 @@ public function flush(): bool $this->memoized = []; $this->memoizedMissing = []; $this->accessOrder = []; + $this->accessCounter = 0; return $this->repository->flush(); } @@ -404,6 +406,7 @@ public function clearMemoization(): void $this->memoized = []; $this->memoizedMissing = []; $this->accessOrder = []; + $this->accessCounter = 0; } /** diff --git a/src/Events/OptimizationApplied.php b/src/Events/OptimizationApplied.php index 3009dd3..449c431 100644 --- a/src/Events/OptimizationApplied.php +++ b/src/Events/OptimizationApplied.php @@ -53,7 +53,7 @@ public function __construct(string $key, string $strategy, int $originalSize, in $this->strategy = $strategy; $this->originalSize = $originalSize; $this->optimizedSize = $optimizedSize; - $this->ratio = $optimizedSize > 0 ? $optimizedSize / $originalSize : 0; + $this->ratio = $originalSize > 0 ? $optimizedSize / $originalSize : 0; } } diff --git a/src/Facades/SmartCache.php b/src/Facades/SmartCache.php index 1a7ea6f..27fe602 100644 --- a/src/Facades/SmartCache.php +++ b/src/Facades/SmartCache.php @@ -12,20 +12,53 @@ * @method static bool forever(string $key, mixed $value) * @method static mixed remember(string $key, \DateTimeInterface|\DateInterval|int|null $ttl, \Closure $callback) * @method static mixed rememberForever(string $key, \Closure $callback) + * @method static mixed pull(string $key, mixed $default = null) + * @method static bool add(string $key, mixed $value, \DateTimeInterface|\DateInterval|int|null $ttl = null) + * @method static int|bool increment(string $key, int $value = 1) + * @method static int|bool decrement(string $key, int $value = 1) + * @method static bool clear() + * @method static array many(array $keys) + * @method static bool putMany(array $values, \DateTimeInterface|\DateInterval|int|null $ttl = null) + * @method static \SmartCache\SmartCache store(string|null $name = null) + * @method static \Illuminate\Contracts\Cache\Repository repository(string|null $name = null) + * @method static mixed getRaw(string $key) + * @method static \Illuminate\Contracts\Cache\Store getStore() * @method static mixed flexible(string $key, array $durations, \Closure $callback) * @method static mixed swr(string $key, \Closure $callback, int $ttl = 3600, int $staleTtl = 7200) * @method static mixed stale(string $key, \Closure $callback, int $ttl = 1800, int $staleTtl = 86400) * @method static mixed refreshAhead(string $key, \Closure $callback, int $ttl = 3600, int $refreshWindow = 600) - * @method static \SmartCache\SmartCache store(string|null $name = null) - * @method static \Illuminate\Contracts\Cache\Repository repository(string|null $name = null) - * @method static bool clear() - * @method static array getManagedKeys() + * @method static mixed asyncSwr(string $key, callable|string $callback, int $ttl = 3600, int $staleTtl = 7200, ?string $queue = null) + * @method static mixed rememberIf(string $key, mixed $ttl, \Closure $callback, callable $condition) + * @method static mixed rememberWithStampedeProtection(string $key, int $ttl, \Closure $callback, float $beta = 1.0) + * @method static \SmartCache\SmartCache memo(?string $store = null) + * @method static \SmartCache\SmartCache namespace(string $namespace) + * @method static \SmartCache\SmartCache withoutNamespace() + * @method static int flushNamespace(string $namespace) + * @method static array getNamespaceKeys(string $namespace) + * @method static \SmartCache\SmartCache withJitter(float $percentage = 0.1) + * @method static \SmartCache\SmartCache withoutJitter() + * @method static int|null applyJitter(?int $ttl, ?float $jitterPercentage = null) + * @method static bool putWithJitter(string $key, mixed $value, int $ttl, float $jitterPercentage = 0.1) + * @method static mixed rememberWithJitter(string $key, int $ttl, float $jitterPercentage, \Closure $callback) + * @method static \SmartCache\SmartCache withCircuitBreaker() + * @method static \SmartCache\SmartCache withoutCircuitBreaker() + * @method static bool isAvailable() + * @method static array getCircuitBreakerStats() + * @method static mixed withFallback(callable $callback, mixed $fallback = null) + * @method static mixed throttle(string $key, int $maxAttempts, int $decaySeconds, callable $callback) * @method static static tags(string|array $tags) * @method static bool flushTags(string|array $tags) * @method static static dependsOn(string $key, string|array $dependencies) * @method static bool invalidate(string $key) * @method static int flushPatterns(array $patterns) * @method static int invalidateModel(string $modelClass, mixed $modelId, array $relationships = []) + * @method static array|null cacheValue(string $key) + * @method static array getCacheValueReport() + * @method static array suggestEvictions(int $count = 10) + * @method static void persistCostMetadata() + * @method static array getManagedKeys() + * @method static int cleanupExpiredManagedKeys() + * @method static void persistManagedKeys() * @method static array getStatistics() * @method static array healthCheck() * @method static \SmartCache\Services\CacheInvalidationService invalidationService() @@ -34,9 +67,25 @@ * @method static array getPerformanceMetrics() * @method static void resetPerformanceMetrics() * @method static array analyzePerformance() - * @method static int cleanupExpiredManagedKeys() * @method static bool hasFeature(string $feature) - * @method static mixed rememberIf(string $key, mixed $ttl, \Closure $callback, callable $condition) + * @method static \Illuminate\Contracts\Cache\Lock lock(string $name, int $seconds = 0, ?string $owner = null) + * @method static \Illuminate\Contracts\Cache\Lock restoreLock(string $name, string $owner) + * @method static bool flush() + * @method static bool set(string $key, mixed $value, \DateTimeInterface|\DateInterval|int|null $ttl = null) + * @method static bool delete(string $key) + * @method static mixed sear(string $key, \Closure $callback) + * @method static iterable getMultiple(iterable $keys, mixed $default = null) + * @method static bool setMultiple(iterable $values, \DateTimeInterface|\DateInterval|int|null $ttl = null) + * @method static bool deleteMultiple(iterable $keys) + * @method static void refreshAsync(string $key, callable|string $callback, ?int $ttl = null, ?string $queue = null) + * @method static string|null getNamespace() + * @method static array cleanupOrphanChunks() + * @method static \SmartCache\Services\OrphanChunkCleanupService chunkCleanupService() + * @method static \SmartCache\Services\CircuitBreaker circuitBreaker() + * @method static \SmartCache\Services\RateLimiter rateLimiter() + * @method static self addStrategy(\SmartCache\Contracts\OptimizationStrategy $strategy) + * @method static array getStrategies() + * @method static \SmartCache\Services\CostAwareCacheManager|null getCostAwareManager() * * @see \SmartCache\SmartCache */ diff --git a/src/Providers/SmartCacheServiceProvider.php b/src/Providers/SmartCacheServiceProvider.php index 4409081..a176264 100644 --- a/src/Providers/SmartCacheServiceProvider.php +++ b/src/Providers/SmartCacheServiceProvider.php @@ -10,6 +10,7 @@ use SmartCache\Strategies\AdaptiveCompressionStrategy; use SmartCache\Strategies\ChunkingStrategy; use SmartCache\Strategies\EncryptionStrategy; +use SmartCache\Strategies\SmartSerializationStrategy; use SmartCache\Console\Commands\ClearCommand; use SmartCache\Console\Commands\CleanupChunksCommand; use SmartCache\Console\Commands\StatusCommand; @@ -79,6 +80,15 @@ public function register(): void ); } + // Add smart serialization strategy if enabled + if ($config->get('smart-cache.strategies.serialization.enabled', false)) { + $strategies[] = new SmartSerializationStrategy( + $config->get('smart-cache.strategies.serialization.preferred_method', 'auto'), + $config->get('smart-cache.strategies.serialization.auto_detect', true), + $config->get('smart-cache.strategies.serialization.size_threshold', 1024) + ); + } + // Create cost-aware manager if enabled $costAwareManager = null; if ($config->get('smart-cache.cost_aware.enabled', true)) { @@ -131,6 +141,23 @@ public function boot(): void } }); + // Register terminating callback to persist adaptive compression frequency data + $this->app->terminating(function () { + try { + $smartCache = $this->app->make(SmartCacheContract::class); + // Access the strategies to find the AdaptiveCompressionStrategy + if (\method_exists($smartCache, 'getStrategies')) { + foreach ($smartCache->getStrategies() as $strategy) { + if ($strategy instanceof AdaptiveCompressionStrategy) { + $strategy->persistFrequency(); + } + } + } + } catch (\Throwable $e) { + // Silently fail — don't break the response + } + }); + // Register command metadata for HTTP context $this->app->singleton('smart-cache.commands', fn () => [ 'smart-cache:clear' => [ diff --git a/src/SmartCache.php b/src/SmartCache.php index 5703fce..f622f02 100644 --- a/src/SmartCache.php +++ b/src/SmartCache.php @@ -248,6 +248,13 @@ public function __construct( // Initialize self-healing cache $this->selfHealingEnabled = (bool) $config->get('smart-cache.self_healing.enabled', true); + // Initialize circuit breaker from config + $this->circuitBreakerEnabled = (bool) $config->get('smart-cache.circuit_breaker.enabled', false); + + // Initialize TTL jitter from config + $this->jitterEnabled = (bool) $config->get('smart-cache.jitter.enabled', false); + $this->jitterPercentage = (float) $config->get('smart-cache.jitter.percentage', 0.1); + // Managed keys, dependencies, and performance metrics are lazy-loaded on first access } @@ -263,6 +270,16 @@ public function addStrategy(OptimizationStrategy $strategy): self return $this; } + /** + * Get all registered optimization strategies. + * + * @return array + */ + public function getStrategies(): array + { + return $this->strategies; + } + /** * {@inheritdoc} */ diff --git a/src/Strategies/AdaptiveCompressionStrategy.php b/src/Strategies/AdaptiveCompressionStrategy.php index dac5a4e..e7305dd 100644 --- a/src/Strategies/AdaptiveCompressionStrategy.php +++ b/src/Strategies/AdaptiveCompressionStrategy.php @@ -110,9 +110,14 @@ public function setCacheRepository(Repository $cache): static */ public function shouldApply(mixed $value, array $context = []): bool { + // Guard: zlib extension is required for gzencode/gzdecode + if (!function_exists('gzencode')) { + return false; + } + // Check if driver supports compression - if (isset($context['driver']) && - isset($context['config']['drivers'][$context['driver']]['compression']) && + if (isset($context['driver']) && + isset($context['config']['drivers'][$context['driver']]['compression']) && $context['config']['drivers'][$context['driver']]['compression'] === false) { return false; } diff --git a/src/Strategies/CompressionStrategy.php b/src/Strategies/CompressionStrategy.php index 36ff64a..0948a93 100644 --- a/src/Strategies/CompressionStrategy.php +++ b/src/Strategies/CompressionStrategy.php @@ -33,6 +33,11 @@ public function __construct(int $threshold = 51200, int $level = 6) */ public function shouldApply(mixed $value, array $context = []): bool { + // Guard: zlib extension is required for gzencode/gzdecode + if (!function_exists('gzencode')) { + return false; + } + // Check if driver supports compression if (isset($context['driver']) && isset($context['config']['drivers'][$context['driver']]['compression']) &&