Full Code of php-http/cache-plugin for AI

2.x c7fd57abaf6d cached
30 files
66.7 KB
17.1k tokens
57 symbols
1 requests
Download .txt
Repository: php-http/cache-plugin
Branch: 2.x
Commit: c7fd57abaf6d
Files: 30
Total size: 66.7 KB

Directory structure:
gitextract_nm_wkhzu/

├── .editorconfig
├── .gitattributes
├── .github/
│   ├── CONTRIBUTING.md
│   ├── ISSUE_TEMPLATE.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── workflows/
│       ├── .editorconfig
│       ├── static.yml
│       └── tests.yml
├── .gitignore
├── .php_cs
├── .scrutinizer.yml
├── .styleci.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── composer.json
├── phpspec.ci.yml
├── phpspec.yml.dist
├── phpstan.neon.dist
├── phpunit.xml.dist
├── src/
│   ├── Cache/
│   │   ├── Generator/
│   │   │   ├── CacheKeyGenerator.php
│   │   │   ├── HeaderCacheKeyGenerator.php
│   │   │   └── SimpleGenerator.php
│   │   └── Listener/
│   │       ├── AddHeaderCacheListener.php
│   │       └── CacheListener.php
│   ├── CachePlugin.php
│   └── Exception/
│       └── RewindStreamException.php
└── tests/
    └── Cache/
        ├── CachePluginTest.php
        └── Generator/
            ├── HeaderCacheKeyGeneratorTest.php
            └── SimpleGeneratorTest.php

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true


================================================
FILE: .gitattributes
================================================
.editorconfig     export-ignore
.gitattributes    export-ignore
/.github/         export-ignore
.gitignore        export-ignore
/.php_cs          export-ignore
/.scrutinizer.yml export-ignore
/.styleci.yml     export-ignore
/behat.yml.dist   export-ignore
/features/        export-ignore
/phpspec.ci.yml   export-ignore
/phpspec.yml.dist export-ignore
/spec/            export-ignore


================================================
FILE: .github/CONTRIBUTING.md
================================================
# Contributing

Please see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html).


================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
| Q            | A
| ------------ | ---
| Bug?         | no|yes
| New Feature? | no|yes
| Version      | Specific version or SHA of a commit


#### Actual Behavior

What is the actual behavior?


#### Expected Behavior

What is the behavior you expect?


#### Steps to Reproduce

What are the steps to reproduce this bug? Please add code examples,
screenshots or links to GitHub repositories that reproduce the problem.


#### Possible Solutions

If you have already ideas how to solve the issue, add them here.
(remove this section if not needed)


================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
| Q               | A
| --------------- | ---
| Bug fix?        | no|yes
| New feature?    | no|yes
| BC breaks?      | no|yes
| Deprecations?   | no|yes
| Related tickets | fixes #X, partially #Y, mentioned in #Z
| Documentation   | if this is a new feature, link to pull request in https://github.com/php-http/documentation that adds relevant documentation
| License         | MIT


#### What's in this PR?

Explain what the changes in this PR do.


#### Why?

Which problem does the PR fix? (remove this section if you linked an issue above)


#### Example Usage

``` php
// If you added new features, show examples of how to use them here
// (remove this section if not a new feature)

$foo = new Foo();

// Now we can do
$foo->doSomething();
```


#### Checklist

- [ ] Updated CHANGELOG.md to describe BC breaks / deprecations | new feature | bugfix
- [ ] Documentation pull request created (if not simply a bugfix)


#### To Do

- [ ] If the PR is not complete but you want to discuss the approach, list what remains to be done here


================================================
FILE: .github/workflows/.editorconfig
================================================
[*.yml]
indent_size = 2


================================================
FILE: .github/workflows/static.yml
================================================
name: Static analysis

on:
  push:
    branches:
      - '*.x'
  pull_request:

jobs:
  phpstan:
    name: PHPStan
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: PHPStan
        uses: docker://oskarstark/phpstan-ga
        with:
          args: analyze --no-progress


================================================
FILE: .github/workflows/tests.yml
================================================
name: tests

on:
  push:
    branches:
      - '*.x'
  pull_request:

jobs:
  latest:
    name: PHP ${{ matrix.php }} Latest
    runs-on: ubuntu-latest
    strategy:
      matrix:
        php: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          tools: composer:v2
          coverage: none

      - name: Install PHP dependencies
        run: composer update --prefer-dist --no-interaction --no-progress

      - name: Execute tests
        run: composer test

  lowest:
    name: PHP ${{ matrix.php }} Lowest
    runs-on: ubuntu-latest
    strategy:
      matrix:
        php: ['7.1', '7.4', '8.0']

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          tools: composer:v2
          coverage: none

      - name: Install dependencies
        run: |
          composer require "sebastian/comparator:^3.0.2" --no-interaction --no-update
          composer update --prefer-dist --prefer-stable --prefer-lowest --no-interaction --no-progress

      - name: Execute tests
        run: composer test

  coverage:
    name: Code Coverage
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: 7.4
          tools: composer:v2
          coverage: xdebug

      - name: Install dependencies
        run: |
          composer require "friends-of-phpspec/phpspec-code-coverage" --no-interaction --no-update
          composer update --prefer-dist --no-interaction --no-progress

      - name: Execute tests
        run: composer test-ci

      - name: Upload coverage
        run: |
          wget https://scrutinizer-ci.com/ocular.phar
          php ocular.phar code-coverage:upload --format=php-clover build/coverage.xml


================================================
FILE: .gitignore
================================================
/behat.yml
/build/
/composer.lock
/phpspec.yml
/phpunit.xml
/vendor/


================================================
FILE: .php_cs
================================================
<?php

/*
 * In order to make it work, fabpot/php-cs-fixer and sllh/php-cs-fixer-styleci-bridge must be installed globally
 * with composer.
 *
 * @link https://github.com/Soullivaneuh/php-cs-fixer-styleci-bridge
 * @link https://github.com/FriendsOfPHP/PHP-CS-Fixer
 */

use SLLH\StyleCIBridge\ConfigBridge;

return ConfigBridge::create();


