[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 4\nindent_style = space\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".gitattributes",
    "content": ".editorconfig     export-ignore\n.gitattributes    export-ignore\n/.github/         export-ignore\n.gitignore        export-ignore\n/.php_cs          export-ignore\n/.scrutinizer.yml export-ignore\n/.styleci.yml     export-ignore\n/behat.yml.dist   export-ignore\n/features/        export-ignore\n/phpspec.ci.yml   export-ignore\n/phpspec.yml.dist export-ignore\n/spec/            export-ignore\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "# Contributing\n\nPlease see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html).\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "| Q            | A\n| ------------ | ---\n| Bug?         | no|yes\n| New Feature? | no|yes\n| Version      | Specific version or SHA of a commit\n\n\n#### Actual Behavior\n\nWhat is the actual behavior?\n\n\n#### Expected Behavior\n\nWhat is the behavior you expect?\n\n\n#### Steps to Reproduce\n\nWhat are the steps to reproduce this bug? Please add code examples,\nscreenshots or links to GitHub repositories that reproduce the problem.\n\n\n#### Possible Solutions\n\nIf you have already ideas how to solve the issue, add them here.\n(remove this section if not needed)\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "| Q               | A\n| --------------- | ---\n| Bug fix?        | no|yes\n| New feature?    | no|yes\n| BC breaks?      | no|yes\n| Deprecations?   | no|yes\n| Related tickets | fixes #X, partially #Y, mentioned in #Z\n| Documentation   | if this is a new feature, link to pull request in https://github.com/php-http/documentation that adds relevant documentation\n| License         | MIT\n\n\n#### What's in this PR?\n\nExplain what the changes in this PR do.\n\n\n#### Why?\n\nWhich problem does the PR fix? (remove this section if you linked an issue above)\n\n\n#### Example Usage\n\n``` php\n// If you added new features, show examples of how to use them here\n// (remove this section if not a new feature)\n\n$foo = new Foo();\n\n// Now we can do\n$foo->doSomething();\n```\n\n\n#### Checklist\n\n- [ ] Updated CHANGELOG.md to describe BC breaks / deprecations | new feature | bugfix\n- [ ] Documentation pull request created (if not simply a bugfix)\n\n\n#### To Do\n\n- [ ] If the PR is not complete but you want to discuss the approach, list what remains to be done here\n"
  },
  {
    "path": ".github/workflows/.editorconfig",
    "content": "[*.yml]\nindent_size = 2\n"
  },
  {
    "path": ".github/workflows/static.yml",
    "content": "name: Static analysis\n\non:\n  push:\n    branches:\n      - '*.x'\n  pull_request:\n\njobs:\n  phpstan:\n    name: PHPStan\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: PHPStan\n        uses: docker://oskarstark/phpstan-ga\n        with:\n          args: analyze --no-progress\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: tests\n\non:\n  push:\n    branches:\n      - '*.x'\n  pull_request:\n\njobs:\n  latest:\n    name: PHP ${{ matrix.php }} Latest\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        php: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: ${{ matrix.php }}\n          tools: composer:v2\n          coverage: none\n\n      - name: Install PHP dependencies\n        run: composer update --prefer-dist --no-interaction --no-progress\n\n      - name: Execute tests\n        run: composer test\n\n  lowest:\n    name: PHP ${{ matrix.php }} Lowest\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        php: ['7.1', '7.4', '8.0']\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: ${{ matrix.php }}\n          tools: composer:v2\n          coverage: none\n\n      - name: Install dependencies\n        run: |\n          composer require \"sebastian/comparator:^3.0.2\" --no-interaction --no-update\n          composer update --prefer-dist --prefer-stable --prefer-lowest --no-interaction --no-progress\n\n      - name: Execute tests\n        run: composer test\n\n  coverage:\n    name: Code Coverage\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: 7.4\n          tools: composer:v2\n          coverage: xdebug\n\n      - name: Install dependencies\n        run: |\n          composer require \"friends-of-phpspec/phpspec-code-coverage\" --no-interaction --no-update\n          composer update --prefer-dist --no-interaction --no-progress\n\n      - name: Execute tests\n        run: composer test-ci\n\n      - name: Upload coverage\n        run: |\n          wget https://scrutinizer-ci.com/ocular.phar\n          php ocular.phar code-coverage:upload --format=php-clover build/coverage.xml\n"
  },
  {
    "path": ".gitignore",
    "content": "/behat.yml\n/build/\n/composer.lock\n/phpspec.yml\n/phpunit.xml\n/vendor/\n"
  },
  {
    "path": ".php_cs",
    "content": "<?php\n\n/*\n * In order to make it work, fabpot/php-cs-fixer and sllh/php-cs-fixer-styleci-bridge must be installed globally\n * with composer.\n *\n * @link https://github.com/Soullivaneuh/php-cs-fixer-styleci-bridge\n * @link https://github.com/FriendsOfPHP/PHP-CS-Fixer\n */\n\nuse SLLH\\StyleCIBridge\\ConfigBridge;\n\nreturn ConfigBridge::create();\n"
  },
  {
    "path": ".scrutinizer.yml",
    "content": "filter:\n    paths: [src/*]\nchecks:\n    php:\n        code_rating: true\n        duplication: true\ntools:\n    external_code_coverage: true\n"
  },
  {
    "path": ".styleci.yml",
    "content": "preset: symfony\n\nfinder:\n    exclude:\n        - \"spec\"\n    path:\n        - \"src\"\n        - \"tests\"\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# 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- Test with PHP 8.3 and 8.4.\n\n## 2.0.0 - 2024-02-19\n\n### Changed\n\n- Drop support of deprecated PHP-HTTP `StreamFactory`, only PSR-17 `StreamFactoryInterface` is now supported.\n\n# Version 1\n\n## 1.8.1 - 2023-11-21\n\n- Allow installation with Symfony 7.\n\n## 1.8.0 - 2023-04-28\n\n- Avoid PHP warning about serializing resources when serializing the response by detaching the stream.\n\n## 1.7.6 - 2023-04-28\n\n- Test with PHP 8.1 and 8.2\n- Made phpspec tests compatible with PSR-7 2.0 strict typing\n- Detect `null` and use 0 explicitly to calculate expiration\n\n## 1.7.5 - 2022-01-18\n\n- Allow installation with psr/cache 3.0 (1.0 and 2.0 are still allowed too)\n\n## 1.7.4 - 2021-11-30\n\n### Added\n\n- Allow installation with Symfony 6\n\n## 1.7.3 - 2021-11-03\n\n### Changed\n\n- Be more defensive about cache hits. A cache entry can technically contain `null`.\n\n## 1.7.2 - 2021-04-14\n\n### Added\n\n- Allow installation with psr/cache 2.0 (1.0 still allowed too)\n\n## 1.7.1 - 2020-07-13\n\n### Added\n\n- Support for PHP 8\n\n## 1.7.0 - 2019-12-17\n\n### Added\n\n* Support for Symfony 5.\n* Support for PSR-17 `StreamFactoryInterface`.\n* 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.\n\n## 1.6.0 - 2019-01-23\n\n### Added\n\n* Support for HTTPlug 2 / PSR-18\n* 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.\n\n## 1.5.0 - 2017-11-29\n\n### Added\n\n* Support for Symfony 4\n\n### Changed\n\n* Removed check if etag is a string. Etag can never be a string, it is always an array.\n\n## 1.4.0 - 2017-04-05\n\n### Added\n\n- `CacheKeyGenerator` interface that allow you to configure how the PSR-6 cache key is created. There are two implementations\nof this interface: `SimpleGenerator` (default) and `HeaderCacheKeyGenerator`.\n\n### Fixed\n\n- Issue where deprecation warning always was triggered. Not it is just triggered if `respect_cache_headers` is used.\n\n## 1.3.0 - 2017-03-28\n\n### Added\n\n- New `methods` option which allows to configure the request methods which can be cached.\n- New `respect_response_cache_directives` option to define specific cache directives to respect when handling responses.\n- Introduced `CachePlugin::clientCache` and `CachePlugin::serverCache` factory methods to easily setup the plugin with\n  the correct config settigns for each usecase.\n\n### Changed\n\n- 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`.\n- We always rewind the stream after loading response from cache.\n\n### Deprecated\n\n- 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.\n  If you had set `respect_cache_headers` to `false`, set the directives to `[]` to ignore all directives.\n\n\n## 1.2.0 - 2016-08-16\n\n### Changed\n\n- The default value for `default_ttl` is changed from `null` to `0`.\n\n### Fixed\n\n- Issue when you use `respect_cache_headers=>false` in combination with `default_ttl=>null`.\n- We allow `cache_lifetime` to be set to `null`.\n\n\n## 1.1.0 - 2016-08-04\n\n### Added\n\n- Support for cache validation with ETag and Last-Modified headers. (Enabled automatically when the server sends the relevant headers.)\n- `hash_algo` config option used for cache key generation (defaults to **sha1**).\n\n### Changed\n\n- Default hash algo used for cache generation (from **md5** to **sha1**).\n\n### Fixed\n\n- Cast max age header to integer in order to get valid expiration value.\n\n\n## 1.0.0 - 2016-05-05\n\n- Initial release\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2015-2016 PHP HTTP Team <team@php-http.org>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is furnished\nto do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Cache Plugin\n\n[![Latest Version](https://img.shields.io/github/release/php-http/cache-plugin.svg?style=flat-square)](https://github.com/php-http/cache-plugin/releases)\n[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)\n[![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)\n[![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)\n[![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)\n[![Total Downloads](https://img.shields.io/packagist/dt/php-http/cache-plugin.svg?style=flat-square)](https://packagist.org/packages/php-http/cache-plugin)\n\n**PSR-6 Cache plugin for HTTPlug.**\n\n\n## Install\n\nVia Composer\n\n``` bash\ncomposer require php-http/cache-plugin\n```\n\n\n## Documentation\n\nPlease see the [official documentation](http://docs.php-http.org/en/latest/plugins/cache.html).\n\n\n## Testing\n\n``` bash\ncomposer test\n```\n\n\n## Contributing\n\nPlease see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html).\n\n\n## Security\n\nIf you discover any security related issues, please contact us at [security@php-http.org](mailto:security@php-http.org).\n\n\n## License\n\nThe MIT License (MIT). Please see [License File](LICENSE) for more information.\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"php-http/cache-plugin\",\n    \"description\": \"PSR-6 Cache plugin for HTTPlug\",\n    \"license\": \"MIT\",\n    \"keywords\": [\"cache\", \"http\", \"httplug\", \"plugin\"],\n    \"homepage\": \"http://httplug.io\",\n    \"authors\": [\n        {\n            \"name\": \"Márk Sági-Kazár\",\n            \"email\": \"mark.sagikazar@gmail.com\"\n        }\n    ],\n    \"require\": {\n        \"php\": \"^7.1 || ^8.0\",\n        \"psr/cache\": \"^1.0 || ^2.0 || ^3.0\",\n        \"php-http/client-common\": \"^1.9 || ^2.0\",\n        \"psr/http-factory-implementation\": \"^1.0\",\n        \"symfony/options-resolver\": \"^2.6 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0\"\n    },\n    \"require-dev\": {\n        \"nyholm/psr7\": \"^1.6.1\",\n        \"phpunit/phpunit\": \"^7.5 || ^8.5 || ^9.6\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Http\\\\Client\\\\Common\\\\Plugin\\\\\": \"src/\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Http\\\\Client\\\\Common\\\\Plugin\\\\Tests\\\\\": \"tests/\"\n        }\n    },\n    \"scripts\": {\n        \"test\": \"vendor/bin/phpunit\",\n        \"test-ci\": \"vendor/bin/phpunit\"\n    }\n}\n"
  },
  {
    "path": "phpspec.ci.yml",
    "content": "suites:\n    cache_plugin_suite:\n        namespace: Http\\Client\\Common\\Plugin\n        psr4_prefix: Http\\Client\\Common\\Plugin\nformatter.name: pretty\nextensions:\n    FriendsOfPhpSpec\\PhpSpec\\CodeCoverage\\CodeCoverageExtension: ~\ncode_coverage:\n    format: clover\n    output: build/coverage.xml\n"
  },
  {
    "path": "phpspec.yml.dist",
    "content": "suites:\n    cache_plugin_suite:\n        namespace: Http\\Client\\Common\\Plugin\n        psr4_prefix: Http\\Client\\Common\\Plugin\nformatter.name: pretty\n"
  },
  {
    "path": "phpstan.neon.dist",
    "content": "parameters:\n    level: 8\n    paths:\n        - src\n    treatPhpDocTypesAsCertain: false\n"
  },
  {
    "path": "phpunit.xml.dist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/9.6/phpunit.xsd\"\n         bootstrap=\"vendor/autoload.php\"\n         colors=\"true\"\n         beStrictAboutTestsThatDoNotTestAnything=\"true\"\n         beStrictAboutTodoAnnotatedTests=\"true\">\n    <testsuites>\n        <testsuite name=\"Cache Plugin Test Suite\">\n            <directory>tests</directory>\n        </testsuite>\n    </testsuites>\n</phpunit>\n"
  },
  {
    "path": "src/Cache/Generator/CacheKeyGenerator.php",
    "content": "<?php\n\nnamespace Http\\Client\\Common\\Plugin\\Cache\\Generator;\n\nuse Psr\\Http\\Message\\RequestInterface;\n\n/**\n * An interface for generate a cache key.\n *\n * @author Tobias Nyholm <tobias.nyholm@gmail.com>\n */\ninterface CacheKeyGenerator\n{\n    /**\n     * Generate a cache key from a Request.\n     *\n     * @return string\n     */\n    public function generate(RequestInterface $request);\n}\n"
  },
  {
    "path": "src/Cache/Generator/HeaderCacheKeyGenerator.php",
    "content": "<?php\n\nnamespace Http\\Client\\Common\\Plugin\\Cache\\Generator;\n\nuse Psr\\Http\\Message\\RequestInterface;\n\n/**\n * Generate a cache key by using HTTP headers.\n *\n * @author Tobias Nyholm <tobias.nyholm@gmail.com>\n */\nclass HeaderCacheKeyGenerator implements CacheKeyGenerator\n{\n    /**\n     * The header names we should take into account when creating the cache key.\n     *\n     * @var string[]\n     */\n    private $headerNames;\n\n    /**\n     * @param string[] $headerNames\n     */\n    public function __construct(array $headerNames)\n    {\n        $this->headerNames = $headerNames;\n    }\n\n    public function generate(RequestInterface $request)\n    {\n        $concatenatedHeaders = [];\n        foreach ($this->headerNames as $headerName) {\n            $concatenatedHeaders[] = sprintf(' %s:\"%s\"', $headerName, $request->getHeaderLine($headerName));\n        }\n\n        return $request->getMethod().' '.$request->getUri().implode('', $concatenatedHeaders).' '.$request->getBody();\n    }\n}\n"
  },
  {
    "path": "src/Cache/Generator/SimpleGenerator.php",
    "content": "<?php\n\nnamespace Http\\Client\\Common\\Plugin\\Cache\\Generator;\n\nuse Psr\\Http\\Message\\RequestInterface;\n\n/**\n * Generate a cache key from the request method, URI and body.\n *\n * @author Tobias Nyholm <tobias.nyholm@gmail.com>\n */\nclass SimpleGenerator implements CacheKeyGenerator\n{\n    public function generate(RequestInterface $request)\n    {\n        $body = (string) $request->getBody();\n        if (!empty($body)) {\n            $body = ' '.$body;\n        }\n\n        return $request->getMethod().' '.$request->getUri().$body;\n    }\n}\n"
  },
  {
    "path": "src/Cache/Listener/AddHeaderCacheListener.php",
    "content": "<?php\n\nnamespace Http\\Client\\Common\\Plugin\\Cache\\Listener;\n\nuse Psr\\Http\\Message\\RequestInterface;\nuse Psr\\Http\\Message\\ResponseInterface;\nuse Psr\\Cache\\CacheItemInterface;\n\n/**\n * Adds a header indicating if the response came from cache.\n *\n * @author Iain Connor <iainconnor@gmail.com>\n */\nclass AddHeaderCacheListener implements CacheListener\n{\n    /** @var string */\n    private $headerName;\n\n    /**\n     * @param string $headerName\n     */\n    public function __construct($headerName = 'X-Cache')\n    {\n        $this->headerName = $headerName;\n    }\n\n    /**\n     * Called before the cache plugin returns the response, with information on whether that response came from cache.\n     *\n     * @param bool                    $fromCache Whether the `$response` was from the cache or not.\n     *                                           Note that checking `$cacheItem->isHit()` is not sufficent to determine this.\n     * @param CacheItemInterface|null $cacheItem\n     *\n     * @return ResponseInterface\n     */\n    public function onCacheResponse(RequestInterface $request, ResponseInterface $response, $fromCache, $cacheItem)\n    {\n        return $response->withHeader($this->headerName, $fromCache ? 'HIT' : 'MISS');\n    }\n}\n"
  },
  {
    "path": "src/Cache/Listener/CacheListener.php",
    "content": "<?php\n\nnamespace Http\\Client\\Common\\Plugin\\Cache\\Listener;\n\nuse Psr\\Http\\Message\\RequestInterface;\nuse Psr\\Http\\Message\\ResponseInterface;\nuse Psr\\Cache\\CacheItemInterface;\n\n/**\n * Called by the cache plugin with information on the cache status.\n * Provides an opportunity to update the response based on whether the cache was a hit or a miss, or\n * other cache-meta-data.\n *\n * @author Iain Connor <iainconnor@gmail.com>\n */\ninterface CacheListener\n{\n    /**\n     * Called before the cache plugin returns the response, with information on whether that response came from cache.\n     *\n     * @param bool                    $fromCache Whether the `$response` was from the cache or not.\n     *                                           Note that checking `$cacheItem->isHit()` is not sufficent to determine this.\n     * @param CacheItemInterface|null $cacheItem\n     *\n     * @return ResponseInterface\n     */\n    public function onCacheResponse(RequestInterface $request, ResponseInterface $response, $fromCache, $cacheItem);\n}\n"
  },
  {
    "path": "src/CachePlugin.php",
    "content": "<?php\n\nnamespace Http\\Client\\Common\\Plugin;\n\nuse Http\\Client\\Common\\Plugin;\nuse Http\\Client\\Common\\Plugin\\Exception\\RewindStreamException;\nuse Http\\Client\\Common\\Plugin\\Cache\\Generator\\CacheKeyGenerator;\nuse Http\\Client\\Common\\Plugin\\Cache\\Generator\\SimpleGenerator;\nuse Http\\Promise\\FulfilledPromise;\nuse Http\\Promise\\Promise;\nuse Psr\\Cache\\CacheItemInterface;\nuse Psr\\Cache\\CacheItemPoolInterface;\nuse Psr\\Http\\Message\\RequestInterface;\nuse Psr\\Http\\Message\\ResponseInterface;\nuse Psr\\Http\\Message\\StreamFactoryInterface;\nuse Symfony\\Component\\OptionsResolver\\Options;\nuse Symfony\\Component\\OptionsResolver\\OptionsResolver;\n\n/**\n * Allow for caching a response with a PSR-6 compatible caching engine.\n *\n * It can follow the RFC-7234 caching specification or use a fixed cache lifetime.\n *\n * @author Tobias Nyholm <tobias.nyholm@gmail.com>\n */\nfinal class CachePlugin implements Plugin\n{\n    use VersionBridgePlugin;\n\n    /**\n     * @var CacheItemPoolInterface\n     */\n    private $pool;\n\n    /**\n     * @var StreamFactoryInterface\n     */\n    private $streamFactory;\n\n    /**\n     * @var mixed[]\n     */\n    private $config;\n\n    /**\n     * Cache directives indicating if a response can not be cached.\n     *\n     * @var string[]\n     */\n    private $noCacheFlags = ['no-cache', 'private', 'no-store'];\n\n    /**\n     * @param mixed[] $config\n     *\n     *     bool respect_cache_headers: Whether to look at the cache directives or ignore them\n     *     int default_ttl: (seconds) If we do not respect cache headers or can't calculate a good ttl, use this value\n     *     string hash_algo: The hashing algorithm to use when generating cache keys\n     *     int|null cache_lifetime: (seconds) To support serving a previous stale response when the server answers 304\n     *              we have to store the cache for a longer time than the server originally says it is valid for.\n     *              We store a cache item for $cache_lifetime + max age of the response.\n     *     string[] methods: list of request methods which can be cached\n     *     string[] blacklisted_paths: list of regex for URLs explicitly not to be cached\n     *     string[] respect_response_cache_directives: list of cache directives this plugin will respect while caching responses\n     *     CacheKeyGenerator cache_key_generator: an object to generate the cache key. Defaults to a new instance of SimpleGenerator\n     *     CacheListener[] cache_listeners: an array of objects to act on the response based on the results of the cache check.\n     *              Defaults to an empty array\n     * }\n     */\n    public function __construct(CacheItemPoolInterface $pool, StreamFactoryInterface $streamFactory, array $config = [])\n    {\n        $this->pool = $pool;\n        $this->streamFactory = $streamFactory;\n\n        if (\\array_key_exists('respect_cache_headers', $config) && \\array_key_exists('respect_response_cache_directives', $config)) {\n            throw new \\InvalidArgumentException('You can\\'t provide config option \"respect_cache_headers\" and \"respect_response_cache_directives\". Use \"respect_response_cache_directives\" instead.');\n        }\n\n        $optionsResolver = new OptionsResolver();\n        $this->configureOptions($optionsResolver);\n        $this->config = $optionsResolver->resolve($config);\n\n        if (null === $this->config['cache_key_generator']) {\n            $this->config['cache_key_generator'] = new SimpleGenerator();\n        }\n    }\n\n    /**\n     * This method will setup the cachePlugin in client cache mode. When using the client cache mode the plugin will\n     * cache responses with `private` cache directive.\n     *\n     * @param mixed[] $config For all possible config options see the constructor docs\n     *\n     * @return CachePlugin\n     */\n    public static function clientCache(CacheItemPoolInterface $pool, StreamFactoryInterface $streamFactory, array $config = [])\n    {\n        // Allow caching of private requests\n        if (\\array_key_exists('respect_response_cache_directives', $config)) {\n            $config['respect_response_cache_directives'][] = 'no-cache';\n            $config['respect_response_cache_directives'][] = 'max-age';\n            $config['respect_response_cache_directives'] = array_unique($config['respect_response_cache_directives']);\n        } else {\n            $config['respect_response_cache_directives'] = ['no-cache', 'max-age'];\n        }\n\n        return new self($pool, $streamFactory, $config);\n    }\n\n    /**\n     * This method will setup the cachePlugin in server cache mode. This is the default caching behavior it refuses to\n     * cache responses with the `private`or `no-cache` directives.\n     *\n     * @param mixed[] $config For all possible config options see the constructor docs\n     *\n     * @return CachePlugin\n     */\n    public static function serverCache(CacheItemPoolInterface $pool, StreamFactoryInterface $streamFactory, array $config = [])\n    {\n        return new self($pool, $streamFactory, $config);\n    }\n\n    /**\n     * {@inheritdoc}\n     *\n     * @return Promise Resolves a PSR-7 Response or fails with an Http\\Client\\Exception (The same as HttpAsyncClient)\n     */\n    protected function doHandleRequest(RequestInterface $request, callable $next, callable $first)\n    {\n        $method = strtoupper($request->getMethod());\n        // if the request not is cachable, move to $next\n        if (!in_array($method, $this->config['methods'])) {\n            return $next($request)->then(function (ResponseInterface $response) use ($request) {\n                $response = $this->handleCacheListeners($request, $response, false, null);\n\n                return $response;\n            });\n        }\n\n        // If we can cache the request\n        $key = $this->createCacheKey($request);\n        $cacheItem = $this->pool->getItem($key);\n\n        if ($cacheItem->isHit()) {\n            $data = $cacheItem->get();\n            if (is_array($data)) {\n                // The array_key_exists() is to be removed in 2.0.\n                if (array_key_exists('expiresAt', $data) && (null === $data['expiresAt'] || time() < $data['expiresAt'])) {\n                    // This item is still valid according to previous cache headers\n                    $response = $this->createResponseFromCacheItem($cacheItem);\n                    $response = $this->handleCacheListeners($request, $response, true, $cacheItem);\n\n                    return new FulfilledPromise($response);\n                }\n\n                // Add headers to ask the server if this cache is still valid\n                if ($modifiedSinceValue = $this->getModifiedSinceHeaderValue($cacheItem)) {\n                    $request = $request->withHeader('If-Modified-Since', $modifiedSinceValue);\n                }\n\n                if ($etag = $this->getETag($cacheItem)) {\n                    $request = $request->withHeader('If-None-Match', $etag);\n                }\n            }\n        }\n\n        return $next($request)->then(function (ResponseInterface $response) use ($request, $cacheItem) {\n            if (304 === $response->getStatusCode()) {\n                if (!$cacheItem->isHit()) {\n                    /*\n                     * We do not have the item in cache. This plugin did not add If-Modified-Since\n                     * or If-None-Match headers. Return the response from server.\n                     */\n                    return $this->handleCacheListeners($request, $response, false, $cacheItem);\n                }\n\n                // The cached response we have is still valid\n                $data = $cacheItem->get();\n                $maxAge = $this->getMaxAge($response);\n                $data['expiresAt'] = $this->calculateResponseExpiresAt($maxAge);\n                $cacheItem->set($data)->expiresAfter($this->calculateCacheItemExpiresAfter($maxAge));\n                $this->pool->save($cacheItem);\n\n                return $this->handleCacheListeners($request, $this->createResponseFromCacheItem($cacheItem), true, $cacheItem);\n            }\n\n            if ($this->isCacheable($response) && $this->isCacheableRequest($request)) {\n                /* The PSR-7 response body is a stream. We can't expect that the response implements Serializable and handles the body.\n                 * Therefore we store the body separately and detach the stream to avoid attempting to serialize a resource.\n                .* Our implementation still makes the assumption that the response object apart from the body can be serialized and deserialized.\n                 */\n                $bodyStream = $response->getBody();\n                $body = $bodyStream->__toString();\n                $bodyStream->detach();\n\n                $maxAge = $this->getMaxAge($response);\n                $cacheItem\n                    ->expiresAfter($this->calculateCacheItemExpiresAfter($maxAge))\n                    ->set([\n                        'response' => $response,\n                        'body' => $body,\n                        'expiresAt' => $this->calculateResponseExpiresAt($maxAge),\n                        'createdAt' => time(),\n                        'etag' => $response->getHeader('ETag'),\n                    ]);\n                $this->pool->save($cacheItem);\n\n                $bodyStream = $this->streamFactory->createStream($body);\n                if ($bodyStream->isSeekable()) {\n                    $bodyStream->rewind();\n                }\n\n                $response = $response->withBody($bodyStream);\n            }\n\n            return $this->handleCacheListeners($request, $response, false, $cacheItem);\n        });\n    }\n\n    /**\n     * Calculate the timestamp when this cache item should be dropped from the cache. The lowest value that can be\n     * returned is $maxAge.\n     *\n     * @return int|null Unix system time passed to the PSR-6 cache\n     */\n    private function calculateCacheItemExpiresAfter(?int $maxAge): ?int\n    {\n        if (null === $this->config['cache_lifetime'] && null === $maxAge) {\n            return null;\n        }\n\n        return ($this->config['cache_lifetime'] ?: 0) + ($maxAge ?: 0);\n    }\n\n    /**\n     * Calculate the timestamp when a response expires. After that timestamp, we need to send a\n     * If-Modified-Since / If-None-Match request to validate the response.\n     *\n     * @return int|null Unix system time. A null value means that the response expires when the cache item expires\n     */\n    private function calculateResponseExpiresAt(?int $maxAge): ?int\n    {\n        if (null === $maxAge) {\n            return null;\n        }\n\n        return time() + $maxAge;\n    }\n\n    /**\n     * Verify that we can cache this response.\n     *\n     * @return bool\n     */\n    protected function isCacheable(ResponseInterface $response)\n    {\n        if (!in_array($response->getStatusCode(), [200, 203, 300, 301, 302, 404, 410])) {\n            return false;\n        }\n\n        $nocacheDirectives = array_intersect($this->config['respect_response_cache_directives'], $this->noCacheFlags);\n        foreach ($nocacheDirectives as $nocacheDirective) {\n            if ($this->getCacheControlDirective($response, $nocacheDirective)) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    /**\n     * Verify that we can cache this request.\n     */\n    private function isCacheableRequest(RequestInterface $request): bool\n    {\n        $uri = $request->getUri()->__toString();\n        foreach ($this->config['blacklisted_paths'] as $regex) {\n            if (1 === preg_match($regex, $uri)) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    /**\n     * Get the value of a parameter in the cache control header.\n     *\n     * @param string $name The field of Cache-Control to fetch\n     *\n     * @return bool|string The value of the directive, true if directive without value, false if directive not present\n     */\n    private function getCacheControlDirective(ResponseInterface $response, string $name)\n    {\n        $headers = $response->getHeader('Cache-Control');\n        foreach ($headers as $header) {\n            if (preg_match(sprintf('|%s=?([0-9]+)?|i', $name), $header, $matches)) {\n                // return the value for $name if it exists\n                if (isset($matches[1])) {\n                    return $matches[1];\n                }\n\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    private function createCacheKey(RequestInterface $request): string\n    {\n        $key = $this->config['cache_key_generator']->generate($request);\n\n        return hash($this->config['hash_algo'], $key);\n    }\n\n    /**\n     * Get a ttl in seconds.\n     *\n     * Returns null if we do not respect cache headers and got no defaultTtl.\n     */\n    private function getMaxAge(ResponseInterface $response): ?int\n    {\n        if (!in_array('max-age', $this->config['respect_response_cache_directives'], true)) {\n            return $this->config['default_ttl'];\n        }\n\n        // check for max age in the Cache-Control header\n        $maxAge = $this->getCacheControlDirective($response, 'max-age');\n        if (!is_bool($maxAge)) {\n            $ageHeaders = $response->getHeader('Age');\n            foreach ($ageHeaders as $age) {\n                return ((int) $maxAge) - ((int) $age);\n            }\n\n            return (int) $maxAge;\n        }\n\n        // check for ttl in the Expires header\n        $headers = $response->getHeader('Expires');\n        foreach ($headers as $header) {\n            return (new \\DateTime($header))->getTimestamp() - (new \\DateTime())->getTimestamp();\n        }\n\n        return $this->config['default_ttl'];\n    }\n\n    /**\n     * Configure an options resolver.\n     */\n    private function configureOptions(OptionsResolver $resolver): void\n    {\n        $resolver->setDefaults([\n            'cache_lifetime' => 86400 * 30, // 30 days\n            'default_ttl' => 0,\n            // Deprecated as of v1.3, to be removed in v2.0. Use respect_response_cache_directives instead\n            'respect_cache_headers' => null,\n            'hash_algo' => 'sha1',\n            'methods' => ['GET', 'HEAD'],\n            'respect_response_cache_directives' => ['no-cache', 'private', 'max-age', 'no-store'],\n            'cache_key_generator' => null,\n            'cache_listeners' => [],\n            'blacklisted_paths' => [],\n        ]);\n\n        $resolver->setAllowedTypes('cache_lifetime', ['int', 'null']);\n        $resolver->setAllowedTypes('default_ttl', ['int', 'null']);\n        $resolver->setAllowedTypes('respect_cache_headers', ['bool', 'null']);\n        $resolver->setAllowedTypes('methods', 'array');\n        $resolver->setAllowedTypes('cache_key_generator', ['null', CacheKeyGenerator::class]);\n        $resolver->setAllowedTypes('blacklisted_paths', 'array');\n        $resolver->setAllowedValues('hash_algo', hash_algos());\n        $resolver->setAllowedValues('methods', function ($value) {\n            /* RFC7230 sections 3.1.1 and 3.2.6 except limited to uppercase characters. */\n            $matches = preg_grep('/[^A-Z0-9!#$%&\\'*+\\-.^_`|~]/', $value);\n\n            return empty($matches);\n        });\n        $resolver->setAllowedTypes('cache_listeners', ['array']);\n\n        $resolver->setNormalizer('respect_cache_headers', function (Options $options, $value) {\n            if (null !== $value) {\n                @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);\n            }\n\n            return null === $value ? true : $value;\n        });\n\n        $resolver->setNormalizer('respect_response_cache_directives', function (Options $options, $value) {\n            if (false === $options['respect_cache_headers']) {\n                return [];\n            }\n\n            return $value;\n        });\n    }\n\n    private function createResponseFromCacheItem(CacheItemInterface $cacheItem): ResponseInterface\n    {\n        $data = $cacheItem->get();\n\n        /** @var ResponseInterface $response */\n        $response = $data['response'];\n        $stream = $this->streamFactory->createStream($data['body']);\n\n        try {\n            $stream->rewind();\n        } catch (\\Exception $e) {\n            throw new RewindStreamException('Cannot rewind stream.', 0, $e);\n        }\n\n        return $response->withBody($stream);\n    }\n\n    /**\n     * Get the value for the \"If-Modified-Since\" header.\n     */\n    private function getModifiedSinceHeaderValue(CacheItemInterface $cacheItem): ?string\n    {\n        $data = $cacheItem->get();\n        // The isset() is to be removed in 2.0.\n        if (!isset($data['createdAt'])) {\n            return null;\n        }\n\n        $modified = new \\DateTime('@'.$data['createdAt']);\n        $modified->setTimezone(new \\DateTimeZone('GMT'));\n\n        return sprintf('%s GMT', $modified->format('l, d-M-y H:i:s'));\n    }\n\n    /**\n     * Get the ETag from the cached response.\n     */\n    private function getETag(CacheItemInterface $cacheItem): ?string\n    {\n        $data = $cacheItem->get();\n        // The isset() is to be removed in 2.0.\n        if (!isset($data['etag'])) {\n            return null;\n        }\n\n        foreach ($data['etag'] as $etag) {\n            if (!empty($etag)) {\n                return $etag;\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Call the registered cache listeners.\n     */\n    private function handleCacheListeners(RequestInterface $request, ResponseInterface $response, bool $cacheHit, ?CacheItemInterface $cacheItem): ResponseInterface\n    {\n        foreach ($this->config['cache_listeners'] as $cacheListener) {\n            $response = $cacheListener->onCacheResponse($request, $response, $cacheHit, $cacheItem);\n        }\n\n        return $response;\n    }\n}\n"
  },
  {
    "path": "src/Exception/RewindStreamException.php",
    "content": "<?php\n\nnamespace Http\\Client\\Common\\Plugin\\Exception;\n\nuse Http\\Client\\Exception;\n\n/**\n * @author Théo FIDRY <theo.fidry@gmail.com>\n */\nclass RewindStreamException extends \\RuntimeException implements Exception\n{\n}\n"
  },
  {
    "path": "tests/Cache/CachePluginTest.php",
    "content": "<?php\n\nnamespace Http\\Client\\Common\\Plugin\\Tests\\Cache;\n\nuse Http\\Client\\Common\\Plugin;\nuse Http\\Client\\Common\\Plugin\\Cache\\Generator\\SimpleGenerator;\nuse Http\\Client\\Common\\Plugin\\CachePlugin;\nuse Http\\Promise\\FulfilledPromise;\nuse PHPUnit\\Framework\\Constraint\\Callback;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Cache\\CacheItemInterface;\nuse Psr\\Cache\\CacheItemPoolInterface;\nuse Psr\\Http\\Message\\RequestInterface;\nuse Psr\\Http\\Message\\ResponseInterface;\nuse Psr\\Http\\Message\\StreamFactoryInterface;\nuse Psr\\Http\\Message\\StreamInterface;\nuse Psr\\Http\\Message\\UriInterface;\nuse Symfony\\Component\\OptionsResolver\\Exception\\InvalidOptionsException;\n\nclass CachePluginTest extends TestCase\n{\n    private function createPlugin(CacheItemPoolInterface $pool, StreamFactoryInterface $streamFactory, array $config = []): CachePlugin\n    {\n        $defaults = [\n            'default_ttl' => 60,\n            'cache_lifetime' => 1000,\n        ];\n\n        return new CachePlugin($pool, $streamFactory, array_merge($defaults, $config));\n    }\n\n    private function cacheItemConstraint(array $expected): Callback\n    {\n        return $this->callback(function ($actual) use ($expected) {\n            if (!is_array($actual)) {\n                return false;\n            }\n\n            foreach ($expected as $key => $value) {\n                if (!array_key_exists($key, $actual)) {\n                    return false;\n                }\n\n                if (in_array($key, ['expiresAt', 'createdAt'], true)) {\n                    continue;\n                }\n\n                if ($actual[$key] !== $value) {\n                    return false;\n                }\n            }\n\n            return true;\n        });\n    }\n\n    private function createFulfilledNext(ResponseInterface $response): callable\n    {\n        return function (RequestInterface $request) use ($response) {\n            return new FulfilledPromise($response);\n        };\n    }\n\n    public function testInterface(): void\n    {\n        $plugin = $this->createPlugin(\n            $this->createMock(CacheItemPoolInterface::class),\n            $this->createMock(StreamFactoryInterface::class)\n        );\n\n        self::assertInstanceOf(Plugin::class, $plugin);\n    }\n\n    public function testCacheResponses(): void\n    {\n        $httpBody = 'body';\n\n        $stream = $this->createMock(StreamInterface::class);\n        $stream->method('__toString')->willReturn($httpBody);\n        $stream->method('isSeekable')->willReturn(true);\n        $stream->expects($this->once())->method('rewind');\n        $stream->expects($this->once())->method('detach');\n\n        $streamFactory = $this->createMock(StreamFactoryInterface::class);\n        $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream);\n\n        $uri = $this->createMock(UriInterface::class);\n        $uri->method('__toString')->willReturn('https://example.com/');\n\n        $request = $this->createMock(RequestInterface::class);\n        $request->method('getMethod')->willReturn('GET');\n        $request->method('getUri')->willReturn($uri);\n        $request->method('getBody')->willReturn($stream);\n\n        $response = $this->createMock(ResponseInterface::class);\n        $response->method('getStatusCode')->willReturn(200);\n        $response->method('getBody')->willReturn($stream);\n        $response->method('getHeader')->willReturn([]);\n        $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf();\n\n        $item = $this->createMock(CacheItemInterface::class);\n        $item->method('isHit')->willReturn(false);\n        $item->expects($this->once())->method('expiresAfter')->with(1060)->willReturnSelf();\n        $item->expects($this->once())->method('set')->with($this->cacheItemConstraint([\n            'response' => $response,\n            'body' => $httpBody,\n            'expiresAt' => 0,\n            'createdAt' => 0,\n            'etag' => [],\n        ]))->willReturnSelf();\n\n        $pool = $this->createMock(CacheItemPoolInterface::class);\n        $pool->expects($this->once())->method('getItem')->with($this->anything())->willReturn($item);\n        $pool->expects($this->once())->method('save')->with($item);\n\n        $plugin = $this->createPlugin($pool, $streamFactory);\n        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {\n        })->wait();\n\n        self::assertSame($response, $result);\n    }\n\n    public function testDoNotStoreFailedResponses(): void\n    {\n        $item = $this->createMock(CacheItemInterface::class);\n        $item->method('isHit')->willReturn(false);\n\n        $pool = $this->createMock(CacheItemPoolInterface::class);\n        $pool->expects($this->once())->method('getItem')->with($this->anything())->willReturn($item);\n        $pool->expects($this->never())->method('save');\n\n        $uri = $this->createMock(UriInterface::class);\n        $uri->method('__toString')->willReturn('https://example.com/');\n\n        $requestBody = $this->createMock(StreamInterface::class);\n        $requestBody->method('__toString')->willReturn('body');\n\n        $request = $this->createMock(RequestInterface::class);\n        $request->method('getMethod')->willReturn('GET');\n        $request->method('getUri')->willReturn($uri);\n        $request->method('getBody')->willReturn($requestBody);\n\n        $response = $this->createMock(ResponseInterface::class);\n        $response->method('getStatusCode')->willReturn(400);\n\n        $plugin = $this->createPlugin($pool, $this->createMock(StreamFactoryInterface::class));\n\n        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {\n        })->wait();\n\n        self::assertSame($response, $result);\n    }\n\n    public function testDoNotStorePostRequestsByDefault(): void\n    {\n        $pool = $this->createMock(CacheItemPoolInterface::class);\n        $pool->expects($this->never())->method('getItem');\n\n        $request = $this->createMock(RequestInterface::class);\n        $request->method('getMethod')->willReturn('POST');\n\n        $response = $this->createMock(ResponseInterface::class);\n\n        $plugin = $this->createPlugin($pool, $this->createMock(StreamFactoryInterface::class));\n\n        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {\n        })->wait();\n\n        self::assertSame($response, $result);\n    }\n\n    public function testStorePostRequestsWhenAllowed(): void\n    {\n        $httpBody = 'hello=world';\n\n        $stream = $this->createMock(StreamInterface::class);\n        $stream->method('__toString')->willReturn($httpBody);\n        $stream->method('isSeekable')->willReturn(true);\n        $stream->expects($this->once())->method('rewind');\n        $stream->expects($this->once())->method('detach');\n\n        $streamFactory = $this->createMock(StreamFactoryInterface::class);\n        $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream);\n\n        $uri = $this->createMock(UriInterface::class);\n        $uri->method('__toString')->willReturn('https://example.com/');\n\n        $request = $this->createMock(RequestInterface::class);\n        $request->method('getMethod')->willReturn('POST');\n        $request->method('getUri')->willReturn($uri);\n        $request->method('getBody')->willReturn($stream);\n\n        $response = $this->createMock(ResponseInterface::class);\n        $response->method('getStatusCode')->willReturn(200);\n        $response->method('getBody')->willReturn($stream);\n        $response->method('getHeader')->willReturn([]);\n        $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf();\n\n        $item = $this->createMock(CacheItemInterface::class);\n        $item->method('isHit')->willReturn(false);\n        $item->expects($this->once())->method('expiresAfter')->with(1060)->willReturnSelf();\n        $item->expects($this->once())->method('set')->with($this->cacheItemConstraint([\n            'response' => $response,\n            'body' => $httpBody,\n            'expiresAt' => 0,\n            'createdAt' => 0,\n            'etag' => [],\n        ]))->willReturnSelf();\n\n        $pool = $this->createMock(CacheItemPoolInterface::class);\n        $pool->expects($this->once())->method('getItem')->willReturn($item);\n        $pool->expects($this->once())->method('save')->with($item);\n\n        $plugin = $this->createPlugin($pool, $streamFactory, [\n            'methods' => ['GET', 'HEAD', 'POST'],\n        ]);\n\n        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {\n        })->wait();\n\n        self::assertSame($response, $result);\n    }\n\n    /**\n     * @dataProvider invalidMethodProvider\n     */\n    public function testDoNotAllowInvalidRequestMethods(array $methods): void\n    {\n        $this->expectException(InvalidOptionsException::class);\n\n        $this->createPlugin(\n            $this->createMock(CacheItemPoolInterface::class),\n            $this->createMock(StreamFactoryInterface::class),\n            [\n                'methods' => $methods,\n            ]\n        );\n    }\n\n    public function invalidMethodProvider(): array\n    {\n        return [\n            [['GET', 'HEAD', 'POST ']],\n            [['GET', 'HEAD\"', 'POST']],\n            [['GET', 'head', 'POST']],\n        ];\n    }\n\n    public function testCalculateAgeFromResponse(): void\n    {\n        $httpBody = 'body';\n\n        $stream = $this->createMock(StreamInterface::class);\n        $stream->method('__toString')->willReturn($httpBody);\n        $stream->method('isSeekable')->willReturn(true);\n        $stream->expects($this->once())->method('rewind');\n        $stream->expects($this->once())->method('detach');\n\n        $streamFactory = $this->createMock(StreamFactoryInterface::class);\n        $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream);\n\n        $uri = $this->createMock(UriInterface::class);\n        $uri->method('__toString')->willReturn('https://example.com/');\n\n        $request = $this->createMock(RequestInterface::class);\n        $request->method('getMethod')->willReturn('GET');\n        $request->method('getUri')->willReturn($uri);\n        $request->method('getBody')->willReturn($stream);\n\n        $response = $this->createMock(ResponseInterface::class);\n        $response->method('getStatusCode')->willReturn(200);\n        $response->method('getBody')->willReturn($stream);\n        $response->method('getHeader')->willReturnCallback(function ($header) {\n            if ('Cache-Control' === $header) {\n                return ['max-age=40'];\n            }\n\n            if ('Age' === $header) {\n                return ['15'];\n            }\n\n            return [];\n        });\n        $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf();\n\n        $item = $this->createMock(CacheItemInterface::class);\n        $item->method('isHit')->willReturn(false);\n        $item->expects($this->once())->method('set')->with($this->cacheItemConstraint([\n            'response' => $response,\n            'body' => $httpBody,\n            'expiresAt' => 0,\n            'createdAt' => 0,\n            'etag' => [],\n        ]))->willReturnSelf();\n        $item->expects($this->once())->method('expiresAfter')->with(1025)->willReturnSelf();\n\n        $pool = $this->createMock(CacheItemPoolInterface::class);\n        $pool->method('getItem')->willReturn($item);\n        $pool->expects($this->once())->method('save')->with($item);\n\n        $plugin = $this->createPlugin($pool, $streamFactory);\n\n        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {\n        })->wait();\n\n        self::assertSame($response, $result);\n    }\n\n    public function testSaveEtag(): void\n    {\n        $httpBody = 'body';\n\n        $stream = $this->createMock(StreamInterface::class);\n        $stream->method('__toString')->willReturn($httpBody);\n        $stream->method('isSeekable')->willReturn(true);\n        $stream->expects($this->once())->method('rewind');\n        $stream->expects($this->once())->method('detach');\n\n        $streamFactory = $this->createMock(StreamFactoryInterface::class);\n        $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream);\n\n        $uri = $this->createMock(UriInterface::class);\n        $uri->method('__toString')->willReturn('https://example.com/');\n\n        $request = $this->createMock(RequestInterface::class);\n        $request->method('getBody')->willReturn($stream);\n        $request->method('getMethod')->willReturn('GET');\n        $request->method('getUri')->willReturn($uri);\n\n        $response = $this->createMock(ResponseInterface::class);\n        $response->method('getStatusCode')->willReturn(200);\n        $response->method('getBody')->willReturn($stream);\n        $response->method('getHeader')->willReturnCallback(function ($header) {\n            if ('ETag' === $header) {\n                return ['foo_etag'];\n            }\n\n            return [];\n        });\n        $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf();\n\n        $item = $this->createMock(CacheItemInterface::class);\n        $item->method('isHit')->willReturn(false);\n        $item->expects($this->once())->method('expiresAfter')->with(1060)->willReturnSelf();\n        $item->expects($this->once())->method('set')->with($this->cacheItemConstraint([\n            'response' => $response,\n            'body' => $httpBody,\n            'expiresAt' => 0,\n            'createdAt' => 0,\n            'etag' => ['foo_etag'],\n        ]))->willReturnSelf();\n\n        $pool = $this->createMock(CacheItemPoolInterface::class);\n        $pool->expects($this->once())->method('getItem')->willReturn($item);\n        $pool->expects($this->once())->method('save')->with($item);\n\n        $plugin = $this->createPlugin($pool, $streamFactory);\n\n        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {\n        })->wait();\n\n        self::assertSame($response, $result);\n    }\n\n    public function testAddEtagAndModifiedSinceToRequest(): void\n    {\n        $httpBody = 'body';\n\n        $stream = $this->createMock(StreamInterface::class);\n        $stream->method('__toString')->willReturn('');\n\n        $streamFactory = $this->createMock(StreamFactoryInterface::class);\n        $streamFactory->expects($this->never())->method('createStream');\n\n        $uri = $this->createMock(UriInterface::class);\n        $uri->method('__toString')->willReturn('https://example.com/');\n\n        $request = $this->createMock(RequestInterface::class);\n        $request->method('getMethod')->willReturn('GET');\n        $request->method('getUri')->willReturn($uri);\n        $request->method('getBody')->willReturn($stream);\n        $request->expects($this->exactly(2))\n            ->method('withHeader')\n            ->withConsecutive(\n                ['If-Modified-Since', 'Thursday, 01-Jan-70 01:18:31 GMT'],\n                ['If-None-Match', 'foo_etag']\n            )\n            ->willReturnSelf();\n\n        $response = $this->createMock(ResponseInterface::class);\n        $response->method('getStatusCode')->willReturn(304);\n\n        $item = $this->createMock(CacheItemInterface::class);\n        $item->expects($this->exactly(2))->method('isHit')->willReturnOnConsecutiveCalls(true, false);\n        $item->method('get')->willReturn([\n            'response' => $response,\n            'body' => $httpBody,\n            'expiresAt' => 0,\n            'createdAt' => 4711,\n            'etag' => ['foo_etag'],\n        ]);\n\n        $pool = $this->createMock(CacheItemPoolInterface::class);\n        $pool->expects($this->once())->method('getItem')->willReturn($item);\n\n        $plugin = $this->createPlugin($pool, $streamFactory);\n\n        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {\n        })->wait();\n\n        self::assertSame($response, $result);\n    }\n\n    public function testServeCachedResponse(): void\n    {\n        $httpBody = 'body';\n\n        $stream = $this->createMock(StreamInterface::class);\n\n        $streamFactory = $this->createMock(StreamFactoryInterface::class);\n        $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream);\n\n        $uri = $this->createMock(UriInterface::class);\n        $uri->method('__toString')->willReturn('https://example.com/');\n\n        $requestBody = $this->createMock(StreamInterface::class);\n        $requestBody->method('__toString')->willReturn('');\n\n        $request = $this->createMock(RequestInterface::class);\n        $request->method('getMethod')->willReturn('GET');\n        $request->method('getUri')->willReturn($uri);\n        $request->method('getBody')->willReturn($requestBody);\n\n        $response = $this->createMock(ResponseInterface::class);\n        $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf();\n\n        $item = $this->createMock(CacheItemInterface::class);\n        $item->method('isHit')->willReturn(true);\n        $item->method('get')->willReturn([\n            'response' => $response,\n            'body' => $httpBody,\n            'expiresAt' => time() + 1000000,\n            'createdAt' => 4711,\n            'etag' => [],\n        ]);\n\n        $pool = $this->createMock(CacheItemPoolInterface::class);\n        $pool->expects($this->once())->method('getItem')->willReturn($item);\n\n        $plugin = $this->createPlugin($pool, $streamFactory);\n\n        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {\n        })->wait();\n\n        self::assertSame($response, $result);\n    }\n\n    public function testServeAndResaveExpiredResponse(): void\n    {\n        $httpBody = 'body';\n\n        $uri = $this->createMock(UriInterface::class);\n        $uri->method('__toString')->willReturn('https://example.com/');\n\n        $requestStream = $this->createMock(StreamInterface::class);\n        $requestStream->method('__toString')->willReturn('');\n\n        $request = $this->createMock(RequestInterface::class);\n        $request->method('getMethod')->willReturn('GET');\n        $request->method('getUri')->willReturn($uri);\n        $request->method('getBody')->willReturn($requestStream);\n        $request->method('withHeader')->willReturnSelf();\n\n        $stream = $this->createMock(StreamInterface::class);\n        $streamFactory = $this->createMock(StreamFactoryInterface::class);\n        $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream);\n\n        $response = $this->createMock(ResponseInterface::class);\n        $response->method('getStatusCode')->willReturn(304);\n        $response->method('getHeader')->willReturn([]);\n        $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf();\n\n        $item = $this->createMock(CacheItemInterface::class);\n        $item->method('isHit')->willReturn(true);\n        $item->expects($this->once())->method('expiresAfter')->with(1060)->willReturnSelf();\n        $item->method('get')->willReturn([\n            'response' => $response,\n            'body' => $httpBody,\n            'expiresAt' => 0,\n            'createdAt' => 4711,\n            'etag' => ['foo_etag'],\n        ]);\n        $item->expects($this->once())->method('set')->with($this->cacheItemConstraint([\n            'response' => $response,\n            'body' => $httpBody,\n            'expiresAt' => 0,\n            'createdAt' => 0,\n            'etag' => ['foo_etag'],\n        ]))->willReturnSelf();\n\n        $pool = $this->createMock(CacheItemPoolInterface::class);\n        $pool->expects($this->once())->method('getItem')->willReturn($item);\n        $pool->expects($this->once())->method('save')->with($item);\n\n        $plugin = $this->createPlugin($pool, $streamFactory);\n\n        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {\n        })->wait();\n\n        self::assertSame($response, $result);\n    }\n\n    public function testCachePrivateResponsesWhenAllowed(): void\n    {\n        $httpBody = 'body';\n\n        $stream = $this->createMock(StreamInterface::class);\n        $stream->method('__toString')->willReturn($httpBody);\n        $stream->method('isSeekable')->willReturn(true);\n        $stream->expects($this->once())->method('rewind');\n        $stream->expects($this->once())->method('detach');\n\n        $streamFactory = $this->createMock(StreamFactoryInterface::class);\n        $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream);\n\n        $uri = $this->createMock(UriInterface::class);\n        $uri->method('__toString')->willReturn('https://example.com/');\n\n        $request = $this->createMock(RequestInterface::class);\n        $request->method('getMethod')->willReturn('GET');\n        $request->method('getUri')->willReturn($uri);\n        $request->method('getBody')->willReturn($stream);\n\n        $response = $this->createMock(ResponseInterface::class);\n        $response->method('getStatusCode')->willReturn(200);\n        $response->method('getBody')->willReturn($stream);\n        $response->method('getHeader')->willReturnCallback(function ($header) {\n            if ('Cache-Control' === $header) {\n                return ['private'];\n            }\n\n            return [];\n        });\n        $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf();\n\n        $item = $this->createMock(CacheItemInterface::class);\n        $item->method('isHit')->willReturn(false);\n        $item->expects($this->once())->method('expiresAfter')->with(1060)->willReturnSelf();\n        $item->expects($this->once())->method('set')->with($this->cacheItemConstraint([\n            'response' => $response,\n            'body' => $httpBody,\n            'expiresAt' => 0,\n            'createdAt' => 0,\n            'etag' => [],\n        ]))->willReturnSelf();\n\n        $pool = $this->createMock(CacheItemPoolInterface::class);\n        $pool->expects($this->once())->method('getItem')->willReturn($item);\n        $pool->expects($this->once())->method('save')->with($item);\n\n        $plugin = CachePlugin::clientCache($pool, $streamFactory, [\n            'default_ttl' => 60,\n            'cache_lifetime' => 1000,\n        ]);\n\n        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {\n        })->wait();\n\n        self::assertSame($response, $result);\n    }\n\n    public function testDoNotStoreResponsesOfRequestsToBlacklistedPaths(): void\n    {\n        $httpBody = 'body';\n\n        $stream = $this->createMock(StreamInterface::class);\n        $stream->method('__toString')->willReturn($httpBody);\n        $stream->method('isSeekable')->willReturn(true);\n\n        $streamFactory = $this->createMock(StreamFactoryInterface::class);\n\n        $uri = $this->createMock(UriInterface::class);\n        $uri->method('__toString')->willReturn('https://example.com/foo');\n\n        $request = $this->createMock(RequestInterface::class);\n        $request->method('getMethod')->willReturn('GET');\n        $request->method('getUri')->willReturn($uri);\n        $request->method('getBody')->willReturn($stream);\n\n        $response = $this->createMock(ResponseInterface::class);\n        $response->method('getStatusCode')->willReturn(200);\n        $response->method('getBody')->willReturn($stream);\n        $response->method('getHeader')->willReturn([]);\n\n        $item = $this->createMock(CacheItemInterface::class);\n        $item->method('isHit')->willReturn(false);\n        $item->expects($this->never())->method('set');\n\n        $pool = $this->createMock(CacheItemPoolInterface::class);\n        $pool->expects($this->once())->method('getItem')->willReturn($item);\n        $pool->expects($this->never())->method('save');\n\n        $plugin = CachePlugin::clientCache($pool, $streamFactory, [\n            'default_ttl' => 60,\n            'cache_lifetime' => 1000,\n            'blacklisted_paths' => ['@/foo@'],\n        ]);\n\n        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {\n        })->wait();\n\n        self::assertSame($response, $result);\n    }\n\n    public function testStoreResponsesOfRequestsNotInBlacklistedPaths(): void\n    {\n        $httpBody = 'body';\n\n        $stream = $this->createMock(StreamInterface::class);\n        $stream->method('__toString')->willReturn($httpBody);\n        $stream->method('isSeekable')->willReturn(true);\n        $stream->expects($this->once())->method('rewind');\n        $stream->expects($this->once())->method('detach');\n\n        $streamFactory = $this->createMock(StreamFactoryInterface::class);\n        $streamFactory->expects($this->once())->method('createStream')->with($httpBody)->willReturn($stream);\n\n        $uri = $this->createMock(UriInterface::class);\n        $uri->method('__toString')->willReturn('https://example.com/');\n\n        $request = $this->createMock(RequestInterface::class);\n        $request->method('getMethod')->willReturn('GET');\n        $request->method('getUri')->willReturn($uri);\n        $request->method('getBody')->willReturn($stream);\n\n        $response = $this->createMock(ResponseInterface::class);\n        $response->method('getStatusCode')->willReturn(200);\n        $response->method('getBody')->willReturn($stream);\n        $response->method('getHeader')->willReturn([]);\n        $response->expects($this->once())->method('withBody')->with($stream)->willReturnSelf();\n\n        $item = $this->createMock(CacheItemInterface::class);\n        $item->method('isHit')->willReturn(false);\n        $item->expects($this->once())->method('expiresAfter')->with(1060)->willReturnSelf();\n        $item->expects($this->once())->method('set')->with($this->cacheItemConstraint([\n            'response' => $response,\n            'body' => $httpBody,\n            'expiresAt' => 0,\n            'createdAt' => 0,\n            'etag' => [],\n        ]))->willReturnSelf();\n\n        $pool = $this->createMock(CacheItemPoolInterface::class);\n        $pool->expects($this->once())->method('getItem')->willReturn($item);\n        $pool->expects($this->once())->method('save')->with($item);\n\n        $plugin = CachePlugin::clientCache($pool, $streamFactory, [\n            'default_ttl' => 60,\n            'cache_lifetime' => 1000,\n            'blacklisted_paths' => ['@/foo@'],\n        ]);\n\n        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {\n        })->wait();\n\n        self::assertSame($response, $result);\n    }\n\n    public function testCustomCacheKeyGenerator(): void\n    {\n        $stream = $this->createMock(StreamInterface::class);\n        $stream->expects($this->once())->method('rewind');\n        $streamFactory = $this->createMock(StreamFactoryInterface::class);\n        $streamFactory->expects($this->once())->method('createStream')->willReturn($stream);\n\n        $request = $this->createMock(RequestInterface::class);\n        $request->method('getMethod')->willReturn('GET');\n\n        $response = $this->createMock(ResponseInterface::class);\n        $response->expects($this->once())->method('withBody')->willReturnSelf();\n\n        $item = $this->createMock(CacheItemInterface::class);\n        $item->method('isHit')->willReturn(true);\n        $item->method('get')->willReturn([\n            'response' => $response,\n            'body' => 'body',\n            'expiresAt' => null,\n            'createdAt' => 0,\n            'etag' => [],\n        ]);\n\n        $pool = $this->createMock(CacheItemPoolInterface::class);\n        $pool->expects($this->once())->method('getItem')->willReturn($item);\n\n        $generator = $this->createMock(SimpleGenerator::class);\n        $generator->expects($this->once())->method('generate')->with($request)->willReturn('foo');\n\n        $plugin = CachePlugin::clientCache($pool, $streamFactory, [\n            'cache_key_generator' => $generator,\n        ]);\n\n        $result = $plugin->handleRequest($request, $this->createFulfilledNext($response), function () {\n        })->wait();\n\n        self::assertSame($response, $result);\n    }\n}\n"
  },
  {
    "path": "tests/Cache/Generator/HeaderCacheKeyGeneratorTest.php",
    "content": "<?php\n\nnamespace Http\\Client\\Common\\Plugin\\Tests\\Cache\\Generator;\n\nuse Http\\Client\\Common\\Plugin\\Cache\\Generator\\CacheKeyGenerator;\nuse Http\\Client\\Common\\Plugin\\Cache\\Generator\\HeaderCacheKeyGenerator;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Http\\Message\\RequestInterface;\nuse Psr\\Http\\Message\\StreamInterface;\nuse Psr\\Http\\Message\\UriInterface;\n\nclass HeaderCacheKeyGeneratorTest extends TestCase\n{\n    public function testInterface(): void\n    {\n        $this->assertInstanceOf(CacheKeyGenerator::class, new HeaderCacheKeyGenerator(['Authorization', 'Content-Type']));\n    }\n\n    public function testGenerateCacheFromRequest(): void\n    {\n        $uri = $this->createMock(UriInterface::class);\n        $uri->expects($this->once())->method('__toString')->willReturn('http://example.com/foo');\n\n        $body = $this->createMock(StreamInterface::class);\n        $body->expects($this->once())->method('__toString')->willReturn('');\n\n        $request = $this->createMock(RequestInterface::class);\n        $request->expects($this->once())->method('getMethod')->willReturn('GET');\n        $request->expects($this->once())->method('getUri')->willReturn($uri);\n        $request->expects($this->exactly(2))->method('getHeaderLine')->willReturnMap([\n            ['Authorization', 'bar'],\n            ['Content-Type', 'application/baz'],\n        ]);\n        $request->expects($this->once())->method('getBody')->willReturn($body);\n\n        $generator = new HeaderCacheKeyGenerator(['Authorization', 'Content-Type']);\n\n        $this->assertSame(\n            'GET http://example.com/foo Authorization:\"bar\" Content-Type:\"application/baz\" ',\n            $generator->generate($request)\n        );\n    }\n}\n"
  },
  {
    "path": "tests/Cache/Generator/SimpleGeneratorTest.php",
    "content": "<?php\n\nnamespace Http\\Client\\Common\\Plugin\\Tests\\Cache\\Generator;\n\nuse Http\\Client\\Common\\Plugin\\Cache\\Generator\\CacheKeyGenerator;\nuse Http\\Client\\Common\\Plugin\\Cache\\Generator\\SimpleGenerator;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Http\\Message\\RequestInterface;\nuse Psr\\Http\\Message\\StreamInterface;\nuse Psr\\Http\\Message\\UriInterface;\n\nclass SimpleGeneratorTest extends TestCase\n{\n    public function testInterface(): void\n    {\n        $generator = new SimpleGenerator();\n        $this->assertInstanceOf(CacheKeyGenerator::class, $generator);\n    }\n\n    public function testGenerateCacheFromRequest(): void\n    {\n        $uri = $this->createMock(UriInterface::class);\n        $uri->expects($this->once())->method('__toString')->willReturn('http://example.com/foo');\n\n        $body = $this->createMock(StreamInterface::class);\n        $body->expects($this->once())->method('__toString')->willReturn('bar');\n\n        $request = $this->createMock(RequestInterface::class);\n        $request->expects($this->once())->method('getMethod')->willReturn('GET');\n        $request->expects($this->once())->method('getUri')->willReturn($uri);\n        $request->expects($this->once())->method('getBody')->willReturn($body);\n\n        $generator = new SimpleGenerator();\n\n        $this->assertSame('GET http://example.com/foo bar', $generator->generate($request));\n    }\n\n    public function testGenerateCacheFromRequestWithNoBody(): void\n    {\n        $uri = $this->createMock(UriInterface::class);\n        $uri->expects($this->once())->method('__toString')->willReturn('http://example.com/foo');\n\n        $body = $this->createMock(StreamInterface::class);\n        $body->expects($this->once())->method('__toString')->willReturn('');\n\n        $request = $this->createMock(RequestInterface::class);\n        $request->expects($this->once())->method('getMethod')->willReturn('GET');\n        $request->expects($this->once())->method('getUri')->willReturn($uri);\n        $request->expects($this->once())->method('getBody')->willReturn($body);\n\n        $generator = new SimpleGenerator();\n\n        $this->assertSame('GET http://example.com/foo', $generator->generate($request));\n    }\n}\n"
  }
]