A HTTP Cache for Guzzle 6. It's a simple Middleware to be added in the HandlerStack.

Overview

guzzle-cache-middleware

Latest Stable Version Total Downloads License Tests Scrutinizer Code Quality Code Coverage

A HTTP Cache for Guzzle 6+. It's a simple Middleware to be added in the HandlerStack.

Goals

  • RFC 7234 compliance
  • Performance and transparency
  • Assured compatibility with PSR-7

Built-in storage interfaces

Installation

composer require kevinrob/guzzle-cache-middleware

or add it the your composer.json and run composer update kevinrob/guzzle-cache-middleware.

Why?

Performance. It's very common to do some HTTP calls to an API for rendering a page and it takes times to do it.

How?

With a simple Middleware added at the top of the HandlerStack of Guzzle.

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use Kevinrob\GuzzleCache\CacheMiddleware;

// Create default HandlerStack
$stack = HandlerStack::create();

// Add this middleware to the top with `push`
$stack->push(new CacheMiddleware(), 'cache');

// Initialize the client with the handler option
$client = new Client(['handler' => $stack]);

Examples

Doctrine/Cache

You can use a cache from Doctrine/Cache:

[...]
use Doctrine\Common\Cache\FilesystemCache;
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
use Kevinrob\GuzzleCache\Storage\DoctrineCacheStorage;

[...]
$stack->push(
  new CacheMiddleware(
    new PrivateCacheStrategy(
      new DoctrineCacheStorage(
        new FilesystemCache('/tmp/')
      )
    )
  ),
  'cache'
);

You can use ChainCache for using multiple CacheProvider instances. With that provider, you have to sort the different caches from the faster to the slower. Like that, you can have a very fast cache.

[...]
use Doctrine\Common\Cache\ChainCache;
use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\Cache\FilesystemCache;
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
use Kevinrob\GuzzleCache\Storage\DoctrineCacheStorage;

[...]
$stack->push(new CacheMiddleware(
  new PrivateCacheStrategy(
    new DoctrineCacheStorage(
      new ChainCache([
        new ArrayCache(),
        new FilesystemCache('/tmp/'),
      ])
    )
  )
), 'cache');

Laravel cache

You can use a cache with Laravel, e.g. Redis, Memcache etc.:

[...]
use Illuminate\Support\Facades\Cache;
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
use Kevinrob\GuzzleCache\Storage\LaravelCacheStorage;

[...]

$stack->push(
  new CacheMiddleware(
    new PrivateCacheStrategy(
      new LaravelCacheStorage(
        Cache::store('redis')
      )
    )
  ),
  'cache'
);

Flysystem

[...]
use League\Flysystem\Adapter\Local;
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
use Kevinrob\GuzzleCache\Storage\FlysystemStorage;

[...]

$stack->push(
  new CacheMiddleware(
    new PrivateCacheStrategy(
      new FlysystemStorage(
        new Local('/path/to/cache')
      )
    )
  ),
  'cache'
);

WordPress Object Cache

[...]
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
use Kevinrob\GuzzleCache\Storage\WordPressObjectCacheStorage;

[...]

$stack->push(
  new CacheMiddleware(
    new PrivateCacheStrategy(
      new WordPressObjectCacheStorage()
    )
  ),
  'cache'
);

Public and shared

It's possible to add a public shared cache to the stack:

[...]
use Doctrine\Common\Cache\FilesystemCache;
use Doctrine\Common\Cache\PredisCache;
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
use Kevinrob\GuzzleCache\Strategy\PublicCacheStrategy;
use Kevinrob\GuzzleCache\Storage\DoctrineCacheStorage;

[...]
// Private caching
$stack->push(
  new CacheMiddleware(
    new PrivateCacheStrategy(
      new DoctrineCacheStorage(
        new FilesystemCache('/tmp/')
      )
    )
  ),
  'private-cache'
);

// Public caching
$stack->push(
  new CacheMiddleware(
    new PublicCacheStrategy(
      new DoctrineCacheStorage(
        new PredisCache(
          new Predis\Client('tcp://10.0.0.1:6379')
        )
      )
    )
  ),
  'shared-cache'
);

Greedy caching