================================================
FILE: .scrutinizer.yml
================================================
filter:
    paths: [src/*]
checks:
    php:
        code_rating: true
        duplication: true
tools:
    external_code_coverage: true


================================================
FILE: .styleci.yml
================================================
preset: symfony

finder:
    exclude:
        - "spec"
    path:
        - "src"
        - "tests"


================================================
FILE: CHANGELOG.md
================================================
# Change Log

# Version 2

## 2.0.2 - 2025-12-01

- Support Symfony 8.
- Test with PHP 8.5.

## 2.0.1 - 2024-10-02

- Test with PHP 8.3 and 8.4.

## 2.0.0 - 2024-02-19

### Changed

- Drop support of deprecated PHP-HTTP `StreamFactory`, only PSR-17 `StreamFactoryInterface` is now supported.

# Version 1

## 1.8.1 - 2023-11-21

- Allow installation with Symfony 7.

## 1.8.0 - 2023-04-28

- Avoid PHP warning about serializing resources when serializing the response by detaching the stream.

## 1.7.6 - 2023-04-28

- Test with PHP 8.1 and 8.2
- Made phpspec tests compatible with PSR-7 2.0 strict typing
- Detect `null` and use 0 explicitly to calculate expiration

## 1.7.5 - 2022-01-18

- Allow installation with psr/cache 3.0 (1.0 and 2.0 are still allowed too)

## 1.7.4 - 2021-11-30

### Added

- Allow installation with Symfony 6

## 1.7.3 - 2021-11-03

### Changed

- Be more defensive about cache hits. A cache entry can technically contain `null`.

## 1.7.2 - 2021-04-14

### Added

- Allow installation with psr/cache 2.0 (1.0 still allowed too)

## 1.7.1 - 2020-07-13

### Added

- Support for PHP 8

## 1.7.0 - 2019-12-17

### Added

* Support for Symfony 5.
* Support for PSR-17 `StreamFactoryInterface`.
* Added `blacklisted_paths` option, which takes an array of `strings` (regular expressions) and allows to define paths, that shall not be cached in any case.

## 1.6.0 - 2019-01-23

### Added

* Support for HTTPlug 2 / PSR-18
* Added `cache_listeners` option, which takes an array of `CacheListener`s, who get notified and can optionally act on a Response based on a cache hit or miss event. An implementation, `AddHeaderCacheListener`, is provided which will add an `X-Cache` header to the response with this information.

## 1.5.0 - 2017-11-29

### Added

* Support for Symfony 4

### Changed

* Removed check if etag is a string. Etag can never be a string, it is always an array.

## 1.4.0 - 2017-04-05

### Added

- `CacheKeyGenerator` interface that allow you to configure how the PSR-6 cache key is created. There are two implementations
of this interface: `SimpleGenerator` (default) and `HeaderCacheKeyGenerator`.

### Fixed

- Issue where deprecation warning always was triggered. Not it is just triggered if `respect_cache_headers` is used.

## 1.3.0 - 2017-03-28

### Added

- New `methods` option which allows to configure the request methods which can be cached.
- New `respect_response_cache_directives` option to define specific cache directives to respect when handling responses.
- Introduced `CachePlugin::clientCache` and `CachePlugin::serverCache` factory methods to easily setup the plugin with
  the correct config settigns for each usecase.

### Changed

- The `no-cache` directive is now respected by the plugin and will not cache the response. If you need the previous behaviour, configure `respect_response_cache_directives`.
- We always rewind the stream after loading response from cache.

### Deprecated

- The `respect_cache_headers` option is deprecated and will be removed in 2.0. This option is replaced by the new `respect_response_cache_directives` option.
  If you had set `respect_cache_headers` to `false`, set the directives to `[]` to ignore all directives.


## 1.2.0 - 2016-08-16

### Changed

- The default value for `default_ttl` is changed from `null` to `0`.

### Fixed

- Issue when you use `respect_cache_headers=>false` in combination with `default_ttl=>null`.
- We allow `cache_lifetime` to be set to `null`.


## 1.1.0 - 2016-08-04

### Added

- Support for cache validation with ETag and Last-Modified headers. (Enabled automatically when the server sends the relevant headers.)
- `hash_algo` config option used for cache key generation (defaults to **sha1**).

### Changed

- Default hash algo used for cache generation (from **md5** to **sha1**).

### Fixed

- Cast max age header to integer in order to get valid expiration value.


## 1.0.0 - 2016-05-05

- Initial release


================================================
FILE: LICENSE
================================================
Copyright (c) 2015-2016 PHP HTTP Team <team@php-http.org>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: README.md
================================================
# Cache Plugin

[![Latest Version](https://img.shields.io/github/release/php-http/cache-plugin.svg?style=flat-square)](https://github.com/php-http/cache-plugin/releases)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
[![Build Status](https://github.com/php-http/cache-plugin/actions/workflows/tests.yml/badge.svg)](https://github.com/php-http/cache-plugin/actions/workflows/tests.yml)
[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-http/cache-plugin.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/cache-plugin)
[![Quality Score](https://img.shields.io/scrutinizer/g/php-http/cache-plugin.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/cache-plugin)
[![Total Downloads](https://img.shields.io/packagist/dt/php-http/cache-plugin.svg?style=flat-square)](https://packagist.org/packages/php-http/cache-plugin)

**PSR-6 Cache plugin for HTTPlug.**


## Install

Via Composer

``` bash
composer require php-http/cache-plugin
```


## Documentation

Please see the [official documentation](http://docs.php-http.org/en/latest/plugins/cache.html).


## Testing

``` bash
composer test
```


## Contributing

Please see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html).


## Security

If you discover any security related issues, please contact us at [security@php-http.org](mailto:security@php-http.org).


## License

The MIT License (MIT). Please see [License File](LICENSE) for more information.


================================================
FILE: composer.json
================================================
{
    "name": "php-http/cache-plugin",
    "description": "PSR-6 Cache plugin for HTTPlug",
    "license": "MIT",
    "keywords": ["cache", "http", "httplug", "plugin"],
    "homepage": "http://httplug.io",
    "authors": [
        {
            "name": "Márk Sági-Kazár",
            "email": "mark.sagikazar@gmail.com"
        }
    ],
    "require": {
        "php": "^7.1 || ^8.0",
        "psr/cache": "^1.0 || ^2.0 || ^3.0",
        "php-http/client-common": "^1.9 || ^2.0",
        "psr/http-factory-implementation": "^1.0",
        "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0"
    },
    "require-dev": {
        "nyholm/psr7": "^1.6.1",
        "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
    },
    "autoload": {
        "psr-4": {
            "Http\\Client\\Common\\Plugin\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Http\\Client\\Common\\Plugin\\Tests\\": "tests/"
        }
    },
    "scripts": {
        "test": "vendor/bin/phpunit",
        "test-ci": "vendor/bin/phpunit"
    }
}


================================================
FILE: phpspec.ci.yml
================================================
suites:
    cache_plugin_suite:
        namespace: Http\Client\Common\Plugin
        psr4_prefix: Http\Client\Common\Plugin
formatter.name: pretty
extensions:
    FriendsOfPhpSpec\PhpSpec\CodeCoverage\CodeCoverageExtension: ~
code_coverage:
    format: clover
    output: build/coverage.xml


================================================
FILE: phpspec.yml.dist
================================================
suites:
    cache_plugin_suite:
        namespace: Http\Client\Common\Plugin
        psr4_prefix: Http\Client\Common\Plugin
formatter.name: pretty


================================================
FILE: phpstan.neon.dist
================================================
parameters:
    level: 8
    paths:
        - src
    treatPhpDocTypesAsCertain: false


================================================
FILE: phpunit.xml.dist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
         beStrictAboutTestsThatDoNotTestAnything="true"
         beStrictAboutTodoAnnotatedTests="true">
    <testsuites>
        <testsuite name="Cache Plugin Test Suite">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>


================================================
FILE: src/Cache/Generator/CacheKeyGenerator.php
================================================
<?php

namespace Http\Client\Common\Plugin\Cache\Generator;

use Psr\Http\Message\RequestInterface;

/**
 * An interface for generate a cache key.
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
interface CacheKeyGenerator
{
    /**
     * Generate a cache key from a Request.
     *
     * @return string
     */
    public function generate(RequestInterface $request);
}


================================================
FILE: src/Cache/Generator/HeaderCacheKeyGenerator.php
================================================
<?php

namespace Http\Client\Common\Plugin\Cache\Generator;

use Psr\Http\Message\RequestInterface;

/**
 * Generate a cache key by using HTTP headers.
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
class HeaderCacheKeyGenerator implements CacheKeyGenerator
{
    /**
     * The header names we should take into account when creating the cache key.
     *
     * @var string[]
     */
    private $headerNames;

    /**
     * @param string[] $headerNames
     */
    public function __construct(array $headerNames)
    {
        $this->headerNames = $headerNames;
    }

    public function generate(RequestInterface $request)
    {
        $concatenatedHeaders = [];
        foreach ($this->headerNames as $headerName) {
            $concatenatedHeaders[] = sprintf(' %s:"%s"', $headerName, $request->getHeaderLine($headerName));
        }

        return $request->getMethod().' '.$request->getUri().implode('', $concatenatedHeaders).' '.$request->getBody();
    }
}


================================================
FILE: src/Cache/Generator/SimpleGenerator.php
================================================
<?php

namespace Http\Client\Common\Plugin\Cache\Generator;

use Psr\Http\Message\RequestInterface;

/**
 * Generate a cache key from the request method, URI and body.
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
class SimpleGenerator implements CacheKeyGenerator
{
    public function generate(RequestInterface $request)
    {
        $body = (string) $request->getBody();
        if (!empty($body)) {
            $body = ' '.$body;
        }

        return $request->getMethod().' '.$request->getUri().$body;
    }
}


================================================
FILE: src/Cache/Listener/AddHeaderCacheListener.php
================================================
<?php

namespace Http\Client\Common\Plugin\Cache\Listener;

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Cache\CacheItemInterface;

/**
 * Adds a header indicating if the response came from cache.
 *
 * @author Iain Connor <iainconnor@gmail.com>
 */
class AddHeaderCacheListener implements CacheListener
{
    /** @var string */
    private $headerName;

    /**
     * @param string $headerName
     */
    public function __construct($headerName = 'X-Cache')
    {
        $this->headerName = $headerName;
    }

    /**
     * Called before the cache plugin returns the response, with information on whether that response came from cache.
     *
     * @param bool                    $fromCache Whether the `$response` was from the cache or not.
     *                                           Note that checking `$cacheItem->isHit()` is not sufficent to determine this.
     * @param CacheItemInterface|null $cacheItem
     *
     * @return ResponseInterface
     */
    public function onCacheResponse(RequestInterface $request, ResponseInterface $response, $fromCache, $cacheItem)
    {
        return $response->withHeader($this->headerName, $fromCache ? 'HIT' : 'MISS');
    }
}


================================================
FILE: src/Cache/Listener/CacheListener.php
================================================
<?php

namespace Http\Client\Common\Plugin\Cache\Listener;

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Cache\CacheItemInterface;

/**
 * Called by the cache plugin with information on the cache status.
 * Provides an opportunity to update the response based on whether the cache was a hit or a miss, or
 * other cache-meta-data.
 *
 * @author Iain Connor <iainconnor@gmail.com>
 */
interface CacheListener
{
    /**
     * Called before the cache plugin returns the response, with information on whether that response came from cache.
     *
     * @param bool                    $fromCache Whether the `$response` was from the cache or not.
     *                                           Note that checking `$cacheItem->isHit()` is not sufficent to determine this.
     * @param CacheItemInterface|null $cacheItem
     *
     * @return ResponseInterface
     */
    public function onCacheResponse(RequestInterface $request, ResponseInterface $response, $fromCache, $cacheItem);
}


================================================
FILE: src/CachePlugin.php
================================================
<?php

namespace Http\Client\Common\Plugin;

use Http\Client\Common\Plugin;
use Http\Client\Common\Plugin\Exception\RewindStreamException;
use Http\Client\Common\Plugin\Cache\Generator\CacheKeyGenerator;
use Http\Client\Common\Plugin\Cache\Generator\SimpleGenerator;
use Http\Promise\FulfilledPromise;
use Http\Promise\Promise;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Allow for caching a response with a PSR-6 compatible caching engine.
 *
 * It can follow the RFC-7234 caching specification or use a fixed cache lifetime.
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
final class CachePlugin implements Plugin
{
    use VersionBridgePlugin;

    /**
     * @var CacheItemPoolInterface
     */
    private $pool;

