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.
Install the package:
composer require kevinrob/guzzle-cache-middleware
Basic Guzzle integration (for Laravel or standalone PHP):
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use Kevinrob\GuzzleCache\CacheMiddleware;
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
use Kevinrob\GuzzleCache\Storage\Psr6CacheStorage;
use Cache\Adapter\PHPArray\ArrayCachePool; // Fallback for testing
$stack = HandlerStack::create();
$stack->push(
new CacheMiddleware(
new PrivateCacheStrategy(
new Psr6CacheStorage(new ArrayCachePool())
)
),
'cache'
);
$client = new Client(['handler' => $stack]);
First use case: Cache API responses for a Laravel controller:
public function fetchData()
{
$response = $this->client->get('https://api.example.com/data');
return response()->json(json_decode($response->getBody(), true));
}
src/CacheMiddleware.php: Core middleware logic.src/Strategy/: Cache strategies (PrivateCacheStrategy, PublicCacheStrategy, GreedyCacheStrategy).src/Storage/: Storage adapters (Laravel, PSR6, Flysystem, etc.).tests/: Real-world usage examples and edge cases.use Illuminate\Support\Facades\Cache;
use Kevinrob\GuzzleCache\Storage\LaravelCacheStorage;
$stack->push(
new CacheMiddleware(
new PrivateCacheStrategy(
new LaravelCacheStorage(Cache::store('redis'))
)
),
'api-cache'
);
$request = $this->client->getAsync('https://api.example.com/data')
->then(function (ResponseInterface $response) {
return $response->getBody();
});
// Override TTL for greedy caching
$request->withHeader('X-Cache-TTL', 3600); // 1 hour
| Strategy | Use Case | Headers Respected |
|---|---|---|
PrivateCacheStrategy |
User-specific data (e.g., dashboards, personalized APIs). | Cache-Control: private |
PublicCacheStrategy |
Shared data (e.g., public APIs, static assets). | Cache-Control: public |
GreedyCacheStrategy |
APIs with missing/inconsistent Cache-Control headers. |
Custom TTL (ignores headers) |
NullCacheStrategy |
Disable caching for specific endpoints (via Delegate strategy). |
None |
Example: Delegate Strategy for Mixed APIs
$delegateStrategy = new DelegatingCacheStrategy(new NullCacheStrategy());
$delegateStrategy->registerRequestMatcher(
new class implements RequestMatcherInterface {
public function matches(RequestInterface $request) {
return str_contains($request->getUri()->getHost(), 'internal-api.example.com');
}
},
new PrivateCacheStrategy(new Psr6CacheStorage($cachePool))
);
$stack->push(new CacheMiddleware($delegateStrategy), 'delegate-cache');
public function register()
{
$this->app->singleton('api.client', function ($app) {
$stack = HandlerStack::create();
$stack->push(
new CacheMiddleware(
new PrivateCacheStrategy(
new LaravelCacheStorage($app['cache.store'])
)
),
'api-cache'
);
return new Client(['handler' => $stack, 'base_uri' => 'https://api.example.com']);
});
}
Route::get('/dashboard', function () {
$client = app('api.client');
$response = $client->get('/user-data');
return view('dashboard', ['data' => json_decode($response->getBody(), true)]);
})->middleware('cache.headers'); // Optional: Add custom middleware to validate headers.
public function testCachedResponse()
{
$cachePool = $this->createMock(Psr6CacheItemPoolInterface::class);
$cachePool->method('getItem')
->willReturn($this->createMock(Psr6CacheItemInterface::class));
$strategy = new PrivateCacheStrategy(new Psr6CacheStorage($cachePool));
$middleware = new CacheMiddleware($strategy);
$request = new Request('GET', 'https://api.example.com/data');
$response = new Response(200, [], '{"cached": true}');
$this->assertEquals('cached', $middleware->__invoke($request, $response)->getBody());
}
public function testCacheMiss()
{
$cachePool = $this->createMock(Psr6CacheItemPoolInterface::class);
$cachePool->method('getItem')
->willReturnCallback(function ($key) {
throw new CacheItemNotFoundException();
});
$strategy = new PrivateCacheStrategy(new Psr6CacheStorage($cachePool));
$middleware = new CacheMiddleware($strategy);
$request = new Request('GET', 'https://api.example.com/data');
$response = $this->createMock(ResponseInterface::class);
$this->assertFalse($middleware->__invoke($request, $response)->isCached());
}
$stack->push(
new CacheMiddleware(
new DelegatingCacheStrategy(new NullCacheStrategy())
),
'no-cache'
);
// Override for allowed endpoints
$delegateStrategy->registerRequestMatcher(
new class implements RequestMatcherInterface {
public function matches(RequestInterface $request) {
return str_contains($request->getUri()->getPath(), '/public');
}
},
new PublicCacheStrategy(new Psr6CacheStorage($cachePool))
);
LocalFilesystemAdapter).
$storage = new FlysystemStorage(
new LocalFilesystemAdapter('/path/to/cache', ['disable_asserts' => true])
);
Vary headers are ignored.GreedyCacheStrategy with custom headers or ensure Vary is respected:
$strategy = new PrivateCacheStrategy(
new Psr6CacheStorage($cachePool),
['Authorization', 'X-User-ID'] // Headers to include in cache key
);
0 or null) may cause unexpected cache behavior.GreedyCacheStrategy:
$strategy = new GreedyCacheStrategy($storage, 3600); // Default TTL: 1 hour
$strategy->setDefaultTTL(3600); // Explicitly set default
Cache-Control directives.PrivateCacheStrategy with explicit TTL:
$strategy = new PrivateCacheStrategy(
new LaravelCacheStorage(Cache::store('redis')),
300 // Override TTL in seconds
);
use Kevinrob\GuzzleCache\Utils;
$cacheEntry = Utils::inspectAll($storage->getItem('cache-key'));
dd($cacheEntry); // Debug cache metadata (TTL, headers, etc.)
$middleware = new CacheMiddleware($strategy);
$middleware->setLogger($this->app->make(LoggerInterface::class));
// Logs cache operations to Laravel's log channel.
// Manually delete corrupted entries (e.g., truncated files).
$storage->delete('corrupted-cache-key');
How can I help you explore Laravel packages today?