In some cases servers might send insufficient or no caching headers at all. Using the greedy caching strategy allows defining an expiry TTL on your own while disregarding any possibly present caching headers:

[...]
use Kevinrob\GuzzleCache\KeyValueHttpHeader;
use Kevinrob\GuzzleCache\Strategy\GreedyCacheStrategy;
use Kevinrob\GuzzleCache\Storage\DoctrineCacheStorage;
use Doctrine\Common\Cache\FilesystemCache;

[...]
// Greedy caching
$stack->push(
  new CacheMiddleware(
    new GreedyCacheStrategy(
      new DoctrineCacheStorage(
        new FilesystemCache('/tmp/')
      ),
      1800, // the TTL in seconds
      new KeyValueHttpHeader(['Authorization']) // Optional - specify the headers that can change the cache key
    )
  ),
  'greedy-cache'
);

Delegate caching

Because your client may call different apps, on different domains, you may need to define which strategy is suitable to your requests.

To solve this, all you have to do is to define a default cache strategy, and override it by implementing your own Request Matchers.

Here's an example:

namespace App\RequestMatcher;

use Kevinrob\GuzzleCache\Strategy\Delegate\RequestMatcherInterface;
use Psr\Http\Message\RequestInterface;

class ExampleOrgRequestMatcher implements RequestMatcherInterface
{

    /**
     * @inheritDoc
     */
    public function matches(RequestInterface $request)
    {
        return false !== strpos($request->getUri()->getHost(), 'example.org');
    }
}
namespace App\RequestMatcher;

use Kevinrob\GuzzleCache\Strategy\Delegate\RequestMatcherInterface;
use Psr\Http\Message\RequestInterface;

class TwitterRequestMatcher implements RequestMatcherInterface
{

    /**
     * @inheritDoc
     */
    public function matches(RequestInterface $request)
    {
        return false !== strpos($request->getUri()->getHost(), 'twitter.com');
    }
}
require_once __DIR__ . '/vendor/autoload.php';

use App\RequestMatcher\ExampleOrgRequestMatcher;
use App\RequestMatcher\TwitterRequestMatcher;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use Kevinrob\GuzzleCache\CacheMiddleware;
use Kevinrob\GuzzleCache\Strategy;

$strategy = new Strategy\Delegate\DelegatingCacheStrategy($defaultStrategy = new Strategy\NullCacheStrategy());
$strategy->registerRequestMatcher(new ExampleOrgRequestMatcher(), new Strategy\PublicCacheStrategy());
$strategy->registerRequestMatcher(new TwitterRequestMatcher(), new Strategy\PrivateCacheStrategy());

$stack = HandlerStack::create();
$stack->push(new CacheMiddleware($strategy));
$guzzle = new Client(['handler' => $stack]);

With this example:

  • All requests to example.org will be handled by PublicCacheStrategy
  • All requests to twitter.com will be handled by PrivateCacheStrategy
  • All other requests won't be cached.

Drupal

See Guzzle Cache module.

Links that talk about the project