    /**
     * @var StreamFactoryInterface
     */
    private $streamFactory;

    /**
     * @var mixed[]
     */
    private $config;

    /**
     * Cache directives indicating if a response can not be cached.
     *
     * @var string[]
     */
    private $noCacheFlags = ['no-cache', 'private', 'no-store'];

    /**
     * @param mixed[] $config
     *
     *     bool respect_cache_headers: Whether to look at the cache directives or ignore them
     *     int default_ttl: (seconds) If we do not respect cache headers or can't calculate a good ttl, use this value
     *     string hash_algo: The hashing algorithm to use when generating cache keys
     *     int|null cache_lifetime: (seconds) To support serving a previous stale response when the server answers 304
     *              we have to store the cache for a longer time than the server originally says it is valid for.
     *              We store a cache item for $cache_lifetime + max age of the response.
     *     string[] methods: list of request methods which can be cached
     *     string[] blacklisted_paths: list of regex for URLs explicitly not to be cached
     *     string[] respect_response_cache_directives: list of cache directives this plugin will respect while caching responses
     *     CacheKeyGenerator cache_key_generator: an object to generate the cache key. Defaults to a new instance of SimpleGenerator
     *     CacheListener[] cache_listeners: an array of objects to act on the response based on the results of the cache check.
     *              Defaults to an empty array
     * }
     */
    public function __construct(CacheItemPoolInterface $pool, StreamFactoryInterface $streamFactory, array $config = [])
    {
        $this->pool = $pool;
        $this->streamFactory = $streamFactory;

        if (\array_key_exists('respect_cache_headers', $config) && \array_key_exists('respect_response_cache_directives', $config)) {
            throw new \InvalidArgumentException('You can\'t provide config option "respect_cache_headers" and "respect_response_cache_directives". Use "respect_response_cache_directives" instead.');
        }

        $optionsResolver = new OptionsResolver();
        $this->configureOptions($optionsResolver);
        $this->config = $optionsResolver->resolve($config);

        if (null === $this->config['cache_key_generator']) {
            $this->config['cache_key_generator'] = new SimpleGenerator();
        }
    }

    /**
     * This method will setup the cachePlugin in client cache mode. When using the client cache mode the plugin will
     * cache responses with `private` cache directive.
     *
     * @param mixed[] $config For all possible config options see the constructor docs
     *
     * @return CachePlugin
     */
    public static function clientCache(CacheItemPoolInterface $pool, StreamFactoryInterface $streamFactory, array $config = [])
    {
        // Allow caching of private requests
        if (\array_key_exists('respect_response_cache_directives', $config)) {
            $config['respect_response_cache_directives'][] = 'no-cache';
            $config['respect_response_cache_directives'][] = 'max-age';
            $config['respect_response_cache_directives'] = array_unique($config['respect_response_cache_directives']);
        } else {
            $config['respect_response_cache_directives'] = ['no-cache', 'max-age'];
        }

        return new self($pool, $streamFactory, $config);
    }

    /**
     * This method will setup the cachePlugin in server cache mode. This is the default caching behavior it refuses to
     * cache responses with the `private`or `no-cache` directives.
     *
     * @param mixed[] $config For all possible config options see the constructor docs
     *
     * @return CachePlugin
     */
    public static function serverCache(CacheItemPoolInterface $pool, StreamFactoryInterface $streamFactory, array $config = [])
    {
        return new self($pool, $streamFactory, $config);
    }

    /**
     * {@inheritdoc}
     *
     * @return Promise Resolves a PSR-7 Response or fails with an Http\Client\Exception (The same as HttpAsyncClient)
     */
    protected function doHandleRequest(RequestInterface $request, callable $next, callable $first)
    {
        $method = strtoupper($request->getMethod());
        // if the request not is cachable, move to $next
        if (!in_array($method, $this->config['methods'])) {
            return $next($request)->then(function (ResponseInterface $response) use ($request) {
                $response = $this->handleCacheListeners($request, $response, false, null);

                return $response;
            });
        }

        // If we can cache the request
        $key = $this->createCacheKey($request);
        $cacheItem = $this->pool->getItem($key);

        if ($cacheItem->isHit()) {
            $data = $cacheItem->get();
            if (is_array($data)) {
                // The array_key_exists() is to be removed in 2.0.
                if (array_key_exists('expiresAt', $data) && (null === $data['expiresAt'] || time() < $data['expiresAt'])) {
                    // This item is still valid according to previous cache headers
                    $response = $this->createResponseFromCacheItem($cacheItem);
                    $response = $this->handleCacheListeners($request, $response, true, $cacheItem);

                    return new FulfilledPromise($response);
                }

                // Add headers to ask the server if this cache is still valid
                if ($modifiedSinceValue = $this->getModifiedSinceHeaderValue($cacheItem)) {
                    $request = $request->withHeader('If-Modified-Since', $modifiedSinceValue);
                }

                if ($etag = $this->getETag($cacheItem)) {
                    $request = $request->withHeader('If-None-Match', $etag);
                }
            }
        }

        return $next($request)->then(function (ResponseInterface $response) use ($request, $cacheItem) {
            if (304 === $response->getStatusCode()) {
                if (!$cacheItem->isHit()) {
                    /*
                     * We do not have the item in cache. This plugin did not add If-Modified-Since
                     * or If-None-Match headers. Return the response from server.
                     */
                    return $this->handleCacheListeners($request, $response, false, $cacheItem);
                }

                // The cached response we have is still valid
                $data = $cacheItem->get();
                $maxAge = $this->getMaxAge($response);
                $data['expiresAt'] = $this->calculateResponseExpiresAt($maxAge);
                $cacheItem->set($data)->expiresAfter($this->calculateCacheItemExpiresAfter($maxAge));
                $this->pool->save($cacheItem);

                return $this->handleCacheListeners($request, $this->createResponseFromCacheItem($cacheItem), true, $cacheItem);
            }

            if ($this->isCacheable($response) && $this->isCacheableRequest($request)) {
                /* The PSR-7 response body is a stream. We can't expect that the response implements Serializable and handles the body.
                 * Therefore we store the body separately and detach the stream to avoid attempting to serialize a resource.
                .* Our implementation still makes the assumption that the response object apart from the body can be serialized and deserialized.
                 */
                $bodyStream = $response->getBody();
                $body = $bodyStream->__toString();
                $bodyStream->detach();

                $maxAge = $this->getMaxAge($response);
                $cacheItem
                    ->expiresAfter($this->calculateCacheItemExpiresAfter($maxAge))
                    ->set([
                        'response' => $response,
                        'body' => $body,
                        'expiresAt' => $this->calculateResponseExpiresAt($maxAge),
                        'createdAt' => time(),
                        'etag' => $response->getHeader('ETag'),
                    ]);
                $this->pool->save($cacheItem);

                $bodyStream = $this->streamFactory->createStream($body);
                if ($bodyStream->isSeekable()) {
                    $bodyStream->rewind();
                }

                $response = $response->withBody($bodyStream);
            }

            return $this->handleCacheListeners($request, $response, false, $cacheItem);
        });
    }

    /**
     * Calculate the timestamp when this cache item should be dropped from the cache. The lowest value that can be
     * returned is $maxAge.
     *
     * @return int|null Unix system time passed to the PSR-6 cache
     */
    private function calculateCacheItemExpiresAfter(?int $maxAge): ?int
    {
        if (null === $this->config['cache_lifetime'] && null === $maxAge) {
            return null;
        }

        return ($this->config['cache_lifetime'] ?: 0) + ($maxAge ?: 0);
    }

    /**
     * Calculate the timestamp when a response expires. After that timestamp, we need to send a
     * If-Modified-Since / If-None-Match request to validate the response.
     *
     * @return int|null Unix system time. A null value means that the response expires when the cache item expires
     */
    private function calculateResponseExpiresAt(?int $maxAge): ?int
    {
        if (null === $maxAge) {
            return null;
        }

        return time() + $maxAge;
    }

