Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
294 changes: 126 additions & 168 deletions README.md

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
26 changes: 26 additions & 0 deletions config/smart-cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
],
],

/*
Expand All @@ -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
Expand All @@ -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
],
Expand Down Expand Up @@ -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,
],
];
108 changes: 52 additions & 56 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<meta name="description" content="Laravel SmartCache — Intelligent caching with automatic compression, chunking, cost-aware eviction, self-healing recovery, write deduplication, and full Laravel Cache API compatibility. A drop-in replacement for the Cache facade supporting PHP 8.1+ and Laravel 8–12.">
<meta name="keywords" content="Laravel, caching, PHP, performance, SWR, compression, enterprise, cost-aware, Redis, optimization, cache stampede, PSR-16, write deduplication, self-healing cache, conditional caching, Memcached, file cache, chunking">
<meta name="author" content="Ismael Azaran">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://iazaran.github.io/smart-cache/">

<!-- Open Graph -->
Expand Down Expand Up @@ -954,34 +955,6 @@ <h2>Strategy Selection</h2>
</tbody>
</table>

<h2>🎯 Adaptive Compression</h2>
<p>Auto-optimize compression levels based on data characteristics:</p>

<pre><code class="language-php">// 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);</code></pre>

<h2>💾 Lazy Loading</h2>
<p>Load large datasets on-demand, keeping only the active chunk in memory:</p>

<pre><code class="language-php">// 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
}</code></pre>

<h2>🧠 Smart Serialization</h2>
<p>Auto-select best serialization method:</p>

Expand Down Expand Up @@ -1454,11 +1427,11 @@ <h1>✨ Extended Features</h1>
<h2>🔐 Encryption Strategy</h2>
<p>Encrypt sensitive cached data automatically:</p>

<pre><code class="language-php">// Configure encryption in config/smart-cache.php
<pre><code class="language-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
Expand Down Expand Up @@ -1516,8 +1489,8 @@ <h2>🔌 Circuit Breaker</h2>
'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
Expand Down Expand Up @@ -1556,8 +1529,8 @@ <h2>🚦 Rate Limiting & Stampede Protection</h2>
// Configure rate limiter
'rate_limiter' => [
'enabled' => true,
'default_limit' => 100,
'window' => 60,
'max_attempts' => 10,
],</code></pre>

<h2>🔥 Cache Warming</h2>
Expand All @@ -1568,26 +1541,35 @@ <h2>🔥 Cache Warming</h2>

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</code></pre>
// Or warm specific warmers
php artisan smart-cache:warm --warmer=products</code></pre>

<h2>🧹 Orphan Chunk Cleanup</h2>
<p>Automatically clean up orphaned chunks from expired data:</p>
Expand Down Expand Up @@ -1742,23 +1724,27 @@ <h2>Configuration Reference</h2>
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' => [
Expand All @@ -1785,6 +1771,10 @@ <h2>Configuration Reference</h2>
'prefix' => 'smart-cache',
'middleware' => ['web'],
],

'warmers' => [
// 'products' => App\CacheWarmers\ProductCacheWarmer::class,
],
];</code></pre>
</div>

Expand Down Expand Up @@ -2448,13 +2438,13 @@ <h2 id="resilience-api">✨ Resilience & Organization API</h2>

<div class="method">
<div class="method-name">SmartCache::withFallback()</div>
<div class="method-signature">SmartCache::withFallback(callable $operation, callable $fallback): mixed</div>
<div class="method-signature">SmartCache::withFallback(callable $callback, mixed $fallback = null): mixed</div>
<p>Execute cache operation with automatic fallback on failure.</p>
<div class="parameter">
<span class="parameter-name">$operation</span> <span class="parameter-type">(callable)</span> - Primary cache operation
<span class="parameter-name">$callback</span> <span class="parameter-type">(callable)</span> - Primary cache operation
</div>
<div class="parameter">
<span class="parameter-name">$fallback</span> <span class="parameter-type">(callable)</span> - Fallback if cache fails
<span class="parameter-name">$fallback</span> <span class="parameter-type">(mixed)</span> - Fallback value or callable if cache fails (default: null)
</div>
<div class="parameter">
<span class="return-type">Returns:</span> mixed - Result from operation or fallback
Expand Down Expand Up @@ -2744,18 +2734,24 @@ <h2 id="cli-api">💻 CLI Commands</h2>

<div class="method">
<div class="method-name">smart-cache:warm</div>
<div class="method-signature">php artisan smart-cache:warm [--keys=]</div>
<div class="method-signature">php artisan smart-cache:warm [--warmer=*] [--list]</div>
<p>Pre-populate cache with frequently accessed data using registered warmers.</p>
<div class="parameter">
<span class="parameter-name">--keys</span> <span class="parameter-type">(optional)</span> - Comma-separated list of specific keys to warm
<span class="parameter-name">--warmer</span> <span class="parameter-type">(optional, repeatable)</span> - Specific warmer(s) to run
</div>
<div class="parameter">
<span class="parameter-name">--list</span> <span class="parameter-type">(optional)</span> - List available warmers
</div>
<div class="example">
<strong>Examples:</strong>
<pre><code class="language-bash"># Warm all registered cache warmers
php artisan smart-cache:warm

# Warm specific keys only
php artisan smart-cache:warm --keys=products,categories</code></pre>
# Warm specific warmers only
php artisan smart-cache:warm --warmer=products --warmer=categories

# List available warmers
php artisan smart-cache:warm --list</code></pre>
</div>
</div>

Expand Down
6 changes: 3 additions & 3 deletions src/Contracts/SmartCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
41 changes: 22 additions & 19 deletions src/Drivers/MemoizedCacheDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,15 @@ class MemoizedCacheDriver implements Repository
protected array $memoizedMissing = [];

/**
* @var array LRU tracking - keys in order of last access
* @var array<string, int> 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
*/
Expand Down Expand Up @@ -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]);
}
}

Expand Down Expand Up @@ -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);
}

Expand All @@ -361,6 +362,7 @@ public function flush(): bool
$this->memoized = [];
$this->memoizedMissing = [];
$this->accessOrder = [];
$this->accessCounter = 0;
return $this->repository->flush();
}

Expand Down Expand Up @@ -404,6 +406,7 @@ public function clearMemoization(): void
$this->memoized = [];
$this->memoizedMissing = [];
$this->accessOrder = [];
$this->accessCounter = 0;
}

/**
Expand Down
Loading