Comments
  • Laravel Cache::store('file')

    Laravel Cache::store('file')

    Does the following code work?

    $stack->push(
        new CacheMiddleware(
            new PrivateCacheStrategy(
                new LaravelCacheStorage(
                    Cache::store('file')
                )
            )
        ),
        'cache'
    );
    

    Where is the cache saved? Normally in storage/framework/cache but i don't find the cache file.

    opened by J-Yen 27
  • Laravel cache duration changes

    Laravel cache duration changes

    https://github.com/Kevinrob/guzzle-cache-middleware/blob/master/src/Storage/LaravelCacheStorage.php#L47

    https://laravel-news.com/cache-ttl-change-coming-to-laravel-5-8

    enhancement 
    opened by Quezler 16
  • igbinary_serialize(): Cannot serialize resource(stream)

    igbinary_serialize(): Cannot serialize resource(stream)

    Hello

    I'm getting this error:

    A PHP Error was encountered
    Severity: 8192
    
    Message: igbinary_serialize(): Cannot serialize resource(stream) and resources may be converted to objects that cannot be serialized in future php releases. Serializing the value as null instead
    

    When using v3.4.1 on PHP v8.0.10 (with igbinary v3.2.6) and Guzzle v7.3.0.

    This message appears to stem from symfony/cache (v5.3.7) and the memcached component. I'm not getting that message with PHP 7.4 (igbinary 3.1.2) and I think that is because of this change in igbinary:

    https://pecl.php.net/package-info.php?package=igbinary&version=3.2.0

    • Emit a deprecation notice when serializing resources.

    I have been using the Doctrine cache successfully, but because that is now deprecated I've been forced to look at alternative implementations.

    opened by jamieburchell 13
  • [Question]: Force cache

    [Question]: Force cache

    Is there a way with the current implementation to force the client to cache the response regardless the headers coming from the response? I would like to ask the response coming from an API but it is so badly implemented that it doesn't send back proper headers. What would be the best way to go about it?

    opened by debo 8
  • Don't store keys without an expiry

    Don't store keys without an expiry

    We see weird behaviour in our redis cluster when we have many keys without an expiry.

    I don't believe the current behaviour is beneficial because a revalidation will only happen if there is a swr value and it's in the future- a cached response that exceeds the swr won't revalidate.

    opened by rhysemmerson 7
  • Also put entries into the cache that allow stale-while-revalidate

    Also put entries into the cache that allow stale-while-revalidate

    The stale-while-revalidate timeout is not taken into consideration when computing the TTL for the cache entry. The result is that the underlying cache layer will discard cache entries once they become stale.

    This effectively prevents stale-while-revalidate behavior, since the cached entry is no longer available when the stale period begins (and revalidation could happen in the background).

    opened by mpdude 6
  • Make greedy cache TTL dynamic

    Make greedy cache TTL dynamic

    When using the greedy cache we must set the TTL in the constructor. It would be a good idea to make it dynamic (maybe using a custom header in the request ?).

    enhancement 
    opened by Neirda24 6
  • Different store to cache strategies

    Different store to cache strategies

    I noticed that every call the cache being updated. Maybe it make sense to introduce different strategies of storing cache? For example, we use Etag as a cache mechanism so it's really useless to update cache everytime server responses with 304

    enhancement 
    opened by vicmosin 6
  • Improve Psr6CacheStorage (fixes #48)

    Improve Psr6CacheStorage (fixes #48)

    This fixes all issues mentioned in #48.

    • Double serialization
    • Useless read from the cache
    • Missing expiration time

    The last 2 points gave me a hard time, because I could not implement the solution I suggested in #48, as it's explicitly forbidden by the spec:

    Calling Libraries MUST NOT instantiate Item objects themselves. They may only be requested from a Pool object via the getItem() method. Calling Libraries SHOULD NOT assume that an Item created by one Implementing Library is compatible with a Pool from another Implementing Library.

    And even if I could provide a custom instance of CacheItem, the interface is missing getters for the expiration time (this is explained by the fact that cache items are not interchangeable), so setting the TTL would have been useless as it could not be read by the pool.

    After testing with a mock implementation, I could verify that fetch() is always called before save(), so my solution was to transiently store the CacheItem retrieved by fetch(), and use it in save().

    If for any reason fetch() is not called before save() for a given key, save() will revert to calling getItem() as before.

    opened by BenMorel 6
  • Keep headers when revalidate cache entry (RFC 7234 - section 4.3.4)

    Keep headers when revalidate cache entry (RFC 7234 - section 4.3.4)

    I'm using your middleware to cache the results from the GitHub API. Like many others, GitHub sends its pagination URLs in the Link header, and currently this header gets lost on revalidation.

    With this PR, the Link header information is kept in the cache and subsequent HTTP requests won't be lost. Let me know, if you think that there are other infos to be kept also :)

    Regards, Pascal

    opened by ppaulis 6
  • Error and TTL

    Error and TTL

    I get "PHP Fatal error: Class 'CacheMiddleware' not found" trying to use your example filecache (which also is missing a ")" at the end) and how do you specify the TTL on the cache?

    opened by Zyles 6
  • Always return stale response if exist on promise rejected callback

    Always return stale response if exist on promise rejected callback

    The code check if the rejection is a TransferException before looking for a stale response, but the next handler can reject with anything (e.g. Ganesha reject with its own exception when the circuit is open) and we loose the stale-on-error that we expect.

    The proposition here is to just always return stale response if exist on rejected callback. I could also propose some sort of service to inject to decide if stale response should be looked for if that is more acceptable.

    What do you think?

    Thanks.

    opened by e-zannelli 0
  • Not compatible with gmponos/guzzle_logger

    Not compatible with gmponos/guzzle_logger

    Since CacheEntry uses PumpStream for the response, attempting to use this middleware with gmponos/guzzle-log-middleware results in the following error:

    StringHandler can not log request/response because the body is not seekable/readable.

    opened by compwright 1
  • Storing PHP serialized data in caches MUST be avoided

    Storing PHP serialized data in caches MUST be avoided

    I was bitten by this several times already.

    You have a running application in production heavily relying on cached data.

    You upgrade a PHP dependency like going from 3.5.0 to 4.0.1, everything is working as expected during tests. You confidently push code to production.

    Everything breaks in production, and you have a hard time figuring why. You revert the changes and the issue does not stop until you somehow manage to either clear all caches (if your infrastructure can support it) or wait for the cache TTL.

    What happened: stored data is incompatible between the 2 versions. Some properties have changed in CacheEntry https://github.com/Kevinrob/guzzle-cache-middleware/compare/v3.5.0...v4.0.1#diff-cc5ac6f8871a586dc5f99a175c4e7d9a9f9093c11638546b5b7d176daa28a271

    Already cached data contains a messageBody property which is then unused by new code. PHP unserialization does not tell you anything but created objects are in invalid state.

    For this reason, I encourage to avoid storing anything other than string in caches and handle serialization elsewhere.

    At the moment, I have a custom PSR16 cache implementation to avoid the issue. I think having something handling serialization of CacheEntry as raw_string or at least, normalized data (stdClass, array or scalar values) which is immune to this issue

    opened by bburnichon 2
  • Fix cache entry with key with vary headers isn't deleted

    Fix cache entry with key with vary headers isn't deleted

    Two cache entries are created when the response has vary headers. But when you call delete, only the entry without the vary headers in the cache key is deleted. When you do a new call directly after deletion everything is ok. The cache entry without vary info is created again and returned. But if you use the Laravel backend, the cache entry with vary info won't be updated. (a If you repeat the call, the old cache entry with the vary info will be returned and not de fresh cache entry.

    To fix this problem, we now delete both entries in the delete method.

    opened by janalwin 0
  • CacheEntry::__sleep mutates response body to PumpStream which is read once

    CacheEntry::__sleep mutates response body to PumpStream which is read once

    Hello

    CacheEntry::__sleep mutates response body to a PumpStream which is read once; if you try to re-read the response body you'll get an empty string.

    I guess the current implementation assumes a CacheEntry is never used after being serialized, so PumpStreamd response is never exposed outside the middleware, but you can actually end-up with a mutated CacheEntry quite easily.

    For instance, if you use a Symfony/cache PSR cache storage implementation with multiple serializing adapters : On $this->cachePool->getItem($key); the ChainAdapter getting a hit on an outer adapter will save($item) on inner adapters, that will trigger __sleep, then return $item with the PumpStream implementation of bodies on request and response.

    opened by e-zannelli 0
Releases(v4.0.2)
Owner
Kevin Robatel
Kevin Robatel
Guzzle, an extensible PHP HTTP client

Guzzle, PHP HTTP client Guzzle is a PHP HTTP client that makes it easy to send HTTP requests and trivial to integrate with web services. Simple interf

Guzzle 22.3k Jan 2, 2023
A simple script i made that generate a valid http(s) proxy in json format with its geo-location info

Gev Proxy Generator GPG is a simple PHP script that generate a proxy using free services on the web, the proxy is HTTP(s) and it generate it in json f

gev 1 Nov 15, 2021
Simple handler system used to power clients and servers in PHP (this project is no longer used in Guzzle 6+)

RingPHP Provides a simple API and specification that abstracts away the details of HTTP into a single PHP function. RingPHP be used to power HTTP clie

Guzzle 846 Dec 6, 2022
Supercharge your app or SDK with a testing library specifically for Guzzle

Full Documentation at guzzler.dev Supercharge your app or SDK with a testing library specifically for Guzzle. Guzzler covers the process of setting up

null 275 Oct 30, 2022
Plug & Play [CURL + Composer Optional], Proxy as a Service, Multi-tenant, Multi-Threaded, with Cache & Article Spinner

?? .yxorP The SAAS(y), Multitenancy & Augmenting Web Proxy Guzzler is a 100% SAAS(y) plug-and-play (PHP CURL+Composer are Optional) solution that leverages SAAS architecture to provide multi-tenancy, multiple threads, caching, and an article spinner service.

4D/ҵ.com Dashboards 12 Nov 17, 2022
A simple PHP Toolkit to parallel generate combinations, save and use the generated terms to brute force attack via the http protocol.

Brutal A simple PHP Toolkit to parallel generate combinations, save and use the generated terms to apply brute force attack via the http protocol. Bru

Jean Carlo de Souza 4 Jul 28, 2021
Zenscrape package is a simple PHP HTTP client-provider that makes it easy to parsing site-pages

Zenscrape package is a simple PHP HTTP client-provider that makes it easy to parsing site-pages

Andrei 3 Jan 17, 2022
A simple yet powerful HTTP metadata and assets provider for NFT collections using Symfony

Safe NFT Metadata Provider A simple yet powerful HTTP metadata and assets provider for NFT collections using Symfony.

HashLips Lab 66 Oct 7, 2022
Simple HTTP cURL client for PHP 7.1+ based on PSR-18

Simple HTTP cURL client for PHP 7.1+ based on PSR-18 Installation composer require sunrise/http-client-curl QuickStart composer require sunrise/http-f

Sunrise // PHP 15 Sep 5, 2022
A simple OOP wrapper to work with HTTP headers in PHP

Headers This package is to allow you to create HTTP Headers in PHP, in a simple and reliable way. Installation composer require http-php/headers Usage

null 5 Aug 9, 2022
Requests for PHP is a humble HTTP request library. It simplifies how you interact with other sites and takes away all your worries.

Requests for PHP Requests is a HTTP library written in PHP, for human beings. It is roughly based on the API from the excellent Requests Python librar

null 3.5k Dec 31, 2022
A Chainable, REST Friendly, PHP HTTP Client. A sane alternative to cURL.

Httpful Httpful is a simple Http Client library for PHP 7.2+. There is an emphasis of readability, simplicity, and flexibility – basically provide the

Nate Good 1.7k Dec 21, 2022
PHP's lightweight HTTP client

Buzz - Scripted HTTP browser Buzz is a lightweight (<1000 lines of code) PHP 7.1 library for issuing HTTP requests. The library includes three clients

Kris Wallsmith 1.9k Jan 4, 2023
HTTPlug, the HTTP client abstraction for PHP

HTTPlug HTTPlug, the HTTP client abstraction for PHP. Intro HTTP client standard built on PSR-7 HTTP messages. The HTTPlug client interface is compati

The PHP HTTP group 2.4k Dec 30, 2022
PSR-7 HTTP Message implementation

zend-diactoros Repository abandoned 2019-12-31 This repository has moved to laminas/laminas-diactoros. Master: Develop: Diactoros (pronunciation: /dɪʌ

Zend Framework 1.6k Dec 9, 2022
Record your test suite's HTTP interactions and replay them during future test runs for fast, deterministic, accurate tests.

This is a port of the VCR Ruby library to PHP. Record your test suite's HTTP interactions and replay them during future test runs for fast, determinis

php-vcr 1.1k Dec 23, 2022
Requests for PHP is a humble HTTP request library. It simplifies how you interact with other sites and takes away all your worries.

Requests for PHP Requests is a HTTP library written in PHP, for human beings. It is roughly based on the API from the excellent Requests Python librar

null 3.5k Dec 31, 2022
The HttpClient component provides powerful methods to fetch HTTP resources synchronously or asynchronously.

HttpClient component The HttpClient component provides powerful methods to fetch HTTP resources synchronously or asynchronously. Resources Documentati

Symfony 1.7k Jan 6, 2023
PSR HTTP Message implementations

laminas-diactoros Diactoros (pronunciation: /dɪʌktɒrɒs/): an epithet for Hermes, meaning literally, "the messenger." This package supercedes and repla

Laminas Project 343 Dec 25, 2022