    /**
     * Verify that we can cache this response.
     *
     * @return bool
     */
    protected function isCacheable(ResponseInterface $response)
    {
        if (!in_array($response->getStatusCode(), [200, 203, 300, 301, 302, 404, 410])) {
            return false;
        }

        $nocacheDirectives = array_intersect($this->config['respect_response_cache_directives'], $this->noCacheFlags);
        foreach ($nocacheDirectives as $nocacheDirective) {
            if ($this->getCacheControlDirective($response, $nocacheDirective)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Verify that we can cache this request.
     */
    private function isCacheableRequest(RequestInterface $request): bool
    {
        $uri = $request->getUri()->__toString();
        foreach ($this->config['blacklisted_paths'] as $regex) {
            if (1 === preg_match($regex, $uri)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Get the value of a parameter in the cache control header.
     *
     * @param string $name The field of Cache-Control to fetch
     *
     * @return bool|string The value of the directive, true if directive without value, false if directive not present
     */
    private function getCacheControlDirective(ResponseInterface $response, string $name)
    {
        $headers = $response->getHeader('Cache-Control');
        foreach ($headers as $header) {
            if (preg_match(sprintf('|%s=?([0-9]+)?|i', $name), $header, $matches)) {
                // return the value for $name if it exists
                if (isset($matches[1])) {
                    return $matches[1];
                }

                return true;
            }
        }

        return false;
    }

    private function createCacheKey(RequestInterface $request): string
    {
        $key = $this->config['cache_key_generator']->generate($request);

        return hash($this->config['hash_algo'], $key);
    }

    /**
     * Get a ttl in seconds.
     *
     * Returns null if we do not respect cache headers and got no defaultTtl.
     */
    private function getMaxAge(ResponseInterface $response): ?int
    {
        if (!in_array('max-age', $this->config['respect_response_cache_directives'], true)) {
            return $this->config['default_ttl'];
        }

        // check for max age in the Cache-Control header
        $maxAge = $this->getCacheControlDirective($response, 'max-age');
        if (!is_bool($maxAge)) {
            $ageHeaders = $response->getHeader('Age');
            foreach ($ageHeaders as $age) {
                return ((int) $maxAge) - ((int) $age);
            }

            return (int) $maxAge;
        }

        // check for ttl in the Expires header
        $headers = $response->getHeader('Expires');
        foreach ($headers as $header) {
            return (new \DateTime($header))->getTimestamp() - (new \DateTime())->getTimestamp();
        }

        return $this->config['default_ttl'];
    }

    /**
     * Configure an options resolver.
     */
    private function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'cache_lifetime' => 86400 * 30, // 30 days
            'default_ttl' => 0,
            // Deprecated as of v1.3, to be removed in v2.0. Use respect_response_cache_directives instead
            'respect_cache_headers' => null,
            'hash_algo' => 'sha1',
            'methods' => ['GET', 'HEAD'],
            'respect_response_cache_directives' => ['no-cache', 'private', 'max-age', 'no-store'],
            'cache_key_generator' => null,
            'cache_listeners' => [],
            'blacklisted_paths' => [],
        ]);

        $resolver->setAllowedTypes('cache_lifetime', ['int', 'null']);
        $resolver->setAllowedTypes('default_ttl', ['int', 'null']);
        $resolver->setAllowedTypes('respect_cache_headers', ['bool', 'null']);
        $resolver->setAllowedTypes('methods', 'array');
        $resolver->setAllowedTypes('cache_key_generator', ['null', CacheKeyGenerator::class]);
        $resolver->setAllowedTypes('blacklisted_paths', 'array');
        $resolver->setAllowedValues('hash_algo', hash_algos());
        $resolver->setAllowedValues('methods', function ($value) {
            /* RFC7230 sections 3.1.1 and 3.2.6 except limited to uppercase characters. */
            $matches = preg_grep('/[^A-Z0-9!#$%&\'*+\-.^_`|~]/', $value);

            return empty($matches);
        });
        $resolver->setAllowedTypes('cache_listeners', ['array']);

        $resolver->setNormalizer('respect_cache_headers', function (Options $options, $value) {
            if (null !== $value) {
                @trigger_error('The option "respect_cache_headers" is deprecated since version 1.3 and will be removed in 2.0. Use "respect_response_cache_directives" instead.', E_USER_DEPRECATED);
            }

            return null === $value ? true : $value;
        });

        $resolver->setNormalizer('respect_response_cache_directives', function (Options $options, $value) {
            if (false === $options['respect_cache_headers']) {
                return [];
            }

            return $value;
        });
    }

    private function createResponseFromCacheItem(CacheItemInterface $cacheItem): ResponseInterface
    {
        $data = $cacheItem->get();

        /** @var ResponseInterface $response */
        $response = $data['response'];
        $stream = $this->streamFactory->createStream($data['body']);

        try {
            $stream->rewind();
        } catch (\Exception $e) {
            throw new RewindStreamException('Cannot rewind stream.', 0, $e);
        }

        return $response->withBody($stream);
    }

    /**
     * Get the value for the "If-Modified-Since" header.
     */
    private function getModifiedSinceHeaderValue(CacheItemInterface $cacheItem): ?string
    {
        $data = $cacheItem->get();
        // The isset() is to be removed in 2.0.
        if (!isset($data['createdAt'])) {
            return null;
        }

        $modified = new \DateTime('@'.$data['createdAt']);
        $modified->setTimezone(new \DateTimeZone('GMT'));

        return sprintf('%s GMT', $modified->format('l, d-M-y H:i:s'));
    }

    /**
     * Get the ETag from the cached response.
     */
    private function getETag(CacheItemInterface $cacheItem): ?string
    {
        $data = $cacheItem->get();
        // The isset() is to be removed in 2.0.
        if (!isset($data['etag'])) {
            return null;
        }

        foreach ($data['etag'] as $etag) {
            if (!empty($etag)) {
                return $etag;
            }
        }

        return null;
    }

    /**
     * Call the registered cache listeners.
     */
    private function handleCacheListeners(RequestInterface $request, ResponseInterface $response, bool $cacheHit, ?CacheItemInterface $cacheItem): ResponseInterface
    {
        foreach ($this->config['cache_listeners'] as $cacheListener) {
            $response = $cacheListener->onCacheResponse($request, $response, $cacheHit, $cacheItem);
        }

        return $response;
    }
}


================================================
FILE: src/Exception/RewindStreamException.php
================================================
<?php

namespace Http\Client\Common\Plugin\Exception;

use Http\Client\Exception;

/**
 * @author Théo FIDRY <theo.fidry@gmail.com>
 */
class RewindStreamException extends \RuntimeException implements Exception
{
}


================================================
FILE: tests/Cache/CachePluginTest.php
================================================
<?php

namespace Http\Client\Common\Plugin\Tests\Cache;

use Http\Client\Common\Plugin;
use Http\Client\Common\Plugin\Cache\Generator\SimpleGenerator;
use Http\Client\Common\Plugin\CachePlugin;
use Http\Promise\FulfilledPromise;
use PHPUnit\Framework\Constraint\Callback;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;

class CachePluginTest extends TestCase
{
    private function createPlugin(CacheItemPoolInterface $pool, StreamFactoryInterface $streamFactory, array $config = []): CachePlugin
    {
        $defaults = [
            'default_ttl' => 60,
            'cache_lifetime' => 1000,
        ];

        return new CachePlugin($pool, $streamFactory, array_merge($defaults, $config));
    }

    private function cacheItemConstraint(array $expected): Callback
    {
        return $this->callback(function ($actual) use ($expected) {
            if (!is_array($actual)) {
                return false;
            }

            foreach ($expected as $key => $value) {
                if (!array_key_exists($key, $actual)) {
                    return false;
                }

                if (in_array($key, ['expiresAt', 'createdAt'], true)) {
                    continue;
                }

                if ($actual[$key] !== $value) {
                    return false;
                }
            }

            return true;
        });
    }

    private function createFulfilledNext(ResponseInterface $response): callable
    {
        return function (RequestInterface $request) use ($response) {
            return new FulfilledPromise($response);
        };
    }

    public function testInterface(): void
    {
        $plugin = $this->createPlugin(
            $this->createMock(CacheItemPoolInterface::class),
            $this->createMock(StreamFactoryInterface::class)
        );

        self::assertInstanceOf(Plugin::class, $plugin);
    }

    public function testCacheResponses(): void
    {
        $httpBody = 'body';

        $stream = $this->createMock(StreamInterface::class);
        $stream->method('__toString')->willReturn($httpBody);
        $stream->method('isSeekable')->willReturn(true);
        $stream->expects($this->once())->method('rewind');
        $stream->expects($this->once())->method('detach');

        $streamFactory = $this->createMock(StreamFactoryInterface::class);
        $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream);

        $uri = $this->createMock(UriInterface::class);
        $uri->method('__toString')->willReturn('https://example.com/');

        $request = $this->createMock(RequestInterface::class);
        $request->method('getMethod')->willReturn('GET');
        $request->method('getUri')->willReturn($uri);
        $request->method('getBody')->willReturn($stream);

        $response = $this->createMock(ResponseInterface::class);
        $response->method('getStatusCode')->willReturn(200);
        $response->method('getBody')->willReturn($stream);
        $response->method('getHeader')->willReturn([]);
        $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf();

        $item = $this->createMock(CacheItemInterface::class);
        $item->method('isHit')->willReturn(false);
        $item->expects($this->once())->method('expiresAfter')->with(1060)->willReturnSelf();
        $item->expects($this->once())->method('set')->with($this->cacheItemConstraint([
            'response' => $response,
            'body' => $httpBody,
            'expiresAt' => 0,
            'createdAt' => 0,
            'etag' => [],
        ]))->willReturnSelf();

        $pool = $this->createMock(CacheItemPoolInterface::class);
        $pool->expects($this->once())->method('getItem')->with($this->anything())->willReturn($item);
        $pool->expects($this->once())->method('save')->with($item);

        $plugin = $this->createPlugin($pool, $streamFactory);
        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {
        })->wait();

        self::assertSame($response, $result);
    }

    public function testDoNotStoreFailedResponses(): void
    {
        $item = $this->createMock(CacheItemInterface::class);
        $item->method('isHit')->willReturn(false);

        $pool = $this->createMock(CacheItemPoolInterface::class);
        $pool->expects($this->once())->method('getItem')->with($this->anything())->willReturn($item);
        $pool->expects($this->never())->method('save');

        $uri = $this->createMock(UriInterface::class);
        $uri->method('__toString')->willReturn('https://example.com/');

        $requestBody = $this->createMock(StreamInterface::class);
        $requestBody->method('__toString')->willReturn('body');

        $request = $this->createMock(RequestInterface::class);
        $request->method('getMethod')->willReturn('GET');
        $request->method('getUri')->willReturn($uri);
        $request->method('getBody')->willReturn($requestBody);

        $response = $this->createMock(ResponseInterface::class);
        $response->method('getStatusCode')->willReturn(400);

        $plugin = $this->createPlugin($pool, $this->createMock(StreamFactoryInterface::class));

        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {
        })->wait();

        self::assertSame($response, $result);
    }

