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
[](https://github.com/php-http/cache-plugin/releases)
[](LICENSE)
[](https://github.com/php-http/cache-plugin/actions/workflows/tests.yml)
[](https://scrutinizer-ci.com/g/php-http/cache-plugin)
[](https://scrutinizer-ci.com/g/php-http/cache-plugin)
[](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));
}
}
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
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[](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.