php-http/cache-plugin
PSR-6 cache plugin for HTTPlug clients. Automatically caches HTTP responses (and can serve stale on error) with configurable cache strategies, TTL, and cache key generation. Drop it into your plugin chain to cut latency and reduce repeated requests.
Install the package and dependencies:
composer require php-http/cache-plugin php-http/httplug-bundle cache/simple-filesystem-adapter
(Replace simple-filesystem-adapter with your preferred PSR-6 cache, e.g., predis/predis for Redis.)
Configure the cache pool (e.g., in config/cache.php):
'connections' => [
'http_cache' => [
'driver' => 'redis',
'host' => env('REDIS_HOST', '127.0.0.1'),
// ... other Redis config
],
],
Wrap your HTTP client (e.g., in a service provider):
use Http\Client\Common\Plugin\CachePlugin;
use Http\Client\Common\Plugin\CachePlugin\CacheKeyGenerator\HeaderCacheKeyGenerator;
use Http\Client\Common\Plugin\CachePlugin\CachePolicy\CachePolicy;
use Http\Client\Common\Plugin\CachePlugin\CachePolicy\CachePolicyInterface;
use Http\Client\Common\Plugin\CachePlugin\CachePolicy\CachePolicyFactory;
use Http\Client\Common\Plugin\CachePlugin\CachePolicy\CachePolicyFactoryInterface;
use Http\Client\Common\Plugin\CachePlugin\CachePolicy\CachePolicyFactoryInterface;
public function register()
{
$this->app->singleton('http.client.cached', function ($app) {
$client = $app['http.client']; // Your base HTTPlug client
$cachePool = $app['cache']->connection('http_cache')->getPsr6Connection();
$cachePlugin = new CachePlugin(
$cachePool,
new HeaderCacheKeyGenerator(), // Customize key generation
new CachePolicyFactory() // Customize cache policies
);
return $cachePlugin->attachTo($client);
});
}
First use case: Cache API responses for a Laravel controller:
public function getData()
{
$cachedClient = app('http.client.cached');
$response = $cachedClient->get('https://api.example.com/data');
return json_decode($response->getBody(), true);
}
Cache-Control headers by default. Use respect_response_cache_directives to fine-tune:
$cachePlugin = new CachePlugin($cachePool, null, [
'respect_response_cache_directives' => ['max-age', 'must-revalidate'],
]);
ETag headers, use EtagCachePlugin:
$etagPlugin = new EtagCachePlugin($cachePool);
$client = $etagPlugin->attachTo($client);
/admin/*):
$cachePlugin = new CachePlugin($cachePool, null, [
'blacklisted_paths' => ['/admin/', '/api/tokens'],
]);
SimpleGenerator (hashes URL + method + headers).CacheKeyGenerator for complex logic (e.g., include query params):
use Http\Client\Common\Plugin\CachePlugin\CacheKeyGenerator\CacheKeyGeneratorInterface;
class QueryParamCacheKeyGenerator implements CacheKeyGeneratorInterface
{
public function generateKey(RequestInterface $request): string
{
return sha1($request->getUri() . $request->getMethod() . $request->getBody());
}
}
X-Cache headers to responses:
use Http\Client\Common\Plugin\CachePlugin\CacheListener\AddHeaderCacheListener;
$listener = new AddHeaderCacheListener();
$cachePlugin = new CachePlugin($cachePool, null, [
'cache_listeners' => [$listener],
]);
CacheListenerInterface to log cache hits/misses or invalidate related caches.public function handle($request, Closure $next)
{
$cachedClient = app('http.client.cached');
$response = $cachedClient->sendRequest($request->toPsrRequest());
return response($response->getBody(), $response->getStatusCode(), $response->getHeaders());
}
public function handle()
{
$client = app('http.client.cached');
$response = $client->get('https://api.example.com/updates');
// Process response...
}
Cache\Adapter\PHPArray\ArrayCachePool for unit tests:
$cachePool = new ArrayCachePool();
$cachePlugin = new CachePlugin($cachePool);
$cachedClient = $cachePlugin->attachTo($client);
X-Cache headers or check cache keys:
$this->assertEquals('HIT', $response->getHeader('X-Cache')[0]);
Stream Handling:
$response->getBody()->rewind();
StreamFactoryInterface (PSR-17) to recreate streams if needed.Cache Key Collisions:
/users?page=1 vs. /users?page=2).HeaderCacheKeyGenerator or a custom generator to include query params.TTL Conflicts:
default_ttl (e.g., 0 for session caching) can override Cache-Control headers.default_ttl to null to rely solely on response headers:
$cachePlugin = new CachePlugin($cachePool, null, ['default_ttl' => null]);
ETag/Last-Modified Validation:
ETag/Last-Modified if the server provides them.ETag but no Last-Modified), validation may fail silently.X-Cache headers or enable cache_listeners to log validation attempts.Blacklist Regex:
blacklisted_paths uses regex, not glob patterns. Escape special chars:
'blacklisted_paths' => ['/api/v1/.*/webhooks'], // Matches `/api/v1/users/webhooks`
Symfony HttpClient:
HttpClient, ensure you’re using the PSR-18 version (not the legacy HttpClient):
$client = SymfonyHttpClient::create(['base_uri' => 'https://api.example.com']);
Enable Cache Headers:
Add AddHeaderCacheListener to inspect cache behavior:
$cachePlugin = new CachePlugin($cachePool, null, [
'cache_listeners' => [new AddHeaderCacheListener()],
]);
X-Cache: HIT → Response served from cache.X-Cache: MISS → Response fetched from origin.Inspect Cache Keys: Log keys to verify they’re unique:
$cachePlugin->getCacheKeyGenerator()->generateKey($request);
Clear Cache Manually:
Use your PSR-6 cache adapter’s clear() method:
$cachePool->clear();
Test with default_ttl = 1:
Set a short TTL (e.g., 1 second) to verify cache invalidation:
$cachePlugin = new CachePlugin($cachePool, null, ['default_ttl' => 1]);
CachePolicyInterface to define dynamic TTLs or cache rules:
class DynamicCachePolicy implements CachePolicyInterface
{
public function getTtl(RequestInterface $request, ResponseInterface $response): ?int
{
if ($request->getUri()->getPath() === '/promotions') {
return 3600; //
How can I help you explore Laravel packages today?