    public function testDoNotStorePostRequestsByDefault(): void
    {
        $pool = $this->createMock(CacheItemPoolInterface::class);
        $pool->expects($this->never())->method('getItem');

        $request = $this->createMock(RequestInterface::class);
        $request->method('getMethod')->willReturn('POST');

        $response = $this->createMock(ResponseInterface::class);

        $plugin = $this->createPlugin($pool, $this->createMock(StreamFactoryInterface::class));

        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {
        })->wait();

        self::assertSame($response, $result);
    }

    public function testStorePostRequestsWhenAllowed(): void
    {
        $httpBody = 'hello=world';

        $stream = $this->createMock(StreamInterface::class);
        $stream->method('__toString')->willReturn($httpBody);
        $stream->method('isSeekable')->willReturn(true);
        $stream->expects($this->once())->method('rewind');
        $stream->expects($this->once())->method('detach');

        $streamFactory = $this->createMock(StreamFactoryInterface::class);
        $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream);

        $uri = $this->createMock(UriInterface::class);
        $uri->method('__toString')->willReturn('https://example.com/');

        $request = $this->createMock(RequestInterface::class);
        $request->method('getMethod')->willReturn('POST');
        $request->method('getUri')->willReturn($uri);
        $request->method('getBody')->willReturn($stream);

        $response = $this->createMock(ResponseInterface::class);
        $response->method('getStatusCode')->willReturn(200);
        $response->method('getBody')->willReturn($stream);
        $response->method('getHeader')->willReturn([]);
        $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf();

        $item = $this->createMock(CacheItemInterface::class);
        $item->method('isHit')->willReturn(false);
        $item->expects($this->once())->method('expiresAfter')->with(1060)->willReturnSelf();
        $item->expects($this->once())->method('set')->with($this->cacheItemConstraint([
            'response' => $response,
            'body' => $httpBody,
            'expiresAt' => 0,
            'createdAt' => 0,
            'etag' => [],
        ]))->willReturnSelf();

        $pool = $this->createMock(CacheItemPoolInterface::class);
        $pool->expects($this->once())->method('getItem')->willReturn($item);
        $pool->expects($this->once())->method('save')->with($item);

        $plugin = $this->createPlugin($pool, $streamFactory, [
            'methods' => ['GET', 'HEAD', 'POST'],
        ]);

        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {
        })->wait();

        self::assertSame($response, $result);
    }

    /**
     * @dataProvider invalidMethodProvider
     */
    public function testDoNotAllowInvalidRequestMethods(array $methods): void
    {
        $this->expectException(InvalidOptionsException::class);

        $this->createPlugin(
            $this->createMock(CacheItemPoolInterface::class),
            $this->createMock(StreamFactoryInterface::class),
            [
                'methods' => $methods,
            ]
        );
    }

    public function invalidMethodProvider(): array
    {
        return [
            [['GET', 'HEAD', 'POST ']],
            [['GET', 'HEAD"', 'POST']],
            [['GET', 'head', 'POST']],
        ];
    }

    public function testCalculateAgeFromResponse(): void
    {
        $httpBody = 'body';

        $stream = $this->createMock(StreamInterface::class);
        $stream->method('__toString')->willReturn($httpBody);
        $stream->method('isSeekable')->willReturn(true);
        $stream->expects($this->once())->method('rewind');
        $stream->expects($this->once())->method('detach');

        $streamFactory = $this->createMock(StreamFactoryInterface::class);
        $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream);

        $uri = $this->createMock(UriInterface::class);
        $uri->method('__toString')->willReturn('https://example.com/');

        $request = $this->createMock(RequestInterface::class);
        $request->method('getMethod')->willReturn('GET');
        $request->method('getUri')->willReturn($uri);
        $request->method('getBody')->willReturn($stream);

        $response = $this->createMock(ResponseInterface::class);
        $response->method('getStatusCode')->willReturn(200);
        $response->method('getBody')->willReturn($stream);
        $response->method('getHeader')->willReturnCallback(function ($header) {
            if ('Cache-Control' === $header) {
                return ['max-age=40'];
            }

            if ('Age' === $header) {
                return ['15'];
            }

            return [];
        });
        $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf();

        $item = $this->createMock(CacheItemInterface::class);
        $item->method('isHit')->willReturn(false);
        $item->expects($this->once())->method('set')->with($this->cacheItemConstraint([
            'response' => $response,
            'body' => $httpBody,
            'expiresAt' => 0,
            'createdAt' => 0,
            'etag' => [],
        ]))->willReturnSelf();
        $item->expects($this->once())->method('expiresAfter')->with(1025)->willReturnSelf();

        $pool = $this->createMock(CacheItemPoolInterface::class);
        $pool->method('getItem')->willReturn($item);
        $pool->expects($this->once())->method('save')->with($item);

        $plugin = $this->createPlugin($pool, $streamFactory);

        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {
        })->wait();

        self::assertSame($response, $result);
    }

    public function testSaveEtag(): void
    {
        $httpBody = 'body';

        $stream = $this->createMock(StreamInterface::class);
        $stream->method('__toString')->willReturn($httpBody);
        $stream->method('isSeekable')->willReturn(true);
        $stream->expects($this->once())->method('rewind');
        $stream->expects($this->once())->method('detach');

        $streamFactory = $this->createMock(StreamFactoryInterface::class);
        $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream);

        $uri = $this->createMock(UriInterface::class);
        $uri->method('__toString')->willReturn('https://example.com/');

        $request = $this->createMock(RequestInterface::class);
        $request->method('getBody')->willReturn($stream);
        $request->method('getMethod')->willReturn('GET');
        $request->method('getUri')->willReturn($uri);

        $response = $this->createMock(ResponseInterface::class);
        $response->method('getStatusCode')->willReturn(200);
        $response->method('getBody')->willReturn($stream);
        $response->method('getHeader')->willReturnCallback(function ($header) {
            if ('ETag' === $header) {
                return ['foo_etag'];
            }

            return [];
        });
        $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf();

        $item = $this->createMock(CacheItemInterface::class);
        $item->method('isHit')->willReturn(false);
        $item->expects($this->once())->method('expiresAfter')->with(1060)->willReturnSelf();
        $item->expects($this->once())->method('set')->with($this->cacheItemConstraint([
            'response' => $response,
            'body' => $httpBody,
            'expiresAt' => 0,
            'createdAt' => 0,
            'etag' => ['foo_etag'],
        ]))->willReturnSelf();

        $pool = $this->createMock(CacheItemPoolInterface::class);
        $pool->expects($this->once())->method('getItem')->willReturn($item);
        $pool->expects($this->once())->method('save')->with($item);

        $plugin = $this->createPlugin($pool, $streamFactory);

        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {
        })->wait();

        self::assertSame($response, $result);
    }

    public function testAddEtagAndModifiedSinceToRequest(): void
    {
        $httpBody = 'body';

        $stream = $this->createMock(StreamInterface::class);
        $stream->method('__toString')->willReturn('');

        $streamFactory = $this->createMock(StreamFactoryInterface::class);
        $streamFactory->expects($this->never())->method('createStream');

        $uri = $this->createMock(UriInterface::class);
        $uri->method('__toString')->willReturn('https://example.com/');

        $request = $this->createMock(RequestInterface::class);
        $request->method('getMethod')->willReturn('GET');
        $request->method('getUri')->willReturn($uri);
        $request->method('getBody')->willReturn($stream);
        $request->expects($this->exactly(2))
            ->method('withHeader')
            ->withConsecutive(
                ['If-Modified-Since', 'Thursday, 01-Jan-70 01:18:31 GMT'],
                ['If-None-Match', 'foo_etag']
            )
            ->willReturnSelf();

        $response = $this->createMock(ResponseInterface::class);
        $response->method('getStatusCode')->willReturn(304);

        $item = $this->createMock(CacheItemInterface::class);
        $item->expects($this->exactly(2))->method('isHit')->willReturnOnConsecutiveCalls(true, false);
        $item->method('get')->willReturn([
            'response' => $response,
            'body' => $httpBody,
            'expiresAt' => 0,
            'createdAt' => 4711,
            'etag' => ['foo_etag'],
        ]);

        $pool = $this->createMock(CacheItemPoolInterface::class);
        $pool->expects($this->once())->method('getItem')->willReturn($item);

        $plugin = $this->createPlugin($pool, $streamFactory);

        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {
        })->wait();

        self::assertSame($response, $result);
    }

    public function testServeCachedResponse(): void
    {
        $httpBody = 'body';

        $stream = $this->createMock(StreamInterface::class);

        $streamFactory = $this->createMock(StreamFactoryInterface::class);
        $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream);

        $uri = $this->createMock(UriInterface::class);
        $uri->method('__toString')->willReturn('https://example.com/');

        $requestBody = $this->createMock(StreamInterface::class);
        $requestBody->method('__toString')->willReturn('');

        $request = $this->createMock(RequestInterface::class);
        $request->method('getMethod')->willReturn('GET');
        $request->method('getUri')->willReturn($uri);
        $request->method('getBody')->willReturn($requestBody);

        $response = $this->createMock(ResponseInterface::class);
        $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf();

        $item = $this->createMock(CacheItemInterface::class);
        $item->method('isHit')->willReturn(true);
        $item->method('get')->willReturn([
            'response' => $response,
            'body' => $httpBody,
            'expiresAt' => time() + 1000000,
            'createdAt' => 4711,
            'etag' => [],
        ]);

        $pool = $this->createMock(CacheItemPoolInterface::class);
        $pool->expects($this->once())->method('getItem')->willReturn($item);

        $plugin = $this->createPlugin($pool, $streamFactory);

        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {
        })->wait();

        self::assertSame($response, $result);
    }

    public function testServeAndResaveExpiredResponse(): void
    {
        $httpBody = 'body';

        $uri = $this->createMock(UriInterface::class);
        $uri->method('__toString')->willReturn('https://example.com/');

        $requestStream = $this->createMock(StreamInterface::class);
        $requestStream->method('__toString')->willReturn('');

        $request = $this->createMock(RequestInterface::class);
        $request->method('getMethod')->willReturn('GET');
        $request->method('getUri')->willReturn($uri);
        $request->method('getBody')->willReturn($requestStream);
        $request->method('withHeader')->willReturnSelf();

        $stream = $this->createMock(StreamInterface::class);
        $streamFactory = $this->createMock(StreamFactoryInterface::class);
        $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream);

        $response = $this->createMock(ResponseInterface::class);
        $response->method('getStatusCode')->willReturn(304);
        $response->method('getHeader')->willReturn([]);
        $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf();

        $item = $this->createMock(CacheItemInterface::class);
        $item->method('isHit')->willReturn(true);
        $item->expects($this->once())->method('expiresAfter')->with(1060)->willReturnSelf();
        $item->method('get')->willReturn([
            'response' => $response,
            'body' => $httpBody,
            'expiresAt' => 0,
            'createdAt' => 4711,
            'etag' => ['foo_etag'],
        ]);
        $item->expects($this->once())->method('set')->with($this->cacheItemConstraint([
            'response' => $response,
            'body' => $httpBody,
            'expiresAt' => 0,
            'createdAt' => 0,
            'etag' => ['foo_etag'],
        ]))->willReturnSelf();

        $pool = $this->createMock(CacheItemPoolInterface::class);
        $pool->expects($this->once())->method('getItem')->willReturn($item);
        $pool->expects($this->once())->method('save')->with($item);

        $plugin = $this->createPlugin($pool, $streamFactory);

        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {
        })->wait();

        self::assertSame($response, $result);
    }

    public function testCachePrivateResponsesWhenAllowed(): void
    {
        $httpBody = 'body';

        $stream = $this->createMock(StreamInterface::class);
        $stream->method('__toString')->willReturn($httpBody);
        $stream->method('isSeekable')->willReturn(true);
        $stream->expects($this->once())->method('rewind');
        $stream->expects($this->once())->method('detach');

        $streamFactory = $this->createMock(StreamFactoryInterface::class);
        $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream);

        $uri = $this->createMock(UriInterface::class);
        $uri->method('__toString')->willReturn('https://example.com/');

        $request = $this->createMock(RequestInterface::class);
        $request->method('getMethod')->willReturn('GET');
        $request->method('getUri')->willReturn($uri);
        $request->method('getBody')->willReturn($stream);

        $response = $this->createMock(ResponseInterface::class);
        $response->method('getStatusCode')->willReturn(200);
        $response->method('getBody')->willReturn($stream);
        $response->method('getHeader')->willReturnCallback(function ($header) {
            if ('Cache-Control' === $header) {
                return ['private'];
            }

            return [];
        });
        $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf();

        $item = $this->createMock(CacheItemInterface::class);
        $item->method('isHit')->willReturn(false);
        $item->expects($this->once())->method('expiresAfter')->with(1060)->willReturnSelf();
        $item->expects($this->once())->method('set')->with($this->cacheItemConstraint([
            'response' => $response,
            'body' => $httpBody,
            'expiresAt' => 0,
            'createdAt' => 0,
            'etag' => [],
        ]))->willReturnSelf();

        $pool = $this->createMock(CacheItemPoolInterface::class);
        $pool->expects($this->once())->method('getItem')->willReturn($item);
        $pool->expects($this->once())->method('save')->with($item);

        $plugin = CachePlugin::clientCache($pool, $streamFactory, [
            'default_ttl' => 60,
            'cache_lifetime' => 1000,
        ]);

        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {
        })->wait();

        self::assertSame($response, $result);
    }

    public function testDoNotStoreResponsesOfRequestsToBlacklistedPaths(): void
    {
        $httpBody = 'body';

        $stream = $this->createMock(StreamInterface::class);
        $stream->method('__toString')->willReturn($httpBody);
        $stream->method('isSeekable')->willReturn(true);

        $streamFactory = $this->createMock(StreamFactoryInterface::class);

        $uri = $this->createMock(UriInterface::class);
        $uri->method('__toString')->willReturn('https://example.com/foo');

        $request = $this->createMock(RequestInterface::class);
        $request->method('getMethod')->willReturn('GET');
        $request->method('getUri')->willReturn($uri);
        $request->method('getBody')->willReturn($stream);

        $response = $this->createMock(ResponseInterface::class);
        $response->method('getStatusCode')->willReturn(200);
        $response->method('getBody')->willReturn($stream);
        $response->method('getHeader')->willReturn([]);

        $item = $this->createMock(CacheItemInterface::class);
        $item->method('isHit')->willReturn(false);
        $item->expects($this->never())->method('set');

        $pool = $this->createMock(CacheItemPoolInterface::class);
        $pool->expects($this->once())->method('getItem')->willReturn($item);
        $pool->expects($this->never())->method('save');

        $plugin = CachePlugin::clientCache($pool, $streamFactory, [
            'default_ttl' => 60,
            'cache_lifetime' => 1000,
            'blacklisted_paths' => ['@/foo@'],
        ]);

        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {
        })->wait();

        self::assertSame($response, $result);
    }

    public function testStoreResponsesOfRequestsNotInBlacklistedPaths(): void
    {
        $httpBody = 'body';

        $stream = $this->createMock(StreamInterface::class);
        $stream->method('__toString')->willReturn($httpBody);
        $stream->method('isSeekable')->willReturn(true);
        $stream->expects($this->once())->method('rewind');
        $stream->expects($this->once())->method('detach');

        $streamFactory = $this->createMock(StreamFactoryInterface::class);
        $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream);

        $uri = $this->createMock(UriInterface::class);
        $uri->method('__toString')->willReturn('https://example.com/');

        $request = $this->createMock(RequestInterface::class);
        $request->method('getMethod')->willReturn('GET');
        $request->method('getUri')->willReturn($uri);
        $request->method('getBody')->willReturn($stream);

        $response = $this->createMock(ResponseInterface::class);
        $response->method('getStatusCode')->willReturn(200);
        $response->method('getBody')->willReturn($stream);
        $response->method('getHeader')->willReturn([]);
        $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf();

        $item = $this->createMock(CacheItemInterface::class);
        $item->method('isHit')->willReturn(false);
        $item->expects($this->once())->method('expiresAfter')->with(1060)->willReturnSelf();
        $item->expects($this->once())->method('set')->with($this->cacheItemConstraint([
            'response' => $response,
            'body' => $httpBody,
            'expiresAt' => 0,
            'createdAt' => 0,
            'etag' => [],
        ]))->willReturnSelf();

        $pool = $this->createMock(CacheItemPoolInterface::class);
        $pool->expects($this->once())->method('getItem')->willReturn($item);
        $pool->expects($this->once())->method('save')->with($item);

        $plugin = CachePlugin::clientCache($pool, $streamFactory, [
            'default_ttl' => 60,
            'cache_lifetime' => 1000,
            'blacklisted_paths' => ['@/foo@'],
        ]);

        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {
        })->wait();

        self::assertSame($response, $result);
    }

    public function testCustomCacheKeyGenerator(): void
    {
        $stream = $this->createMock(StreamInterface::class);
        $stream->expects($this->once())->method('rewind');
        $streamFactory = $this->createMock(StreamFactoryInterface::class);
        $streamFactory->expects($this->once())->method('createStream')->willReturn($stream);

        $request = $this->createMock(RequestInterface::class);
        $request->method('getMethod')->willReturn('GET');

        $response = $this->createMock(ResponseInterface::class);
        $response->expects($this->once())->method('withBody')->willReturnSelf();

        $item = $this->createMock(CacheItemInterface::class);
        $item->method('isHit')->willReturn(true);
        $item->method('get')->willReturn([
            'response' => $response,
            'body' => 'body',
            'expiresAt' => null,
            'createdAt' => 0,
            'etag' => [],
        ]);

        $pool = $this->createMock(CacheItemPoolInterface::class);
        $pool->expects($this->once())->method('getItem')->willReturn($item);

        $generator = $this->createMock(SimpleGenerator::class);
        $generator->expects($this->once())->method('generate')->with($request)->willReturn('foo');

        $plugin = CachePlugin::clientCache($pool, $streamFactory, [
            'cache_key_generator' => $generator,
        ]);

        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {
        })->wait();

        self::assertSame($response, $result);
    }
}


