kevinrob/guzzle-cache-middleware
RFC 7234–compliant HTTP cache middleware for Guzzle 6+ using a HandlerStack. Improve API call performance with transparent caching. Supports PSR-7 and multiple storages: Laravel cache, Flysystem, PSR-6/16, and WordPress object cache.
Installation:
composer require kevinrob/guzzle-cache-middleware
Basic Integration (Laravel example):
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use Kevinrob\GuzzleCache\CacheMiddleware;
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
use Kevinrob\GuzzleCache\Storage\LaravelCacheStorage;
$stack = HandlerStack::create();
$stack->push(
new CacheMiddleware(
new PrivateCacheStrategy(
new LaravelCacheStorage(app('cache.store'))
)
),
'cache'
);
$client = new Client(['handler' => $stack]);
First Use Case: Cache API responses for a Laravel controller:
public function fetchData()
{
$response = $client->get('https://api.example.com/data');
return response()->json(json_decode($response->getBody(), true));
}
CacheMiddleware: Core middleware.PrivateCacheStrategy/PublicCacheStrategy: Default strategies.GreedyCacheStrategy: For custom TTLs.DelegatingCacheStrategy: For request-specific rules.PrivateCacheStrategy for non-shared data (e.g., user-specific API calls).PublicCacheStrategy for shared resources (e.g., public API endpoints).$stack->push(
new CacheMiddleware(
new GreedyCacheStrategy(
new LaravelCacheStorage(app('cache.store')),
ttl: 3600, // Custom TTL in seconds
headers: ['Authorization'] // Headers affecting cache key
)
)
);
Use DelegatingCacheStrategy to apply rules per domain/endpoint:
$strategy = new DelegatingCacheStrategy(new NullCacheStrategy());
$strategy->registerRequestMatcher(
new class implements RequestMatcherInterface {
public function matches(RequestInterface $request) {
return str_contains($request->getUri()->getHost(), 'api.example.com');
}
},
new PublicCacheStrategy(new LaravelCacheStorage(app('cache.store')))
);
Wrap Guzzle clients in a service provider:
public function register()
{
$this->app->singleton('guzzle', function () {
$stack = HandlerStack::create();
$stack->push(
new CacheMiddleware(
new PrivateCacheStrategy(
new LaravelCacheStorage(app('cache.store'))
)
)
);
return new Client(['handler' => $stack]);
});
}
Manually clear cache for specific requests:
$client->get('https://api.example.com/data', [
'on_stats' => function (TransferStats $stats) {
if ($stats->getHandlerStats('cache')['cache_hit']) {
app('cache')->forget($stats->getHandlerStats('cache')['cache_key']);
}
}
]);
Mock the cache layer:
$mockCache = $this->createMock(Psr6CacheStorage::class);
$mockCache->method('fetch')->willReturn(null);
$middleware = new CacheMiddleware(new PrivateCacheStrategy($mockCache));
Binary Data Truncation:
stream_get_contents() is used for binary responses (fixed in v7.0.0).GuzzleHttp\Psr7\Stream explicitly:
$stream = new Stream(fopen('php://temp', 'r+'));
$stream->write($response->getBody()->getContents());
DST Timezone Bugs:
PSR-20 Clock (v8.0.0+) for deterministic timestamps:
use Kevinrob\GuzzleCache\Clock\SystemClock;
$strategy = new PrivateCacheStrategy($storage, new SystemClock());
Authorization Headers in Public Cache:
Authorization headers by default (RFC 9111 compliance).GreedyCacheStrategy:
new GreedyCacheStrategy($storage, ttl: 3600, headers: ['Authorization'])
Corrupted Cache Entries:
CacheMiddleware::INVALIDATE_ALL (v8.0.0+) to purge:
CacheMiddleware::invalidateAll($storage);
Laravel Cache Duration Changes:
Cache::put() TTL overrides middleware settings. Use GreedyCacheStrategy for consistency.$client->get('...', [
'on_stats' => function (TransferStats $stats) {
dump($stats->getHandlerStats('cache'));
}
]);
CacheMiddleware to log keys:
class DebugCacheMiddleware extends CacheMiddleware {
public function __construct($strategy) {
parent::__construct($strategy);
$this->strategy->setLogger(function ($key, $method) {
\Log::debug("Cache $method: $key");
});
}
}
Custom Storage:
Implement Kevinrob\GuzzleCache\Storage\StorageInterface for new backends (e.g., Redis clusters):
class RedisClusterStorage implements StorageInterface {
public function fetch($key) { /* ... */ }
public function save($key, $entry, $ttl) { /* ... */ }
public function delete($key) { /* ... */ }
}
Dynamic TTLs:
Override GreedyCacheStrategy to fetch TTLs from headers:
class DynamicTTLCacheStrategy extends GreedyCacheStrategy {
protected function getTtl(RequestInterface $request) {
return (int) $request->getHeaderLine('X-Cache-TTL') ?: parent::getTtl($request);
}
}
Request Matching:
Extend RequestMatcherInterface for complex rules (e.g., path patterns):
class PathMatcher implements RequestMatcherInterface {
public function matches(RequestInterface $request) {
return preg_match('/^\/api\/v\d+\//', $request->getUri()->getPath());
}
}
Psr6CacheStorage) is slower for high-throughput apps due to adapter overhead. Use Laravel’s native cache (LaravelCacheStorage) for better performance.app('cache.store')->getStore()->getStats();
PrivateCacheStrategy uses 0 (no cache) by default. Always specify a TTL:
new PrivateCacheStrategy($storage, ttl: 300) // 5 minutes
Vary headers but ignores Cookie/Authorization in public cache (RFC 7234). Explicitly include them in GreedyCacheStrategy if needed.How can I help you explore Laravel packages today?