================================================
FILE: tests/Cache/Generator/HeaderCacheKeyGeneratorTest.php
================================================
<?php

namespace Http\Client\Common\Plugin\Tests\Cache\Generator;

use Http\Client\Common\Plugin\Cache\Generator\CacheKeyGenerator;
use Http\Client\Common\Plugin\Cache\Generator\HeaderCacheKeyGenerator;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;

class HeaderCacheKeyGeneratorTest extends TestCase
{
    public function testInterface(): void
    {
        $this->assertInstanceOf(CacheKeyGenerator::class, new HeaderCacheKeyGenerator(['Authorization', 'Content-Type']));
    }

    public function testGenerateCacheFromRequest(): void
    {
        $uri = $this->createMock(UriInterface::class);
        $uri->expects($this->once())->method('__toString')->willReturn('http://example.com/foo');

        $body = $this->createMock(StreamInterface::class);
        $body->expects($this->once())->method('__toString')->willReturn('');

        $request = $this->createMock(RequestInterface::class);
        $request->expects($this->once())->method('getMethod')->willReturn('GET');
        $request->expects($this->once())->method('getUri')->willReturn($uri);
        $request->expects($this->exactly(2))->method('getHeaderLine')->willReturnMap([
            ['Authorization', 'bar'],
            ['Content-Type', 'application/baz'],
        ]);
        $request->expects($this->once())->method('getBody')->willReturn($body);

        $generator = new HeaderCacheKeyGenerator(['Authorization', 'Content-Type']);

        $this->assertSame(
            'GET http://example.com/foo Authorization:"bar" Content-Type:"application/baz" ',
            $generator->generate($request)
        );
    }
}


================================================
FILE: tests/Cache/Generator/SimpleGeneratorTest.php
================================================
<?php

namespace Http\Client\Common\Plugin\Tests\Cache\Generator;

use Http\Client\Common\Plugin\Cache\Generator\CacheKeyGenerator;
use Http\Client\Common\Plugin\Cache\Generator\SimpleGenerator;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;

class SimpleGeneratorTest extends TestCase
{
    public function testInterface(): void
    {
        $generator = new SimpleGenerator();
        $this->assertInstanceOf(CacheKeyGenerator::class, $generator);
    }

    public function testGenerateCacheFromRequest(): void
    {
        $uri = $this->createMock(UriInterface::class);
        $uri->expects($this->once())->method('__toString')->willReturn('http://example.com/foo');

        $body = $this->createMock(StreamInterface::class);
        $body->expects($this->once())->method('__toString')->willReturn('bar');

        $request = $this->createMock(RequestInterface::class);
        $request->expects($this->once())->method('getMethod')->willReturn('GET');
        $request->expects($this->once())->method('getUri')->willReturn($uri);
        $request->expects($this->once())->method('getBody')->willReturn($body);

        $generator = new SimpleGenerator();

        $this->assertSame('GET http://example.com/foo bar', $generator->generate($request));
    }

    public function testGenerateCacheFromRequestWithNoBody(): void
    {
        $uri = $this->createMock(UriInterface::class);
        $uri->expects($this->once())->method('__toString')->willReturn('http://example.com/foo');

        $body = $this->createMock(StreamInterface::class);
        $body->expects($this->once())->method('__toString')->willReturn('');

        $request = $this->createMock(RequestInterface::class);
        $request->expects($this->once())->method('getMethod')->willReturn('GET');
        $request->expects($this->once())->method('getUri')->willReturn($uri);
        $request->expects($this->once())->method('getBody')->willReturn($body);

        $generator = new SimpleGenerator();

        $this->assertSame('GET http://example.com/foo', $generator->generate($request));
    }
}
Download .txt
gitextract_nm_wkhzu/

├── .editorconfig
├── .gitattributes
├── .github/
│   ├── CONTRIBUTING.md
│   ├── ISSUE_TEMPLATE.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── workflows/
│       ├── .editorconfig
│       ├── static.yml
│       └── tests.yml
├── .gitignore
├── .php_cs
├── .scrutinizer.yml
├── .styleci.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── composer.json
├── phpspec.ci.yml
├── phpspec.yml.dist
├── phpstan.neon.dist
├── phpunit.xml.dist
├── src/
│   ├── Cache/
│   │   ├── Generator/
│   │   │   ├── CacheKeyGenerator.php
│   │   │   ├── HeaderCacheKeyGenerator.php
│   │   │   └── SimpleGenerator.php
│   │   └── Listener/
│   │       ├── AddHeaderCacheListener.php
│   │       └── CacheListener.php
│   ├── CachePlugin.php
│   └── Exception/
│       └── RewindStreamException.php
└── tests/
    └── Cache/
        ├── CachePluginTest.php
        └── Generator/
            ├── HeaderCacheKeyGeneratorTest.php
            └── SimpleGeneratorTest.php
Download .txt
SYMBOL INDEX (57 symbols across 10 files)

FILE: src/Cache/Generator/CacheKeyGenerator.php
  type CacheKeyGenerator (line 12) | interface CacheKeyGenerator
    method generate (line 19) | public function generate(RequestInterface $request);

FILE: src/Cache/Generator/HeaderCacheKeyGenerator.php
  class HeaderCacheKeyGenerator (line 12) | class HeaderCacheKeyGenerator implements CacheKeyGenerator
    method __construct (line 24) | public function __construct(array $headerNames)
    method generate (line 29) | public function generate(RequestInterface $request)

FILE: src/Cache/Generator/SimpleGenerator.php
  class SimpleGenerator (line 12) | class SimpleGenerator implements CacheKeyGenerator
    method generate (line 14) | public function generate(RequestInterface $request)

FILE: src/Cache/Listener/AddHeaderCacheListener.php
  class AddHeaderCacheListener (line 14) | class AddHeaderCacheListener implements CacheListener
    method __construct (line 22) | public function __construct($headerName = 'X-Cache')
    method onCacheResponse (line 36) | public function onCacheResponse(RequestInterface $request, ResponseInt...

FILE: src/Cache/Listener/CacheListener.php
  type CacheListener (line 16) | interface CacheListener
    method onCacheResponse (line 27) | public function onCacheResponse(RequestInterface $request, ResponseInt...

FILE: src/CachePlugin.php
  class CachePlugin (line 26) | final class CachePlugin implements Plugin
    method __construct (line 69) | public function __construct(CacheItemPoolInterface $pool, StreamFactor...
    method clientCache (line 95) | public static function clientCache(CacheItemPoolInterface $pool, Strea...
    method serverCache (line 117) | public static function serverCache(CacheItemPoolInterface $pool, Strea...
    method doHandleRequest (line 127) | protected function doHandleRequest(RequestInterface $request, callable...
    method calculateCacheItemExpiresAfter (line 225) | private function calculateCacheItemExpiresAfter(?int $maxAge): ?int
    method calculateResponseExpiresAt (line 240) | private function calculateResponseExpiresAt(?int $maxAge): ?int
    method isCacheable (line 254) | protected function isCacheable(ResponseInterface $response)
    method isCacheableRequest (line 273) | private function isCacheableRequest(RequestInterface $request): bool
    method getCacheControlDirective (line 292) | private function getCacheControlDirective(ResponseInterface $response,...
    method createCacheKey (line 309) | private function createCacheKey(RequestInterface $request): string
    method getMaxAge (line 321) | private function getMaxAge(ResponseInterface $response): ?int
    method configureOptions (line 350) | private function configureOptions(OptionsResolver $resolver): void
    method createResponseFromCacheItem (line 397) | private function createResponseFromCacheItem(CacheItemInterface $cache...
    method getModifiedSinceHeaderValue (line 417) | private function getModifiedSinceHeaderValue(CacheItemInterface $cache...
    method getETag (line 434) | private function getETag(CacheItemInterface $cacheItem): ?string
    method handleCacheListeners (line 454) | private function handleCacheListeners(RequestInterface $request, Respo...

FILE: src/Exception/RewindStreamException.php
  class RewindStreamException (line 10) | class RewindStreamException extends \RuntimeException implements Exception

FILE: tests/Cache/CachePluginTest.php
  class CachePluginTest (line 20) | class CachePluginTest extends TestCase
    method createPlugin (line 22) | private function createPlugin(CacheItemPoolInterface $pool, StreamFact...
    method cacheItemConstraint (line 32) | private function cacheItemConstraint(array $expected): Callback
    method createFulfilledNext (line 57) | private function createFulfilledNext(ResponseInterface $response): cal...
    method testInterface (line 64) | public function testInterface(): void
    method testCacheResponses (line 74) | public function testCacheResponses(): void
    method testDoNotStoreFailedResponses (line 123) | public function testDoNotStoreFailedResponses(): void
    method testDoNotStorePostRequestsByDefault (line 154) | public function testDoNotStorePostRequestsByDefault(): void
    method testStorePostRequestsWhenAllowed (line 172) | public function testStorePostRequestsWhenAllowed(): void
    method testDoNotAllowInvalidRequestMethods (line 227) | public function testDoNotAllowInvalidRequestMethods(array $methods): void
    method invalidMethodProvider (line 240) | public function invalidMethodProvider(): array
    method testCalculateAgeFromResponse (line 249) | public function testCalculateAgeFromResponse(): void
    method testSaveEtag (line 309) | public function testSaveEtag(): void
    method testAddEtagAndModifiedSinceToRequest (line 365) | public function testAddEtagAndModifiedSinceToRequest(): void
    method testServeCachedResponse (line 414) | public function testServeCachedResponse(): void
    method testServeAndResaveExpiredResponse (line 458) | public function testServeAndResaveExpiredResponse(): void
    method testCachePrivateResponsesWhenAllowed (line 513) | public function testCachePrivateResponsesWhenAllowed(): void
    method testDoNotStoreResponsesOfRequestsToBlacklistedPaths (line 572) | public function testDoNotStoreResponsesOfRequestsToBlacklistedPaths():...
    method testStoreResponsesOfRequestsNotInBlacklistedPaths (line 615) | public function testStoreResponsesOfRequestsNotInBlacklistedPaths(): void
    method testCustomCacheKeyGenerator (line 669) | public function testCustomCacheKeyGenerator(): void

FILE: tests/Cache/Generator/HeaderCacheKeyGeneratorTest.php
  class HeaderCacheKeyGeneratorTest (line 12) | class HeaderCacheKeyGeneratorTest extends TestCase
    method testInterface (line 14) | public function testInterface(): void
    method testGenerateCacheFromRequest (line 19) | public function testGenerateCacheFromRequest(): void

FILE: tests/Cache/Generator/SimpleGeneratorTest.php
  class SimpleGeneratorTest (line 12) | class SimpleGeneratorTest extends TestCase
    method testInterface (line 14) | public function testInterface(): void
    method testGenerateCacheFromRequest (line 20) | public function testGenerateCacheFromRequest(): void
    method testGenerateCacheFromRequestWithNoBody (line 38) | public function testGenerateCacheFromRequestWithNoBody(): void
Condensed preview — 30 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (72K chars).
[
  {
    "path": ".editorconfig",
    "chars": 147,
    "preview": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 4\nindent_style = space\ninsert_final_newline = true\ntrim_"
  },
  {
    "path": ".gitattributes",
    "chars": 384,
    "preview": ".editorconfig     export-ignore\n.gitattributes    export-ignore\n/.github/         export-ignore\n.gitignore        export"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "chars": 119,
    "preview": "# Contributing\n\nPlease see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html).\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "chars": 548,
    "preview": "| Q            | A\n| ------------ | ---\n| Bug?         | no|yes\n| New Feature? | no|yes\n| Version      | Specific versio"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "chars": 1040,
    "preview": "| Q               | A\n| --------------- | ---\n| Bug fix?        | no|yes\n| New feature?    | no|yes\n| BC breaks?      | "
  },
  {
    "path": ".github/workflows/.editorconfig",
    "chars": 24,
    "preview": "[*.yml]\nindent_size = 2\n"
  },
  {
    "path": ".github/workflows/static.yml",
    "chars": 336,
    "preview": "name: Static analysis\n\non:\n  push:\n    branches:\n      - '*.x'\n  pull_request:\n\njobs:\n  phpstan:\n    name: PHPStan\n    r"
  },
  {
    "path": ".github/workflows/tests.yml",
    "chars": 2132,
    "preview": "name: tests\n\non:\n  push:\n    branches:\n      - '*.x'\n  pull_request:\n\njobs:\n  latest:\n    name: PHP ${{ matrix.php }} La"
  },
  {
    "path": ".gitignore",
    "chars": 69,
    "preview": "/behat.yml\n/build/\n/composer.lock\n/phpspec.yml\n/phpunit.xml\n/vendor/\n"
  },
  {
    "path": ".php_cs",
    "chars": 341,
    "preview": "<?php\n\n/*\n * In order to make it work, fabpot/php-cs-fixer and sllh/php-cs-fixer-styleci-bridge must be installed global"
  },
  {
    "path": ".scrutinizer.yml",
    "chars": 136,
    "preview": "filter:\n    paths: [src/*]\nchecks:\n    php:\n        code_rating: true\n        duplication: true\ntools:\n    external_code"
  },
  {
    "path": ".styleci.yml",
    "chars": 99,
    "preview": "preset: symfony\n\nfinder:\n    exclude:\n        - \"spec\"\n    path:\n        - \"src\"\n        - \"tests\"\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 3950,
    "preview": "# Change Log\n\n# Version 2\n\n## 2.0.2 - 2025-12-01\n\n- Support Symfony 8.\n- Test with PHP 8.5.\n\n## 2.0.1 - 2024-10-02\n\n- Te"
  },
  {
    "path": "LICENSE",
    "chars": 1082,
    "preview": "Copyright (c) 2015-2016 PHP HTTP Team <team@php-http.org>\n\nPermission is hereby granted, free of charge, to any person o"
  },
  {
    "path": "README.md",
    "chars": 1542,
    "preview": "# Cache Plugin\n\n[![Latest Version](https://img.shields.io/github/release/php-http/cache-plugin.svg?style=flat-square)](h"
  },
  {
    "path": "composer.json",
    "chars": 1071,
    "preview": "{\n    \"name\": \"php-http/cache-plugin\",\n    \"description\": \"PSR-6 Cache plugin for HTTPlug\",\n    \"license\": \"MIT\",\n    \"k"
  },
  {
    "path": "phpspec.ci.yml",
    "chars": 291,
    "preview": "suites:\n    cache_plugin_suite:\n        namespace: Http\\Client\\Common\\Plugin\n        psr4_prefix: Http\\Client\\Common\\Plu"
  },
  {
    "path": "phpspec.yml.dist",
    "chars": 147,
    "preview": "suites:\n    cache_plugin_suite:\n        namespace: Http\\Client\\Common\\Plugin\n        psr4_prefix: Http\\Client\\Common\\Plu"
  },
  {
    "path": "phpstan.neon.dist",
    "chars": 87,
    "preview": "parameters:\n    level: 8\n    paths:\n        - src\n    treatPhpDocTypesAsCertain: false\n"
  },
  {
    "path": "phpunit.xml.dist",
    "chars": 513,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:noNam"
  },
  {
    "path": "src/Cache/Generator/CacheKeyGenerator.php",
    "chars": 383,
    "preview": "<?php\n\nnamespace Http\\Client\\Common\\Plugin\\Cache\\Generator;\n\nuse Psr\\Http\\Message\\RequestInterface;\n\n/**\n * An interface"
  },
  {
    "path": "src/Cache/Generator/HeaderCacheKeyGenerator.php",
    "chars": 981,
    "preview": "<?php\n\nnamespace Http\\Client\\Common\\Plugin\\Cache\\Generator;\n\nuse Psr\\Http\\Message\\RequestInterface;\n\n/**\n * Generate a c"
  },
  {
    "path": "src/Cache/Generator/SimpleGenerator.php",
    "chars": 533,
    "preview": "<?php\n\nnamespace Http\\Client\\Common\\Plugin\\Cache\\Generator;\n\nuse Psr\\Http\\Message\\RequestInterface;\n\n/**\n * Generate a c"
  },
  {
    "path": "src/Cache/Listener/AddHeaderCacheListener.php",
    "chars": 1230,
    "preview": "<?php\n\nnamespace Http\\Client\\Common\\Plugin\\Cache\\Listener;\n\nuse Psr\\Http\\Message\\RequestInterface;\nuse Psr\\Http\\Message\\"
  },
  {
    "path": "src/Cache/Listener/CacheListener.php",
    "chars": 1028,
    "preview": "<?php\n\nnamespace Http\\Client\\Common\\Plugin\\Cache\\Listener;\n\nuse Psr\\Http\\Message\\RequestInterface;\nuse Psr\\Http\\Message\\"
  },
  {
    "path": "src/CachePlugin.php",
    "chars": 17801,
    "preview": "<?php\n\nnamespace Http\\Client\\Common\\Plugin;\n\nuse Http\\Client\\Common\\Plugin;\nuse Http\\Client\\Common\\Plugin\\Exception\\Rewi"
  },
  {
    "path": "src/Exception/RewindStreamException.php",
    "chars": 215,
    "preview": "<?php\n\nnamespace Http\\Client\\Common\\Plugin\\Exception;\n\nuse Http\\Client\\Exception;\n\n/**\n * @author Théo FIDRY <theo.fidry"
  },
  {
    "path": "tests/Cache/CachePluginTest.php",
    "chars": 28222,
    "preview": "<?php\n\nnamespace Http\\Client\\Common\\Plugin\\Tests\\Cache;\n\nuse Http\\Client\\Common\\Plugin;\nuse Http\\Client\\Common\\Plugin\\Ca"
  },
  {
    "path": "tests/Cache/Generator/HeaderCacheKeyGeneratorTest.php",
    "chars": 1692,
    "preview": "<?php\n\nnamespace Http\\Client\\Common\\Plugin\\Tests\\Cache\\Generator;\n\nuse Http\\Client\\Common\\Plugin\\Cache\\Generator\\CacheKe"
  },
  {
    "path": "tests/Cache/Generator/SimpleGeneratorTest.php",
    "chars": 2171,
    "preview": "<?php\n\nnamespace Http\\Client\\Common\\Plugin\\Tests\\Cache\\Generator;\n\nuse Http\\Client\\Common\\Plugin\\Cache\\Generator\\CacheKe"
  }
]

About this extraction

This page contains the full source code of the php-http/cache-plugin GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 30 files (66.7 KB), approximately 17.1k tokens, and a symbol index with 57 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!