From 3c1dac0c940fe0c3f62c8dda81b7dfd894d295b2 Mon Sep 17 00:00:00 2001 From: Eric GELOEN Date: Wed, 27 Jul 2016 16:28:02 +0200 Subject: [PATCH 01/97] Cast max age to integer (#5) Cast max age to integer to avoid problems with strict implementations --- CHANGELOG.md | 5 +++++ src/CachePlugin.php | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84dc0d1..8b1cd9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## UNRELEASED + +### Fixed + +- Cast max age header to integer in order to get valid expiration value. ## 1.0.0 - 2016-05-05 diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 24bf5c7..dbb3bab 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -174,7 +174,7 @@ private function getMaxAge(ResponseInterface $response) return $maxAge - ((int) $age); } - return $maxAge; + return (int) $maxAge; } // check for ttl in the Expires header From 0c1dde3b4b4a7644e5f04c1308f697cf129b2844 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Wed, 27 Jul 2016 10:28:08 -0400 Subject: [PATCH 02/97] Applied fixes from StyleCI [ci skip] [skip ci] --- src/CachePlugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CachePlugin.php b/src/CachePlugin.php index dbb3bab..956be5b 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -37,7 +37,7 @@ final class CachePlugin implements Plugin * @param StreamFactory $streamFactory * @param array $config { * - * @var bool $respect_cache_headers Whether to look at the cache directives or ignore them. + * @var bool $respect_cache_headers Whether to look at the cache directives or ignore them * @var int $default_ttl If we do not respect cache headers or can't calculate a good ttl, use this value. * } */ @@ -123,7 +123,7 @@ protected function isCacheable(ResponseInterface $response) * @param ResponseInterface $response * @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. + * @return bool|string The value of the directive, true if directive without value, false if directive not present */ private function getCacheControlDirective(ResponseInterface $response, $name) { From c8f865ab4405cb95b1d4859b8c8f0ea24c5e280b Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Mon, 1 Aug 2016 23:13:17 +0100 Subject: [PATCH 03/97] Test on PHP 7.1 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 7a4a218..e3a0481 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ php: - 5.5 - 5.6 - 7.0 + - 7.1 - hhvm env: From c1608f5e08d53d8b779e73a480a7dd1ded8f4d4a Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Tue, 2 Aug 2016 09:34:41 +0100 Subject: [PATCH 04/97] Use sha1 to reduce the risk of collisions (#12) * Use sha1 to reduce the risk of collisions * Only cache if the max age is positive * Updated tests * Support setting a hash_algo * CS * Reverted accidental change --- spec/CachePluginSpec.php | 6 +++--- src/CachePlugin.php | 7 +++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/spec/CachePluginSpec.php b/spec/CachePluginSpec.php index a46327e..808806d 100644 --- a/spec/CachePluginSpec.php +++ b/spec/CachePluginSpec.php @@ -42,7 +42,7 @@ function it_caches_responses(CacheItemPoolInterface $pool, CacheItemInterface $i $response->getHeader('Cache-Control')->willReturn(array()); $response->getHeader('Expires')->willReturn(array()); - $pool->getItem('e3b717d5883a45ef9493d009741f7c64')->shouldBeCalled()->willReturn($item); + $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); $item->set(['response' => $response, 'body' => $httpBody])->willReturn($item)->shouldBeCalled(); $item->expiresAfter(60)->willReturn($item)->shouldBeCalled(); @@ -63,7 +63,7 @@ function it_doesnt_store_failed_responses(CacheItemPoolInterface $pool, CacheIte $response->getHeader('Cache-Control')->willReturn(array()); $response->getHeader('Expires')->willReturn(array()); - $pool->getItem('e3b717d5883a45ef9493d009741f7c64')->shouldBeCalled()->willReturn($item); + $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); $next = function (RequestInterface $request) use ($response) { @@ -101,7 +101,7 @@ function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemI $response->getHeader('Age')->willReturn(array('15')); $response->getHeader('Expires')->willReturn(array()); - $pool->getItem('e3b717d5883a45ef9493d009741f7c64')->shouldBeCalled()->willReturn($item); + $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); // 40-15 should be 25 diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 956be5b..f0bdbd4 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -38,7 +38,8 @@ final class CachePlugin implements Plugin * @param array $config { * * @var bool $respect_cache_headers Whether to look at the cache directives or ignore them - * @var int $default_ttl If we do not respect cache headers or can't calculate a good ttl, use this value. + * @var int $default_ttl If we do not respect cache headers or can't calculate a good ttl, use this value + * @var string $hash_algo The hashing algorithm to use when generating cache keys. * } */ public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamFactory, array $config = []) @@ -150,7 +151,7 @@ private function getCacheControlDirective(ResponseInterface $response, $name) */ private function createCacheKey(RequestInterface $request) { - return md5($request->getMethod().' '.$request->getUri()); + return hash($this->config['hash_algo'], $request->getMethod().' '.$request->getUri()); } /** @@ -196,9 +197,11 @@ private function configureOptions(OptionsResolver $resolver) $resolver->setDefaults([ 'default_ttl' => null, 'respect_cache_headers' => true, + 'hash_algo' => 'sha1', ]); $resolver->setAllowedTypes('default_ttl', ['int', 'null']); $resolver->setAllowedTypes('respect_cache_headers', 'bool'); + $resolver->setAllowedValues('hash_algo', hash_algos()); } } From f5f4d286fc4d9426e41b97c24c50a667a5f7fb36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20S=C3=A1gi-Kaz=C3=A1r?= Date: Tue, 2 Aug 2016 10:41:15 +0200 Subject: [PATCH 05/97] Update changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b1cd9b..0b77da3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,21 @@ # Change Log + ## UNRELEASED +### Added + +- `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 From af95370e038a3b3930852671d0848a83d7d4a5fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20S=C3=A1gi-Kaz=C3=A1r?= Date: Tue, 2 Aug 2016 10:43:35 +0200 Subject: [PATCH 06/97] Generic package improvements --- .gitattributes | 28 +++++++++++++++------------- .github/ISSUE_TEMPLATE.md | 4 ++-- .gitignore | 11 ++++++----- .travis.yml | 8 +++++--- composer.json | 4 ++-- phpspec.yml.ci => phpspec.ci.yml | 0 6 files changed, 30 insertions(+), 25 deletions(-) rename phpspec.yml.ci => phpspec.ci.yml (100%) diff --git a/.gitattributes b/.gitattributes index 7ae0617..c47225a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,13 +1,15 @@ -.editorconfig export-ignore -.gitattributes export-ignore -.github/ export-ignore -.gitignore export-ignore -.php_cs export-ignore -.scrutinizer.yml export-ignore -.styleci.yml export-ignore -.travis.yml export-ignore -phpspec.yml.ci export-ignore -phpspec.yml.dist export-ignore -phpunit.xml.dist export-ignore -spec/ export-ignore -tests/ export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +/.github/ export-ignore +.gitignore export-ignore +/.php_cs export-ignore +/.scrutinizer.yml export-ignore +/.styleci.yml export-ignore +/.travis.yml export-ignore +/behat.yml.dist export-ignore +/features/ export-ignore +/phpspec.ci.yml export-ignore +/phpspec.yml.dist export-ignore +/phpunit.xml.dist export-ignore +/spec/ export-ignore +/tests/ export-ignore diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 49ec72b..d4ecf20 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,7 +1,7 @@ | Q | A | ------------ | --- -| Bug? | no -| New Feature? | no +| Bug? | no|yes +| New Feature? | no|yes | Version | Specific version or SHA of a commit diff --git a/.gitignore b/.gitignore index 7608c4b..16b4a20 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ -build/ -vendor/ -composer.lock -phpspec.yml -phpunit.xml +/behat.yml +/build/ +/composer.lock +/phpspec.yml +/phpunit.xml +/vendor/ diff --git a/.travis.yml b/.travis.yml index e3a0481..44dc5af 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,14 +29,16 @@ matrix: env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" COVERAGE=true TEST_COMMAND="composer test-ci" before_install: - - travis_retry composer self-update + - if [[ $COVERAGE != true ]]; then phpenv config-rm xdebug.ini || true; fi install: + # To be removed when this issue will be resolved: https://github.com/composer/composer/issues/5355 + - if [[ "$COMPOSER_FLAGS" == *"--prefer-lowest"* ]]; then travis_retry composer update --prefer-dist --no-interaction --prefer-stable --quiet; fi - travis_retry composer update ${COMPOSER_FLAGS} --prefer-dist --no-interaction script: - $TEST_COMMAND after_success: - - if [[ "$COVERAGE" = true ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi - - if [[ "$COVERAGE" = true ]]; then php ocular.phar code-coverage:upload --format=php-clover build/coverage.xml; fi + - if [[ $COVERAGE = true ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi + - if [[ $COVERAGE = true ]]; then php ocular.phar code-coverage:upload --format=php-clover build/coverage.xml; fi diff --git a/composer.json b/composer.json index 59019b5..aa8bde7 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "psr/cache": "^1.0", "php-http/client-common": "^1.1", "php-http/message-factory": "^1.0", - "symfony/options-resolver": "^2.6|^3.0" + "symfony/options-resolver": "^2.6 || ^3.0" }, "require-dev": { "phpspec/phpspec": "^2.5", @@ -28,7 +28,7 @@ }, "scripts": { "test": "vendor/bin/phpspec run", - "test-ci": "vendor/bin/phpspec run -c phpspec.yml.ci" + "test-ci": "vendor/bin/phpspec run -c phpspec.ci.yml" }, "extra": { "branch-alias": { diff --git a/phpspec.yml.ci b/phpspec.ci.yml similarity index 100% rename from phpspec.yml.ci rename to phpspec.ci.yml From 4bf191d7c984d960f9383a9381c6f6cbdb2e011e Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Tue, 2 Aug 2016 22:55:45 +0200 Subject: [PATCH 07/97] Added support for Last-Modified and ETag (#8) * Added support for Last-Modified and ETag * style fix * Return the 304 response if we do not have a response in cache * Bugfix and started on the tests * Style fixes * Tests are passing * Style fix * Added tests * Applied fixes from StyleCI [ci skip] [skip ci] * Updated comment * Made the code easier to read * Updated hash * Minor * cs * Removed return statement * cs * Added comment * Removed period * Added docs about isset --- composer.json | 5 + spec/CachePluginSpec.php | 204 +++++++++++++++++++++++++++++++++++++-- src/CachePlugin.php | 124 ++++++++++++++++++++++-- 3 files changed, 315 insertions(+), 18 deletions(-) diff --git a/composer.json b/composer.json index aa8bde7..52df585 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,11 @@ "Http\\Client\\Common\\Plugin\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "spec\\Http\\Client\\Common\\Plugin\\": "spec/" + } + }, "scripts": { "test": "vendor/bin/phpspec run", "test-ci": "vendor/bin/phpspec run -c phpspec.ci.yml" diff --git a/spec/CachePluginSpec.php b/spec/CachePluginSpec.php index 808806d..97afd55 100644 --- a/spec/CachePluginSpec.php +++ b/spec/CachePluginSpec.php @@ -2,6 +2,7 @@ namespace spec\Http\Client\Common\Plugin; +use Prophecy\Argument; use Http\Message\StreamFactory; use Http\Promise\FulfilledPromise; use PhpSpec\ObjectBehavior; @@ -15,7 +16,10 @@ class CachePluginSpec extends ObjectBehavior { function let(CacheItemPoolInterface $pool, StreamFactory $streamFactory) { - $this->beConstructedWith($pool, $streamFactory, ['default_ttl'=>60]); + $this->beConstructedWith($pool, $streamFactory, [ + 'default_ttl' => 60, + 'cache_lifetime' => 1000 + ]); } function it_is_initializable(CacheItemPoolInterface $pool) @@ -39,14 +43,22 @@ function it_caches_responses(CacheItemPoolInterface $pool, CacheItemInterface $i $request->getUri()->willReturn('/'); $response->getStatusCode()->willReturn(200); $response->getBody()->willReturn($stream); - $response->getHeader('Cache-Control')->willReturn(array()); - $response->getHeader('Expires')->willReturn(array()); + $response->getHeader('Cache-Control')->willReturn(array())->shouldBeCalled(); + $response->getHeader('Expires')->willReturn(array())->shouldBeCalled(); + $response->getHeader('ETag')->willReturn(array())->shouldBeCalled(); $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); - $item->set(['response' => $response, 'body' => $httpBody])->willReturn($item)->shouldBeCalled(); - $item->expiresAfter(60)->willReturn($item)->shouldBeCalled(); - $pool->save($item)->shouldBeCalled(); + $item->expiresAfter(1060)->willReturn($item)->shouldBeCalled(); + + $item->set($this->getCacheItemMatcher([ + 'response' => $response->getWrappedObject(), + 'body' => $httpBody, + 'expiresAt' => 0, + 'createdAt' => 0, + 'etag' => [] + ]))->willReturn($item)->shouldBeCalled(); + $pool->save(Argument::any())->shouldBeCalled(); $next = function (RequestInterface $request) use ($response) { return new FulfilledPromise($response->getWrappedObject()); @@ -100,13 +112,20 @@ function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemI $response->getHeader('Cache-Control')->willReturn(array('max-age=40')); $response->getHeader('Age')->willReturn(array('15')); $response->getHeader('Expires')->willReturn(array()); + $response->getHeader('ETag')->willReturn(array()); $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); - // 40-15 should be 25 - $item->set(['response' => $response, 'body' => $httpBody])->willReturn($item)->shouldBeCalled(); - $item->expiresAfter(25)->willReturn($item)->shouldBeCalled(); + $item->set($this->getCacheItemMatcher([ + 'response' => $response->getWrappedObject(), + 'body' => $httpBody, + 'expiresAt' => 0, + 'createdAt' => 0, + 'etag' => [] + ]))->willReturn($item)->shouldBeCalled(); + // 40-15 should be 25 + the default 1000 + $item->expiresAfter(1025)->willReturn($item)->shouldBeCalled(); $pool->save($item)->shouldBeCalled(); $next = function (RequestInterface $request) use ($response) { @@ -115,4 +134,171 @@ function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemI $this->handleRequest($request, $next, function () {}); } + + function it_saves_etag(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response, StreamInterface $stream) + { + $httpBody = 'body'; + $stream->__toString()->willReturn($httpBody); + $stream->isSeekable()->willReturn(true); + $stream->rewind()->shouldBeCalled(); + + $request->getMethod()->willReturn('GET'); + $request->getUri()->willReturn('/'); + $response->getStatusCode()->willReturn(200); + $response->getBody()->willReturn($stream); + $response->getHeader('Cache-Control')->willReturn(array()); + $response->getHeader('Expires')->willReturn(array()); + $response->getHeader('ETag')->willReturn(array('foo_etag')); + + $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); + $item->isHit()->willReturn(false); + $item->expiresAfter(1060)->willReturn($item); + + $item->set($this->getCacheItemMatcher([ + 'response' => $response->getWrappedObject(), + 'body' => $httpBody, + 'expiresAt' => 0, + 'createdAt' => 0, + 'etag' => ['foo_etag'] + ]))->willReturn($item)->shouldBeCalled(); + $pool->save(Argument::any())->shouldBeCalled(); + + $next = function (RequestInterface $request) use ($response) { + return new FulfilledPromise($response->getWrappedObject()); + }; + + $this->handleRequest($request, $next, function () {}); + } + + function it_adds_etag_and_modfied_since_to_request(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response, StreamInterface $stream) + { + $httpBody = 'body'; + + $request->getMethod()->willReturn('GET'); + $request->getUri()->willReturn('/'); + + $request->withHeader('If-Modified-Since', 'Thursday, 01-Jan-70 01:18:31 GMT')->shouldBeCalled()->willReturn($request); + $request->withHeader('If-None-Match', 'foo_etag')->shouldBeCalled()->willReturn($request); + + $response->getStatusCode()->willReturn(304); + + $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); + $item->isHit()->willReturn(true, false); + $item->get()->willReturn([ + 'response' => $response, + 'body' => $httpBody, + 'expiresAt' => 0, + 'createdAt' => 4711, + 'etag' => ['foo_etag'] + ])->shouldBeCalled(); + + $next = function (RequestInterface $request) use ($response) { + return new FulfilledPromise($response->getWrappedObject()); + }; + + $this->handleRequest($request, $next, function () {}); + } + + function it_servces_a_cached_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response, StreamInterface $stream, StreamFactory $streamFactory) + { + $httpBody = 'body'; + + $request->getMethod()->willReturn('GET'); + $request->getUri()->willReturn('/'); + + $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); + $item->isHit()->willReturn(true); + $item->get()->willReturn([ + 'response' => $response, + 'body' => $httpBody, + 'expiresAt' => time()+1000000, //It is in the future + 'createdAt' => 4711, + 'etag' => [] + ])->shouldBeCalled(); + + // Make sure we add back the body + $response->withBody($stream)->willReturn($response)->shouldBeCalled(); + $streamFactory->createStream($httpBody)->shouldBeCalled()->willReturn($stream); + + $next = function (RequestInterface $request) use ($response) { + return new FulfilledPromise($response->getWrappedObject()); + }; + + $this->handleRequest($request, $next, function () {}); + } + + function it_serves_and_resaved_expired_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response, StreamInterface $stream, StreamFactory $streamFactory) + { + $httpBody = 'body'; + + $request->getMethod()->willReturn('GET'); + $request->getUri()->willReturn('/'); + + $request->withHeader(Argument::any(), Argument::any())->willReturn($request); + $request->withHeader(Argument::any(), Argument::any())->willReturn($request); + + $response->getStatusCode()->willReturn(304); + $response->getHeader('Cache-Control')->willReturn(array()); + $response->getHeader('Expires')->willReturn(array())->shouldBeCalled(); + + // Make sure we add back the body + $response->withBody($stream)->willReturn($response)->shouldBeCalled(); + + $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); + $item->isHit()->willReturn(true, true); + $item->expiresAfter(1060)->willReturn($item)->shouldBeCalled(); + $item->get()->willReturn([ + 'response' => $response, + 'body' => $httpBody, + 'expiresAt' => 0, + 'createdAt' => 4711, + 'etag' => ['foo_etag'] + ])->shouldBeCalled(); + + $item->set($this->getCacheItemMatcher([ + 'response' => $response->getWrappedObject(), + 'body' => $httpBody, + 'expiresAt' => 0, + 'createdAt' => 0, + 'etag' => ['foo_etag'] + ]))->willReturn($item)->shouldBeCalled(); + $pool->save(Argument::any())->shouldBeCalled(); + + $streamFactory->createStream($httpBody)->shouldBeCalled()->willReturn($stream); + + $next = function (RequestInterface $request) use ($response) { + return new FulfilledPromise($response->getWrappedObject()); + }; + + $this->handleRequest($request, $next, function () {}); + } + + + /** + * Private function to match cache item data. + * + * @param array $expectedData + * + * @return \Closure + */ + private function getCacheItemMatcher(array $expectedData) + { + return Argument::that(function(array $actualData) use ($expectedData) { + foreach ($expectedData as $key => $value) { + if (!isset($actualData[$key])) { + return false; + } + + if ($key === 'expiresAt' || $key === 'createdAt') { + // We do not need to validate the value of these fields. + continue; + } + + if ($actualData[$key] !== $value) { + return false; + } + } + return true; + }); + } } diff --git a/src/CachePlugin.php b/src/CachePlugin.php index f0bdbd4..c4fb6af 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -5,6 +5,7 @@ use Http\Client\Common\Plugin; use Http\Message\StreamFactory; use Http\Promise\FulfilledPromise; +use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; @@ -38,8 +39,12 @@ final class CachePlugin implements Plugin * @param array $config { * * @var bool $respect_cache_headers Whether to look at the cache directives or ignore them - * @var int $default_ttl If we do not respect cache headers or can't calculate a good ttl, use this value - * @var string $hash_algo The hashing algorithm to use when generating cache keys. + * @var int $default_ttl (seconds) If we do not respect cache headers or can't calculate a good ttl, use this + * value + * @var string $hash_algo The hashing algorithm to use when generating cache keys + * @var int $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 that the server originally says it is valid for. + * We store a cache item for $cache_lifetime + max age of the response. * } */ public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamFactory, array $config = []) @@ -58,7 +63,6 @@ public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamF public function handleRequest(RequestInterface $request, callable $next, callable $first) { $method = strtoupper($request->getMethod()); - // if the request not is cachable, move to $next if ($method !== 'GET' && $method !== 'HEAD') { return $next($request); @@ -69,15 +73,43 @@ public function handleRequest(RequestInterface $request, callable $next, callabl $cacheItem = $this->pool->getItem($key); if ($cacheItem->isHit()) { - // return cached response $data = $cacheItem->get(); - $response = $data['response']; - $response = $response->withBody($this->streamFactory->createStream($data['body'])); + // The isset() is to be removed in 2.0. + if (isset($data['expiresAt']) && time() < $data['expiresAt']) { + // This item is still valid according to previous cache headers + return new FulfilledPromise($this->createResponseFromCacheItem($cacheItem)); + } + + // Add headers to ask the server if this cache is still valid + if ($modifiedSinceValue = $this->getModifiedSinceHeaderValue($cacheItem)) { + $request = $request->withHeader('If-Modified-Since', $modifiedSinceValue); + } - return new FulfilledPromise($response); + if ($etag = $this->getETag($cacheItem)) { + $request = $request->withHeader('If-None-Match', $etag); + } } return $next($request)->then(function (ResponseInterface $response) use ($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 $response; + } + + // The cached response we have is still valid + $data = $cacheItem->get(); + $maxAge = $this->getMaxAge($response); + $data['expiresAt'] = time() + $maxAge; + $cacheItem->set($data)->expiresAfter($this->config['cache_lifetime'] + $maxAge); + $this->pool->save($cacheItem); + + return $this->createResponseFromCacheItem($cacheItem); + } + if ($this->isCacheable($response)) { $bodyStream = $response->getBody(); $body = $bodyStream->__toString(); @@ -87,8 +119,17 @@ public function handleRequest(RequestInterface $request, callable $next, callabl $response = $response->withBody($this->streamFactory->createStream($body)); } - $cacheItem->set(['response' => $response, 'body' => $body]) - ->expiresAfter($this->getMaxAge($response)); + $maxAge = $this->getMaxAge($response); + $currentTime = time(); + $cacheItem + ->expiresAfter($this->config['cache_lifetime'] + $maxAge) + ->set([ + 'response' => $response, + 'body' => $body, + 'expiresAt' => $currentTime + $maxAge, + 'createdAt' => $currentTime, + 'etag' => $response->getHeader('ETag'), + ]); $this->pool->save($cacheItem); } @@ -195,13 +236,78 @@ private function getMaxAge(ResponseInterface $response) private function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ + 'cache_lifetime' => 86400 * 30, // 30 days 'default_ttl' => null, 'respect_cache_headers' => true, 'hash_algo' => 'sha1', ]); + $resolver->setAllowedTypes('cache_lifetime', 'int'); $resolver->setAllowedTypes('default_ttl', ['int', 'null']); $resolver->setAllowedTypes('respect_cache_headers', 'bool'); $resolver->setAllowedValues('hash_algo', hash_algos()); } + + /** + * @param CacheItemInterface $cacheItem + * + * @return ResponseInterface + */ + private function createResponseFromCacheItem(CacheItemInterface $cacheItem) + { + $data = $cacheItem->get(); + + /** @var ResponseInterface $response */ + $response = $data['response']; + $response = $response->withBody($this->streamFactory->createStream($data['body'])); + + return $response; + } + + /** + * Get the value of the "If-Modified-Since" header. + * + * @param CacheItemInterface $cacheItem + * + * @return string|null + */ + private function getModifiedSinceHeaderValue(CacheItemInterface $cacheItem) + { + $data = $cacheItem->get(); + // The isset() is to be removed in 2.0. + if (!isset($data['createdAt'])) { + return; + } + + $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. + * + * @param CacheItemInterface $cacheItem + * + * @return string|null + */ + private function getETag(CacheItemInterface $cacheItem) + { + $data = $cacheItem->get(); + // The isset() is to be removed in 2.0. + if (!isset($data['etag'])) { + return; + } + + if (!is_array($data['etag'])) { + return $data['etag']; + } + + foreach ($data['etag'] as $etag) { + if (!empty($etag)) { + return $etag; + } + } + } } From 63acc8ae81dbc2393bcd8b7b8a0b23ba8f7eb309 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Tue, 2 Aug 2016 22:04:18 +0100 Subject: [PATCH 08/97] Bumped branch alias --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 52df585..0c04c62 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "1.1-dev" } }, "prefer-stable": true, From f00d4ea8b042f6474a6a55beacb6320e7b726783 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Wed, 3 Aug 2016 08:56:19 +0200 Subject: [PATCH 09/97] changelog for cache validation --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b77da3..8c34009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### 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 From a253129a2b0315013289dd6771f4db2705e31d45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20S=C3=A1gi-Kaz=C3=A1r?= Date: Thu, 4 Aug 2016 10:20:12 +0200 Subject: [PATCH 10/97] Prepare release --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c34009..8acd670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Change Log -## UNRELEASED +## 1.1.0 - 2016-08-04 ### Added From 9b380b9b07b0bfff6871fcdb946aebaf6c506e5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20S=C3=A1gi-Kaz=C3=A1r?= Date: Thu, 4 Aug 2016 10:23:12 +0200 Subject: [PATCH 11/97] Improve composer.json --- composer.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 0c04c62..7950a75 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ } ], "require": { - "php": ">=5.4", + "php": "^5.4 || ^7.0", "psr/cache": "^1.0", "php-http/client-common": "^1.1", "php-http/message-factory": "^1.0", @@ -39,7 +39,5 @@ "branch-alias": { "dev-master": "1.1-dev" } - }, - "prefer-stable": true, - "minimum-stability": "dev" + } } From a9dbc04a880137b5e850e3bc0618190c0044278a Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Thu, 4 Aug 2016 11:29:58 +0200 Subject: [PATCH 12/97] Minor typo and fix (#18) typo fix and cleanup composer.json --- src/CachePlugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CachePlugin.php b/src/CachePlugin.php index c4fb6af..07f9a7b 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -43,7 +43,7 @@ final class CachePlugin implements Plugin * value * @var string $hash_algo The hashing algorithm to use when generating cache keys * @var int $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 that the server originally says it is valid for. + * 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. * } */ From 28d904c657cce6c91947ef0f7cee596a911cdaa9 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Mon, 15 Aug 2016 10:47:50 +0200 Subject: [PATCH 13/97] Allow a cache item to be stored forever (#20) Allow cache item to stay valid forever --- CHANGELOG.md | 10 +++++++++ src/CachePlugin.php | 53 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8acd670..1d5ef4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Change Log +## UNRELEASED + +### 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 diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 07f9a7b..5053aad 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -74,8 +74,8 @@ public function handleRequest(RequestInterface $request, callable $next, callabl if ($cacheItem->isHit()) { $data = $cacheItem->get(); - // The isset() is to be removed in 2.0. - if (isset($data['expiresAt']) && time() < $data['expiresAt']) { + // The array_key_exists() is to be removed in 2.0. + if (array_key_exists('expiresAt', $data) && ($data['expiresAt'] === null || time() < $data['expiresAt'])) { // This item is still valid according to previous cache headers return new FulfilledPromise($this->createResponseFromCacheItem($cacheItem)); } @@ -103,8 +103,8 @@ public function handleRequest(RequestInterface $request, callable $next, callabl // The cached response we have is still valid $data = $cacheItem->get(); $maxAge = $this->getMaxAge($response); - $data['expiresAt'] = time() + $maxAge; - $cacheItem->set($data)->expiresAfter($this->config['cache_lifetime'] + $maxAge); + $data['expiresAt'] = $this->calculateResponseExpiresAt($maxAge); + $cacheItem->set($data)->expiresAfter($this->calculateCacheItemExpiresAfter($maxAge)); $this->pool->save($cacheItem); return $this->createResponseFromCacheItem($cacheItem); @@ -120,14 +120,13 @@ public function handleRequest(RequestInterface $request, callable $next, callabl } $maxAge = $this->getMaxAge($response); - $currentTime = time(); $cacheItem - ->expiresAfter($this->config['cache_lifetime'] + $maxAge) + ->expiresAfter($this->calculateCacheItemExpiresAfter($maxAge)) ->set([ 'response' => $response, 'body' => $body, - 'expiresAt' => $currentTime + $maxAge, - 'createdAt' => $currentTime, + 'expiresAt' => $this->calculateResponseExpiresAt($maxAge), + 'createdAt' => time(), 'etag' => $response->getHeader('ETag'), ]); $this->pool->save($cacheItem); @@ -137,6 +136,40 @@ public function handleRequest(RequestInterface $request, callable $next, callabl }); } + /** + * Calculate the timestamp when this cache item should be dropped from the cache. The lowest value that can be + * returned is $maxAge. + * + * @param int|null $maxAge + * + * @return int|null Unix system time passed to the PSR-6 cache + */ + private function calculateCacheItemExpiresAfter($maxAge) + { + if ($this->config['cache_lifetime'] === null && $maxAge === null) { + return; + } + + return $this->config['cache_lifetime'] + $maxAge; + } + + /** + * 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. + * + * @param int|null $maxAge + * + * @return int|null Unix system time. A null value means that the response expires when the cache item expires + */ + private function calculateResponseExpiresAt($maxAge) + { + if ($maxAge === null) { + return; + } + + return time() + $maxAge; + } + /** * Verify that we can cache this response. * @@ -237,12 +270,12 @@ private function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ 'cache_lifetime' => 86400 * 30, // 30 days - 'default_ttl' => null, + 'default_ttl' => 0, 'respect_cache_headers' => true, 'hash_algo' => 'sha1', ]); - $resolver->setAllowedTypes('cache_lifetime', 'int'); + $resolver->setAllowedTypes('cache_lifetime', ['int', 'null']); $resolver->setAllowedTypes('default_ttl', ['int', 'null']); $resolver->setAllowedTypes('respect_cache_headers', 'bool'); $resolver->setAllowedValues('hash_algo', hash_algos()); From 6eea1d718d17d62eb79c07c0e09a16910bda531b Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Mon, 15 Aug 2016 10:27:41 +0100 Subject: [PATCH 14/97] Bumped branch alias (#21) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 7950a75..646bbc5 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-master": "1.2-dev" } } } From b4e421cb5214ad9ef8b25bb32214ed4b71a8b356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20S=C3=A1gi-Kaz=C3=A1r?= Date: Tue, 16 Aug 2016 14:12:50 +0200 Subject: [PATCH 15/97] Prepare release --- CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d5ef4f..d6c3ed3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,17 @@ # Change Log -## UNRELEASED + +## 1.2.0 - 2016-08-16 ### Changed -- The default value for ``default_ttl`` is changed from ``null`` to ``0``. +- 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``. +- We allow `cache_lifetime` to be set to `null`. + ## 1.1.0 - 2016-08-04 From 9ea17142630157a85662788e68300ab4e3fcd3e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20S=C3=A1gi-Kaz=C3=A1r?= Date: Tue, 16 Aug 2016 14:13:14 +0200 Subject: [PATCH 16/97] Bump branch alias --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 646bbc5..a4f0fe4 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.3-dev" } } } From c8dfcc58e8233e36450a62d52ff37216f5b879e2 Mon Sep 17 00:00:00 2001 From: Mika Tuupola Date: Mon, 20 Feb 2017 14:42:28 +0700 Subject: [PATCH 17/97] Optionally enable caching of any request method (#24) Allow configuring any valid request method as defined by RFC7230 to be cached https://tools.ietf.org/html/rfc7230#section-3.1.1 https://tools.ietf.org/html/rfc7230#section-3.2.6 Technically RFC7230 allows lowercase request methods but consensus was they might be too confusing for users. Include request body in cache key calculation --- CHANGELOG.md | 6 ++- spec/CachePluginSpec.php | 80 +++++++++++++++++++++++++++++++++++++++- src/CachePlugin.php | 19 ++++++++-- 3 files changed, 100 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6c3ed3..7df0c97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,15 @@ # Change Log +## 1.3.0 - unreleased +### Added + +- New `methods` setting which allows to configure the request methods which can be cached. ## 1.2.0 - 2016-08-16 ### Changed -- The default value for `default_ttl` is changed from `null` to `0`. +- The default value for `default_ttl` is changed from `null` to `0`. ### Fixed diff --git a/spec/CachePluginSpec.php b/spec/CachePluginSpec.php index 97afd55..a3561fb 100644 --- a/spec/CachePluginSpec.php +++ b/spec/CachePluginSpec.php @@ -41,6 +41,8 @@ function it_caches_responses(CacheItemPoolInterface $pool, CacheItemInterface $i $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn('/'); + $request->getBody()->shouldBeCalled(); + $response->getStatusCode()->willReturn(200); $response->getBody()->willReturn($stream); $response->getHeader('Cache-Control')->willReturn(array())->shouldBeCalled(); @@ -71,6 +73,8 @@ function it_doesnt_store_failed_responses(CacheItemPoolInterface $pool, CacheIte { $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn('/'); + $request->getBody()->shouldBeCalled(); + $response->getStatusCode()->willReturn(400); $response->getHeader('Cache-Control')->willReturn(array()); $response->getHeader('Expires')->willReturn(array()); @@ -85,7 +89,7 @@ function it_doesnt_store_failed_responses(CacheItemPoolInterface $pool, CacheIte $this->handleRequest($request, $next, function () {}); } - function it_doesnt_store_post_requests(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response) + function it_doesnt_store_post_requests_by_default(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response) { $request->getMethod()->willReturn('POST'); $request->getUri()->willReturn('/'); @@ -97,6 +101,74 @@ function it_doesnt_store_post_requests(CacheItemPoolInterface $pool, CacheItemIn $this->handleRequest($request, $next, function () {}); } + function it_stores_post_requests_when_allowed( + CacheItemPoolInterface $pool, + CacheItemInterface $item, + RequestInterface $request, + ResponseInterface $response, + StreamFactory $streamFactory, + StreamInterface $stream + ) { + $this->beConstructedWith($pool, $streamFactory, [ + 'default_ttl' => 60, + 'cache_lifetime' => 1000, + 'methods' => ['GET', 'HEAD', 'POST'] + ]); + + $httpBody = 'hello=world'; + $stream->__toString()->willReturn($httpBody); + $stream->isSeekable()->willReturn(true); + $stream->rewind()->shouldBeCalled(); + + $request->getMethod()->willReturn('POST'); + $request->getUri()->willReturn('/post'); + $request->getBody()->willReturn($stream); + + $response->getStatusCode()->willReturn(200); + $response->getBody()->willReturn($stream); + $response->getHeader('Cache-Control')->willReturn([])->shouldBeCalled(); + $response->getHeader('Expires')->willReturn([])->shouldBeCalled(); + $response->getHeader('ETag')->willReturn([])->shouldBeCalled(); + + $pool->getItem('e4311a9af932c603b400a54efab21b6d7dea7a90')->shouldBeCalled()->willReturn($item); + $item->isHit()->willReturn(false); + $item->expiresAfter(1060)->willReturn($item)->shouldBeCalled(); + + $item->set($this->getCacheItemMatcher([ + 'response' => $response->getWrappedObject(), + 'body' => $httpBody, + 'expiresAt' => 0, + 'createdAt' => 0, + 'etag' => [] + ]))->willReturn($item)->shouldBeCalled(); + + $pool->save(Argument::any())->shouldBeCalled(); + + $next = function (RequestInterface $request) use ($response) { + return new FulfilledPromise($response->getWrappedObject()); + }; + + $this->handleRequest($request, $next, function () {}); + } + + function it_does_not_allow_invalid_request_methods( + CacheItemPoolInterface $pool, + CacheItemInterface $item, + RequestInterface $request, + ResponseInterface $response, + StreamFactory $streamFactory, + StreamInterface $stream + ) { + $this + ->shouldThrow("Symfony\Component\OptionsResolver\Exception\InvalidOptionsException") + ->during('__construct', [$pool, $streamFactory, ['methods' => ['GET', 'HEAD', 'POST ']]]); + $this + ->shouldThrow("Symfony\Component\OptionsResolver\Exception\InvalidOptionsException") + ->during('__construct', [$pool, $streamFactory, ['methods' => ['GET', 'HEAD"', 'POST']]]); + $this + ->shouldThrow("Symfony\Component\OptionsResolver\Exception\InvalidOptionsException") + ->during('__construct', [$pool, $streamFactory, ['methods' => ['GET', 'head', 'POST']]]); + } function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response, StreamInterface $stream) { @@ -107,6 +179,8 @@ function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemI $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn('/'); + $request->getBody()->shouldBeCalled(); + $response->getStatusCode()->willReturn(200); $response->getBody()->willReturn($stream); $response->getHeader('Cache-Control')->willReturn(array('max-age=40')); @@ -141,6 +215,7 @@ function it_saves_etag(CacheItemPoolInterface $pool, CacheItemInterface $item, R $stream->__toString()->willReturn($httpBody); $stream->isSeekable()->willReturn(true); $stream->rewind()->shouldBeCalled(); + $request->getBody()->shouldBeCalled(); $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn('/'); @@ -176,6 +251,7 @@ function it_adds_etag_and_modfied_since_to_request(CacheItemPoolInterface $pool, $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn('/'); + $request->getBody()->shouldBeCalled(); $request->withHeader('If-Modified-Since', 'Thursday, 01-Jan-70 01:18:31 GMT')->shouldBeCalled()->willReturn($request); $request->withHeader('If-None-Match', 'foo_etag')->shouldBeCalled()->willReturn($request); @@ -205,6 +281,7 @@ function it_servces_a_cached_response(CacheItemPoolInterface $pool, CacheItemInt $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn('/'); + $request->getBody()->shouldBeCalled(); $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(true); @@ -233,6 +310,7 @@ function it_serves_and_resaved_expired_response(CacheItemPoolInterface $pool, Ca $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn('/'); + $request->getBody()->shouldBeCalled(); $request->withHeader(Argument::any(), Argument::any())->willReturn($request); $request->withHeader(Argument::any(), Argument::any())->willReturn($request); diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 5053aad..66d38c6 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -45,6 +45,7 @@ final class CachePlugin implements Plugin * @var int $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. + * @var array $methods list of request methods which can be cached. * } */ public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamFactory, array $config = []) @@ -64,7 +65,7 @@ public function handleRequest(RequestInterface $request, callable $next, callabl { $method = strtoupper($request->getMethod()); // if the request not is cachable, move to $next - if ($method !== 'GET' && $method !== 'HEAD') { + if (!in_array($method, $this->config['methods'])) { return $next($request); } @@ -205,7 +206,6 @@ private function getCacheControlDirective(ResponseInterface $response, $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]; @@ -225,7 +225,12 @@ private function getCacheControlDirective(ResponseInterface $response, $name) */ private function createCacheKey(RequestInterface $request) { - return hash($this->config['hash_algo'], $request->getMethod().' '.$request->getUri()); + $body = (string) $request->getBody(); + if (!empty($body)) { + $body = ' '.$body; + } + + return hash($this->config['hash_algo'], $request->getMethod().' '.$request->getUri().$body); } /** @@ -273,12 +278,20 @@ private function configureOptions(OptionsResolver $resolver) 'default_ttl' => 0, 'respect_cache_headers' => true, 'hash_algo' => 'sha1', + 'methods' => ['GET', 'HEAD'], ]); $resolver->setAllowedTypes('cache_lifetime', ['int', 'null']); $resolver->setAllowedTypes('default_ttl', ['int', 'null']); $resolver->setAllowedTypes('respect_cache_headers', 'bool'); + $resolver->setAllowedTypes('methods', '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); + }); } /** From 1568bea5f426955dc815cf67a3d10e99526152ed Mon Sep 17 00:00:00 2001 From: Jeroen Thora Date: Wed, 22 Feb 2017 08:29:06 +0100 Subject: [PATCH 18/97] Implement caching types for client and server caching. Fixes #3 --- CHANGELOG.md | 11 +++++ spec/CachePluginSpec.php | 48 ++++++++++++++++++++++ src/CachePlugin.php | 88 ++++++++++++++++++++++++++++++++++++---- 3 files changed, 140 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7df0c97..a5cec58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ ### Added - New `methods` setting which allows to configure the request methods which can be cached. +- New `respect_response_cache_directives` config setting 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 + +### 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. ## 1.2.0 - 2016-08-16 diff --git a/spec/CachePluginSpec.php b/spec/CachePluginSpec.php index a3561fb..b8f6560 100644 --- a/spec/CachePluginSpec.php +++ b/spec/CachePluginSpec.php @@ -351,6 +351,54 @@ function it_serves_and_resaved_expired_response(CacheItemPoolInterface $pool, Ca $this->handleRequest($request, $next, function () {}); } + function it_caches_private_responses_when_allowed( + CacheItemPoolInterface $pool, + CacheItemInterface $item, + RequestInterface $request, + ResponseInterface $response, + StreamFactory $streamFactory, + StreamInterface $stream + ) { + $this->beConstructedThrough('clientCache', [$pool, $streamFactory, [ + 'default_ttl' => 60, + 'cache_lifetime' => 1000, + ]]); + + $httpBody = 'body'; + $stream->__toString()->willReturn($httpBody); + $stream->isSeekable()->willReturn(true); + $stream->rewind()->shouldBeCalled(); + + $request->getMethod()->willReturn('GET'); + $request->getUri()->willReturn('/'); + $request->getBody()->shouldBeCalled(); + + $response->getStatusCode()->willReturn(200); + $response->getBody()->willReturn($stream); + $response->getHeader('Cache-Control')->willReturn(['private'])->shouldBeCalled(); + $response->getHeader('Expires')->willReturn(array())->shouldBeCalled(); + $response->getHeader('ETag')->willReturn(array())->shouldBeCalled(); + + $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); + $item->isHit()->willReturn(false); + $item->expiresAfter(1060)->willReturn($item)->shouldBeCalled(); + + $item->set($this->getCacheItemMatcher([ + 'response' => $response->getWrappedObject(), + 'body' => $httpBody, + 'expiresAt' => 0, + 'createdAt' => 0, + 'etag' => [] + ]))->willReturn($item)->shouldBeCalled(); + $pool->save(Argument::any())->shouldBeCalled(); + + $next = function (RequestInterface $request) use ($response) { + return new FulfilledPromise($response->getWrappedObject()); + }; + + $this->handleRequest($request, $next, function () {}); + } + /** * Private function to match cache item data. diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 66d38c6..b1ab73b 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -9,6 +9,7 @@ use Psr\Cache\CacheItemPoolInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; +use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; /** @@ -33,6 +34,13 @@ final class CachePlugin implements Plugin */ private $config; + /** + * Cache directives indicating if a response can not be cached. + * + * @var array + */ + private $noCacheFlags = ['no-cache', 'private', 'no-store']; + /** * @param CacheItemPoolInterface $pool * @param StreamFactory $streamFactory @@ -45,7 +53,8 @@ final class CachePlugin implements Plugin * @var int $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. - * @var array $methods list of request methods which can be cached. + * @var array $methods list of request methods which can be cached + * @var array $respect_response_cache_directives list of cache directives this plugin will respect while caching responses. * } */ public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamFactory, array $config = []) @@ -53,11 +62,57 @@ public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamF $this->pool = $pool; $this->streamFactory = $streamFactory; + if (isset($config['respect_cache_headers']) && isset($config['respect_response_cache_directives'])) { + 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); } + /** + * 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 CacheItemPoolInterface $pool + * @param StreamFactory $streamFactory + * @param array $config For all possible config options see the constructor docs + * + * @return CachePlugin + */ + public static function clientCache(CacheItemPoolInterface $pool, StreamFactory $streamFactory, array $config = []) + { + // Allow caching of private requests + if (isset($config['respect_response_cache_directives'])) { + $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 CacheItemPoolInterface $pool + * @param StreamFactory $streamFactory + * @param array $config For all possible config options see the constructor docs + * + * @return CachePlugin + */ + public static function serverCache(CacheItemPoolInterface $pool, StreamFactory $streamFactory, array $config = []) + { + return new self($pool, $streamFactory, $config); + } + /** * {@inheritdoc} */ @@ -183,11 +238,12 @@ protected function isCacheable(ResponseInterface $response) if (!in_array($response->getStatusCode(), [200, 203, 300, 301, 302, 404, 410])) { return false; } - if (!$this->config['respect_cache_headers']) { - return true; - } - if ($this->getCacheControlDirective($response, 'no-store') || $this->getCacheControlDirective($response, 'private')) { - 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; @@ -242,7 +298,7 @@ private function createCacheKey(RequestInterface $request) */ private function getMaxAge(ResponseInterface $response) { - if (!$this->config['respect_cache_headers']) { + if (!in_array('max-age', $this->config['respect_response_cache_directives'], true)) { return $this->config['default_ttl']; } @@ -276,9 +332,11 @@ private function configureOptions(OptionsResolver $resolver) $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' => true, 'hash_algo' => 'sha1', 'methods' => ['GET', 'HEAD'], + 'respect_response_cache_directives' => ['no-cache', 'private', 'max-age', 'no-store'], ]); $resolver->setAllowedTypes('cache_lifetime', ['int', 'null']); @@ -292,6 +350,22 @@ private function configureOptions(OptionsResolver $resolver) return empty($matches); }); + + $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 $value; + }); + + $resolver->setNormalizer('respect_response_cache_directives', function (Options $options, $value) { + if (false === $options['respect_cache_headers']) { + return []; + } + + return $value; + }); } /** From 7110f6c4e0db3e86d176234cdf1d840d4cf94778 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Mon, 27 Feb 2017 10:28:48 +0100 Subject: [PATCH 19/97] cleanup changelog --- CHANGELOG.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5cec58..6b098f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,18 +3,19 @@ ## 1.3.0 - unreleased ### Added -- New `methods` setting which allows to configure the request methods which can be cached. -- New `respect_response_cache_directives` config setting to define specific cache directives to respect when handling responses. +- 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. + the correct config settigns for each usecase. ### Changed -- The `no-cache` directive is now respected by the plugin and will not cache the response +- 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`. ### 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 From 77891394da09d7d371a9e9828933525ccfe539ba Mon Sep 17 00:00:00 2001 From: Mika Tuupola Date: Thu, 23 Mar 2017 19:47:59 +0700 Subject: [PATCH 20/97] Do not allow slash in method name (#33) * Do not allow / character in method name * Make StyleCI happy --- src/CachePlugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CachePlugin.php b/src/CachePlugin.php index b1ab73b..e74657a 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -346,7 +346,7 @@ private function configureOptions(OptionsResolver $resolver) $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); + $matches = preg_grep('/[^A-Z0-9!#$%&\'*+\-.^_`|~]/', $value); return empty($matches); }); From 55f531ab5f5524716056b105c271165f1bc1efea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Tue, 28 Mar 2017 08:08:32 +0100 Subject: [PATCH 21/97] Rewind stream when possible (#29) * Rewind stream retrieved from the cache * Move exception to Common namespace * move exception * reverted --- src/CachePlugin.php | 11 ++++++++++- src/Exception/RewindStreamException.php | 12 ++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 src/Exception/RewindStreamException.php diff --git a/src/CachePlugin.php b/src/CachePlugin.php index e74657a..10b1c25 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -3,6 +3,7 @@ namespace Http\Client\Common\Plugin; use Http\Client\Common\Plugin; +use Http\Client\Common\Plugin\Exception\RewindStreamException; use Http\Message\StreamFactory; use Http\Promise\FulfilledPromise; use Psr\Cache\CacheItemInterface; @@ -379,7 +380,15 @@ private function createResponseFromCacheItem(CacheItemInterface $cacheItem) /** @var ResponseInterface $response */ $response = $data['response']; - $response = $response->withBody($this->streamFactory->createStream($data['body'])); + $stream = $this->streamFactory->createStream($data['body']); + + try { + $stream->rewind(); + } catch (\Exception $e) { + throw new RewindStreamException('Cannot rewind stream.', 0, $e); + } + + $response = $response->withBody($stream); return $response; } diff --git a/src/Exception/RewindStreamException.php b/src/Exception/RewindStreamException.php new file mode 100644 index 0000000..1b9eaee --- /dev/null +++ b/src/Exception/RewindStreamException.php @@ -0,0 +1,12 @@ + + */ +class RewindStreamException extends \RuntimeException implements Exception +{ +} From 9dcdfe4ff3687ed3b80a11bb468cf5860ee10e4e Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Tue, 28 Mar 2017 09:08:57 +0200 Subject: [PATCH 22/97] Prepare for 1.3 (#32) * Prepare for 1.3 * Update CHANGELOG.md * Update CHANGELOG.md --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b098f0..ba560d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Change Log -## 1.3.0 - unreleased +## 1.3.0 - 2017-03-28 + ### Added - New `methods` option which allows to configure the request methods which can be cached. @@ -11,12 +12,14 @@ ### 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 From df86eb57ca4cbb997c00a972b7e66cf710e776e3 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Tue, 28 Mar 2017 10:22:04 +0200 Subject: [PATCH 23/97] Updated branch alias (#34) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a4f0fe4..293c61d 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.3-dev" + "dev-master": "1.4-dev" } } } From c49bab842eefb1df3e47bca8c41adf2ab32f52ec Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Mon, 3 Apr 2017 08:42:35 +0200 Subject: [PATCH 24/97] Added CacheKeyGenerator (#35) * Added CacheKeyGenerator Also added a default generator * cs fix * Support PSR-4 * Added test to make sure cache_key_generator works * cs * Changes according to feedback. * Make plugin final * Bugfixes * Remove final * typo --- spec/Cache/Generator/SimpleGeneratorSpec.php | 38 ++++++++++++++++++ spec/CachePluginSpec.php | 42 ++++++++++++++++++++ src/Cache/Generator/CacheKeyGenerator.php | 22 ++++++++++ src/Cache/Generator/SimpleGenerator.php | 23 +++++++++++ src/CachePlugin.php | 18 ++++++--- 5 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 spec/Cache/Generator/SimpleGeneratorSpec.php create mode 100644 src/Cache/Generator/CacheKeyGenerator.php create mode 100644 src/Cache/Generator/SimpleGenerator.php diff --git a/spec/Cache/Generator/SimpleGeneratorSpec.php b/spec/Cache/Generator/SimpleGeneratorSpec.php new file mode 100644 index 0000000..9ee102c --- /dev/null +++ b/spec/Cache/Generator/SimpleGeneratorSpec.php @@ -0,0 +1,38 @@ +shouldHaveType('Http\Client\Common\Plugin\Cache\Generator\SimpleGenerator'); + } + + public function it_is_a_key_generator() + { + $this->shouldImplement('Http\Client\Common\Plugin\Cache\Generator\CacheKeyGenerator'); + } + + public function it_generates_cache_from_request(RequestInterface $request) + { + $request->getMethod()->shouldBeCalled()->willReturn('GET'); + $request->getUri()->shouldBeCalled()->willReturn('http://example.com/foo'); + $request->getBody()->shouldBeCalled()->willReturn('bar'); + + $this->generate($request)->shouldReturn('GET http://example.com/foo bar'); + } + + public function it_generates_cache_from_request_with_no_body(RequestInterface $request) + { + $request->getMethod()->shouldBeCalled()->willReturn('GET'); + $request->getUri()->shouldBeCalled()->willReturn('http://example.com/foo'); + $request->getBody()->shouldBeCalled()->willReturn(''); + + // No extra space after uri + $this->generate($request)->shouldReturn('GET http://example.com/foo'); + } +} diff --git a/spec/CachePluginSpec.php b/spec/CachePluginSpec.php index b8f6560..af8a483 100644 --- a/spec/CachePluginSpec.php +++ b/spec/CachePluginSpec.php @@ -2,6 +2,7 @@ namespace spec\Http\Client\Common\Plugin; +use Http\Client\Common\Plugin\Cache\Generator\SimpleGenerator; use Prophecy\Argument; use Http\Message\StreamFactory; use Http\Promise\FulfilledPromise; @@ -400,6 +401,47 @@ function it_caches_private_responses_when_allowed( } + function it_can_be_initialized_with_custom_cache_key_generator( + CacheItemPoolInterface $pool, + CacheItemInterface $item, + StreamFactory $streamFactory, + RequestInterface $request, + ResponseInterface $response, + StreamInterface $stream, + SimpleGenerator $generator + ) { + $this->beConstructedThrough('clientCache', [$pool, $streamFactory, [ + 'cache_key_generator' => $generator, + ]]); + + $generator->generate($request)->shouldBeCalled()->willReturn('foo'); + + $stream->isSeekable()->willReturn(true); + $stream->rewind()->shouldBeCalled(); + $streamFactory->createStream(Argument::any())->willReturn($stream); + + $request->getMethod()->willReturn('GET'); + $request->getUri()->willReturn('/'); + $response->withBody(Argument::any())->willReturn($response); + + $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); + $item->isHit()->willReturn(true); + $item->get()->willReturn([ + 'response' => $response->getWrappedObject(), + 'body' => 'body', + 'expiresAt' => null, + 'createdAt' => 0, + 'etag' => [] + ]); + + $next = function (RequestInterface $request) use ($response) { + return new FulfilledPromise($response->getWrappedObject()); + }; + + $this->handleRequest($request, $next, function () {}); + } + + /** * Private function to match cache item data. * diff --git a/src/Cache/Generator/CacheKeyGenerator.php b/src/Cache/Generator/CacheKeyGenerator.php new file mode 100644 index 0000000..d351e57 --- /dev/null +++ b/src/Cache/Generator/CacheKeyGenerator.php @@ -0,0 +1,22 @@ + + */ +interface CacheKeyGenerator +{ + /** + * Generate a cache key from a Request. + * + * @param RequestInterface $request + * + * @return string + */ + public function generate(RequestInterface $request); +} diff --git a/src/Cache/Generator/SimpleGenerator.php b/src/Cache/Generator/SimpleGenerator.php new file mode 100644 index 0000000..4f0ee90 --- /dev/null +++ b/src/Cache/Generator/SimpleGenerator.php @@ -0,0 +1,23 @@ + + */ +class SimpleGenerator implements CacheKeyGenerator +{ + public function generate(RequestInterface $request) + { + $body = (string) $request->getBody(); + if (!empty($body)) { + $body = ' '.$body; + } + + return $request->getMethod().' '.$request->getUri().$body; + } +} diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 10b1c25..fd7d87f 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -4,6 +4,8 @@ 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\Message\StreamFactory; use Http\Promise\FulfilledPromise; use Psr\Cache\CacheItemInterface; @@ -55,7 +57,8 @@ final class CachePlugin implements Plugin * 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. * @var array $methods list of request methods which can be cached - * @var array $respect_response_cache_directives list of cache directives this plugin will respect while caching responses. + * @var array $respect_response_cache_directives list of cache directives this plugin will respect while caching responses + * @var CacheKeyGenerator $cache_key_generator a class to generate the cache key. Defaults to SimpleGenerator * } */ public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamFactory, array $config = []) @@ -73,6 +76,10 @@ public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamF $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(); + } } /** @@ -282,12 +289,9 @@ private function getCacheControlDirective(ResponseInterface $response, $name) */ private function createCacheKey(RequestInterface $request) { - $body = (string) $request->getBody(); - if (!empty($body)) { - $body = ' '.$body; - } + $key = $this->config['cache_key_generator']->generate($request); - return hash($this->config['hash_algo'], $request->getMethod().' '.$request->getUri().$body); + return hash($this->config['hash_algo'], $key); } /** @@ -338,12 +342,14 @@ private function configureOptions(OptionsResolver $resolver) 'hash_algo' => 'sha1', 'methods' => ['GET', 'HEAD'], 'respect_response_cache_directives' => ['no-cache', 'private', 'max-age', 'no-store'], + 'cache_key_generator' => null, ]); $resolver->setAllowedTypes('cache_lifetime', ['int', 'null']); $resolver->setAllowedTypes('default_ttl', ['int', 'null']); $resolver->setAllowedTypes('respect_cache_headers', 'bool'); $resolver->setAllowedTypes('methods', 'array'); + $resolver->setAllowedTypes('cache_key_generator', ['null', 'Http\Client\Common\Plugin\Cache\Generator\CacheKeyGenerator']); $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. */ From 4e605541d8bcb940add22e434d88e58ee5bf663e Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Wed, 5 Apr 2017 13:29:55 +0200 Subject: [PATCH 25/97] Added VaryGenerator (#36) Added HeaderHashKeyGenerator --- .../Generator/HeaderCacheKeyGeneratorSpec.php | 37 ++++++++++++++++++ .../Generator/HeaderCacheKeyGenerator.php | 38 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 spec/Cache/Generator/HeaderCacheKeyGeneratorSpec.php create mode 100644 src/Cache/Generator/HeaderCacheKeyGenerator.php diff --git a/spec/Cache/Generator/HeaderCacheKeyGeneratorSpec.php b/spec/Cache/Generator/HeaderCacheKeyGeneratorSpec.php new file mode 100644 index 0000000..de20cad --- /dev/null +++ b/spec/Cache/Generator/HeaderCacheKeyGeneratorSpec.php @@ -0,0 +1,37 @@ +beConstructedWith(['Authorization', 'Content-Type']); + } + + public function it_is_initializable() + { + $this->shouldHaveType('Http\Client\Common\Plugin\Cache\Generator\HeaderCacheKeyGenerator'); + } + + public function it_is_a_key_generator() + { + $this->shouldImplement('Http\Client\Common\Plugin\Cache\Generator\CacheKeyGenerator'); + } + + public function it_generates_cache_from_request(RequestInterface $request, StreamInterface $body) + { + $request->getMethod()->shouldBeCalled()->willReturn('GET'); + $request->getUri()->shouldBeCalled()->willReturn('http://example.com/foo'); + $request->getHeaderLine('Authorization')->shouldBeCalled()->willReturn('bar'); + $request->getHeaderLine('Content-Type')->shouldBeCalled()->willReturn('application/baz'); + $request->getBody()->shouldBeCalled()->willReturn($body); + $body->__toString()->shouldBeCalled()->willReturn(''); + + $this->generate($request)->shouldReturn('GET http://example.com/foo Authorization:"bar" Content-Type:"application/baz" '); + } +} diff --git a/src/Cache/Generator/HeaderCacheKeyGenerator.php b/src/Cache/Generator/HeaderCacheKeyGenerator.php new file mode 100644 index 0000000..562ed48 --- /dev/null +++ b/src/Cache/Generator/HeaderCacheKeyGenerator.php @@ -0,0 +1,38 @@ + + */ +class HeaderCacheKeyGenerator implements CacheKeyGenerator +{ + /** + * The header names we should take into account when creating the cache key. + * + * @var array + */ + private $headerNames; + + /** + * @param $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(); + } +} From a1c0b7d32f089113176bc505f06090cb4c86ea07 Mon Sep 17 00:00:00 2001 From: Jeroen Thora Date: Wed, 5 Apr 2017 20:30:41 +0200 Subject: [PATCH 26/97] Only throw deprecation when user explicitly sets option --- src/CachePlugin.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CachePlugin.php b/src/CachePlugin.php index fd7d87f..c97666d 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -338,7 +338,7 @@ private function configureOptions(OptionsResolver $resolver) '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' => true, + 'respect_cache_headers' => null, 'hash_algo' => 'sha1', 'methods' => ['GET', 'HEAD'], 'respect_response_cache_directives' => ['no-cache', 'private', 'max-age', 'no-store'], @@ -347,7 +347,7 @@ private function configureOptions(OptionsResolver $resolver) $resolver->setAllowedTypes('cache_lifetime', ['int', 'null']); $resolver->setAllowedTypes('default_ttl', ['int', 'null']); - $resolver->setAllowedTypes('respect_cache_headers', 'bool'); + $resolver->setAllowedTypes('respect_cache_headers', ['bool', 'null']); $resolver->setAllowedTypes('methods', 'array'); $resolver->setAllowedTypes('cache_key_generator', ['null', 'Http\Client\Common\Plugin\Cache\Generator\CacheKeyGenerator']); $resolver->setAllowedValues('hash_algo', hash_algos()); @@ -363,7 +363,7 @@ private function configureOptions(OptionsResolver $resolver) @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 $value; + return null === $value ? true : $value; }); $resolver->setNormalizer('respect_response_cache_directives', function (Options $options, $value) { From 041927c294d79725ee43f82bc01407a063fc485f Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Wed, 5 Apr 2017 22:04:43 +0200 Subject: [PATCH 27/97] Added change log --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba560d5..d61eaef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Change Log +## 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 From 7545810446fdbd7e2fdf94230a8d784c7d4646f3 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Thu, 6 Apr 2017 05:46:01 +0200 Subject: [PATCH 28/97] Updated branch alias --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 293c61d..8e3d115 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.5-dev" } } } From 5ae4cffbc2ef22bcd82b3b442ee5d1a811be3b99 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Wed, 26 Apr 2017 13:32:55 +0200 Subject: [PATCH 29/97] clarify cache_key_generator option --- src/CachePlugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CachePlugin.php b/src/CachePlugin.php index c97666d..a6dccdd 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -58,7 +58,7 @@ final class CachePlugin implements Plugin * We store a cache item for $cache_lifetime + max age of the response. * @var array $methods list of request methods which can be cached * @var array $respect_response_cache_directives list of cache directives this plugin will respect while caching responses - * @var CacheKeyGenerator $cache_key_generator a class to generate the cache key. Defaults to SimpleGenerator + * @var CacheKeyGenerator $cache_key_generator an object to generate the cache key. Defaults to a new instance of SimpleGenerator * } */ public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamFactory, array $config = []) From 46def0baf550ebae273b766ccdaf54ff2b4bdf95 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 30 Jul 2017 00:07:45 +0100 Subject: [PATCH 30/97] The etag from data can never be a string --- src/CachePlugin.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/CachePlugin.php b/src/CachePlugin.php index a6dccdd..7667433 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -435,10 +435,6 @@ private function getETag(CacheItemInterface $cacheItem) return; } - if (!is_array($data['etag'])) { - return $data['etag']; - } - foreach ($data['etag'] as $etag) { if (!empty($etag)) { return $etag; From fe49989cf9031232636794b601f9bf83e776c454 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Mon, 31 Jul 2017 08:36:43 +0200 Subject: [PATCH 31/97] fix hhvm build on travis-ci --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 44dc5af..dee74e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,6 @@ php: - 5.6 - 7.0 - 7.1 - - hhvm env: global: @@ -27,6 +26,8 @@ matrix: include: - php: 5.4 env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" COVERAGE=true TEST_COMMAND="composer test-ci" + - php: hhvm + dist: trusty before_install: - if [[ $COVERAGE != true ]]; then phpenv config-rm xdebug.ini || true; fi From b7f7e098d3bd6b7ffdcbda9b42fd0162345c5de6 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Sat, 18 Nov 2017 12:12:38 +0200 Subject: [PATCH 32/97] Symfony 4 support --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 8e3d115..3ce18be 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "psr/cache": "^1.0", "php-http/client-common": "^1.1", "php-http/message-factory": "^1.0", - "symfony/options-resolver": "^2.6 || ^3.0" + "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0" }, "require-dev": { "phpspec/phpspec": "^2.5", From c547d282899b1ed1d3db6891cfaa82003fef7f93 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Sat, 18 Nov 2017 12:17:48 +0200 Subject: [PATCH 33/97] Style fixes --- src/CachePlugin.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 7667433..166cf9b 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -139,7 +139,7 @@ public function handleRequest(RequestInterface $request, callable $next, callabl if ($cacheItem->isHit()) { $data = $cacheItem->get(); // The array_key_exists() is to be removed in 2.0. - if (array_key_exists('expiresAt', $data) && ($data['expiresAt'] === null || time() < $data['expiresAt'])) { + if (array_key_exists('expiresAt', $data) && (null === $data['expiresAt'] || time() < $data['expiresAt'])) { // This item is still valid according to previous cache headers return new FulfilledPromise($this->createResponseFromCacheItem($cacheItem)); } @@ -210,7 +210,7 @@ public function handleRequest(RequestInterface $request, callable $next, callabl */ private function calculateCacheItemExpiresAfter($maxAge) { - if ($this->config['cache_lifetime'] === null && $maxAge === null) { + if (null === $this->config['cache_lifetime'] && null === $maxAge) { return; } @@ -227,7 +227,7 @@ private function calculateCacheItemExpiresAfter($maxAge) */ private function calculateResponseExpiresAt($maxAge) { - if ($maxAge === null) { + if (null === $maxAge) { return; } From c84e1300c2d844d8c2c113f52b31285a2ddc9056 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Wed, 29 Nov 2017 20:25:50 +0100 Subject: [PATCH 34/97] Added changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d61eaef..795a3ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Change Log +## 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 From 5ae8a805966987a12f5185897914c2966acdf09e Mon Sep 17 00:00:00 2001 From: Iain Connor Date: Mon, 29 Jan 2018 10:52:48 -0600 Subject: [PATCH 35/97] Adding cache listener (#48) --- CHANGELOG.md | 6 +++ composer.json | 2 +- src/Cache/Listener/AddHeaderCacheListener.php | 42 ++++++++++++++++++ src/Cache/Listener/CacheListener.php | 30 +++++++++++++ src/CachePlugin.php | 43 ++++++++++++++++--- 5 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 src/Cache/Listener/AddHeaderCacheListener.php create mode 100644 src/Cache/Listener/CacheListener.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 795a3ff..e822f6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 1.6.0 - 2017-01-29 + +### Added + +* 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 diff --git a/composer.json b/composer.json index 3ce18be..d875cdb 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.5-dev" + "dev-master": "1.6-dev" } } } diff --git a/src/Cache/Listener/AddHeaderCacheListener.php b/src/Cache/Listener/AddHeaderCacheListener.php new file mode 100644 index 0000000..ed306e3 --- /dev/null +++ b/src/Cache/Listener/AddHeaderCacheListener.php @@ -0,0 +1,42 @@ + + */ +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 RequestInterface $request + * @param ResponseInterface $response + * @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'); + } +} diff --git a/src/Cache/Listener/CacheListener.php b/src/Cache/Listener/CacheListener.php new file mode 100644 index 0000000..3bf6007 --- /dev/null +++ b/src/Cache/Listener/CacheListener.php @@ -0,0 +1,30 @@ + + */ +interface CacheListener +{ + /** + * Called before the cache plugin returns the response, with information on whether that response came from cache. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @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); +} diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 166cf9b..8225b75 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -6,6 +6,7 @@ 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\Client\Common\Plugin\Cache\Listener\CacheListener; use Http\Message\StreamFactory; use Http\Promise\FulfilledPromise; use Psr\Cache\CacheItemInterface; @@ -59,6 +60,8 @@ final class CachePlugin implements Plugin * @var array $methods list of request methods which can be cached * @var array $respect_response_cache_directives list of cache directives this plugin will respect while caching responses * @var CacheKeyGenerator $cache_key_generator an object to generate the cache key. Defaults to a new instance of SimpleGenerator + * @var 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, StreamFactory $streamFactory, array $config = []) @@ -129,7 +132,11 @@ public function handleRequest(RequestInterface $request, callable $next, callabl $method = strtoupper($request->getMethod()); // if the request not is cachable, move to $next if (!in_array($method, $this->config['methods'])) { - return $next($request); + return $next($request)->then(function (ResponseInterface $response) use ($request) { + $response = $this->handleCacheListeners($request, $response, false, null); + + return $response; + }); } // If we can cache the request @@ -141,7 +148,10 @@ public function handleRequest(RequestInterface $request, callable $next, callabl // 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 - return new FulfilledPromise($this->createResponseFromCacheItem($cacheItem)); + $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 @@ -154,14 +164,14 @@ public function handleRequest(RequestInterface $request, callable $next, callabl } } - return $next($request)->then(function (ResponseInterface $response) use ($cacheItem) { + 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 $response; + return $this->handleCacheListeners($request, $response, false, $cacheItem); } // The cached response we have is still valid @@ -171,7 +181,7 @@ public function handleRequest(RequestInterface $request, callable $next, callabl $cacheItem->set($data)->expiresAfter($this->calculateCacheItemExpiresAfter($maxAge)); $this->pool->save($cacheItem); - return $this->createResponseFromCacheItem($cacheItem); + return $this->handleCacheListeners($request, $this->createResponseFromCacheItem($cacheItem), true, $cacheItem); } if ($this->isCacheable($response)) { @@ -196,7 +206,7 @@ public function handleRequest(RequestInterface $request, callable $next, callabl $this->pool->save($cacheItem); } - return $response; + return $this->handleCacheListeners($request, $response, false, isset($cacheItem) ? $cacheItem : null); }); } @@ -343,6 +353,7 @@ private function configureOptions(OptionsResolver $resolver) 'methods' => ['GET', 'HEAD'], 'respect_response_cache_directives' => ['no-cache', 'private', 'max-age', 'no-store'], 'cache_key_generator' => null, + 'cache_listeners' => [], ]); $resolver->setAllowedTypes('cache_lifetime', ['int', 'null']); @@ -357,6 +368,7 @@ private function configureOptions(OptionsResolver $resolver) return empty($matches); }); + $resolver->setAllowedTypes('cache_listeners', ['array']); $resolver->setNormalizer('respect_cache_headers', function (Options $options, $value) { if (null !== $value) { @@ -441,4 +453,23 @@ private function getETag(CacheItemInterface $cacheItem) } } } + + /** + * Call the cache listeners, if they are set. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @param bool $cacheHit + * @param CacheItemInterface|null $cacheItem + * + * @return ResponseInterface + */ + private function handleCacheListeners(RequestInterface $request, ResponseInterface $response, $cacheHit, $cacheItem) + { + foreach ($this->config['cache_listeners'] as $cacheListener) { + $response = $cacheListener->onCacheResponse($request, $response, $cacheHit, $cacheItem); + } + + return $response; + } } From 561b54482003a0290e3619c095ac790c90590db9 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Sat, 29 Dec 2018 14:58:28 +0100 Subject: [PATCH 36/97] Support both client-common ^1.9 and 2.x (#52) * Support both client-common ^1.9 and 2.x * Use Traits * Make sure we test with both 2.0 and 1.9 of client-common * Drop tests on hhvm * No need to have special test for client-common:1.9 That is already tested in prefer-lowest * Run test on php7.1 --- .travis.yml | 8 +++++--- composer.json | 6 ++++-- src/CachePlugin.php | 7 +++---- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index dee74e0..2c7a0b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,8 @@ php: - 5.6 - 7.0 - 7.1 + - 7.2 + - 7.3 env: global: @@ -26,8 +28,8 @@ matrix: include: - php: 5.4 env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" COVERAGE=true TEST_COMMAND="composer test-ci" - - php: hhvm - dist: trusty + - php: 7.1 + env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" before_install: - if [[ $COVERAGE != true ]]; then phpenv config-rm xdebug.ini || true; fi @@ -35,7 +37,7 @@ before_install: install: # To be removed when this issue will be resolved: https://github.com/composer/composer/issues/5355 - if [[ "$COMPOSER_FLAGS" == *"--prefer-lowest"* ]]; then travis_retry composer update --prefer-dist --no-interaction --prefer-stable --quiet; fi - - travis_retry composer update ${COMPOSER_FLAGS} --prefer-dist --no-interaction + - composer update ${COMPOSER_FLAGS} --prefer-dist --no-interaction script: - $TEST_COMMAND diff --git a/composer.json b/composer.json index d875cdb..355e82a 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "require": { "php": "^5.4 || ^7.0", "psr/cache": "^1.0", - "php-http/client-common": "^1.1", + "php-http/client-common": "^1.9 || ^2.0", "php-http/message-factory": "^1.0", "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0" }, @@ -39,5 +39,7 @@ "branch-alias": { "dev-master": "1.6-dev" } - } + }, + "prefer-stable": true, + "minimum-stability": "dev" } diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 8225b75..5ca9b6d 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -23,6 +23,8 @@ */ final class CachePlugin implements Plugin { + use VersionBridgePlugin; + /** * @var CacheItemPoolInterface */ @@ -124,10 +126,7 @@ public static function serverCache(CacheItemPoolInterface $pool, StreamFactory $ return new self($pool, $streamFactory, $config); } - /** - * {@inheritdoc} - */ - public function handleRequest(RequestInterface $request, callable $next, callable $first) + protected function doHandleRequest(RequestInterface $request, callable $next, callable $first) { $method = strtoupper($request->getMethod()); // if the request not is cachable, move to $next From a250aa2f14a49884c69405e99877cc0947c8d54a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20S=C3=A1gi-Kaz=C3=A1r?= Date: Tue, 8 Jan 2019 07:16:42 +0100 Subject: [PATCH 37/97] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e822f6b..5e22476 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log -## 1.6.0 - 2017-01-29 +## Unreleased ### Added From 8e2505d2090316fac7cce637b39b6bbb5249c5a8 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Wed, 23 Jan 2019 17:51:58 +0100 Subject: [PATCH 38/97] prepare release --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e22476..4491df1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ # Change Log -## Unreleased +## 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 From 511f2d3fea885270fa94905c8b8767e67b47733e Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Wed, 5 Jun 2019 09:37:37 +0200 Subject: [PATCH 39/97] spec is not analyzed by styleci - but we should use short array syntax --- spec/CachePluginSpec.php | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/spec/CachePluginSpec.php b/spec/CachePluginSpec.php index af8a483..7139b97 100644 --- a/spec/CachePluginSpec.php +++ b/spec/CachePluginSpec.php @@ -46,9 +46,9 @@ function it_caches_responses(CacheItemPoolInterface $pool, CacheItemInterface $i $response->getStatusCode()->willReturn(200); $response->getBody()->willReturn($stream); - $response->getHeader('Cache-Control')->willReturn(array())->shouldBeCalled(); - $response->getHeader('Expires')->willReturn(array())->shouldBeCalled(); - $response->getHeader('ETag')->willReturn(array())->shouldBeCalled(); + $response->getHeader('Cache-Control')->willReturn([])->shouldBeCalled(); + $response->getHeader('Expires')->willReturn([])->shouldBeCalled(); + $response->getHeader('ETag')->willReturn([])->shouldBeCalled(); $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); @@ -77,8 +77,8 @@ function it_doesnt_store_failed_responses(CacheItemPoolInterface $pool, CacheIte $request->getBody()->shouldBeCalled(); $response->getStatusCode()->willReturn(400); - $response->getHeader('Cache-Control')->willReturn(array()); - $response->getHeader('Expires')->willReturn(array()); + $response->getHeader('Cache-Control')->willReturn([]); + $response->getHeader('Expires')->willReturn([]); $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); @@ -184,10 +184,10 @@ function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemI $response->getStatusCode()->willReturn(200); $response->getBody()->willReturn($stream); - $response->getHeader('Cache-Control')->willReturn(array('max-age=40')); - $response->getHeader('Age')->willReturn(array('15')); - $response->getHeader('Expires')->willReturn(array()); - $response->getHeader('ETag')->willReturn(array()); + $response->getHeader('Cache-Control')->willReturn(['max-age=40']); + $response->getHeader('Age')->willReturn(['15']); + $response->getHeader('Expires')->willReturn([]); + $response->getHeader('ETag')->willReturn([]); $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); @@ -222,9 +222,9 @@ function it_saves_etag(CacheItemPoolInterface $pool, CacheItemInterface $item, R $request->getUri()->willReturn('/'); $response->getStatusCode()->willReturn(200); $response->getBody()->willReturn($stream); - $response->getHeader('Cache-Control')->willReturn(array()); - $response->getHeader('Expires')->willReturn(array()); - $response->getHeader('ETag')->willReturn(array('foo_etag')); + $response->getHeader('Cache-Control')->willReturn([]); + $response->getHeader('Expires')->willReturn([]); + $response->getHeader('ETag')->willReturn(['foo_etag']); $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); @@ -317,8 +317,8 @@ function it_serves_and_resaved_expired_response(CacheItemPoolInterface $pool, Ca $request->withHeader(Argument::any(), Argument::any())->willReturn($request); $response->getStatusCode()->willReturn(304); - $response->getHeader('Cache-Control')->willReturn(array()); - $response->getHeader('Expires')->willReturn(array())->shouldBeCalled(); + $response->getHeader('Cache-Control')->willReturn([]); + $response->getHeader('Expires')->willReturn([])->shouldBeCalled(); // Make sure we add back the body $response->withBody($stream)->willReturn($response)->shouldBeCalled(); @@ -377,8 +377,8 @@ function it_caches_private_responses_when_allowed( $response->getStatusCode()->willReturn(200); $response->getBody()->willReturn($stream); $response->getHeader('Cache-Control')->willReturn(['private'])->shouldBeCalled(); - $response->getHeader('Expires')->willReturn(array())->shouldBeCalled(); - $response->getHeader('ETag')->willReturn(array())->shouldBeCalled(); + $response->getHeader('Expires')->willReturn([])->shouldBeCalled(); + $response->getHeader('ETag')->willReturn([])->shouldBeCalled(); $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); From f727b953bb39349a7588f66200de831b8e9fea86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20de=20Guillebon?= Date: Wed, 5 Jun 2019 17:11:06 +0200 Subject: [PATCH 40/97] Clean PHP versions --- .travis.yml | 6 +----- composer.json | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2c7a0b1..61ab88d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,10 +7,6 @@ cache: - $HOME/.composer/cache/files php: - - 5.4 - - 5.5 - - 5.6 - - 7.0 - 7.1 - 7.2 - 7.3 @@ -26,7 +22,7 @@ branches: matrix: fast_finish: true include: - - php: 5.4 + - php: 7.1 env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" COVERAGE=true TEST_COMMAND="composer test-ci" - php: 7.1 env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" diff --git a/composer.json b/composer.json index 355e82a..96f5996 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ } ], "require": { - "php": "^5.4 || ^7.0", + "php": "^7.1", "psr/cache": "^1.0", "php-http/client-common": "^1.9 || ^2.0", "php-http/message-factory": "^1.0", From 02dbeeadae1f1bcc92c206bd31280362cbb6ea4f Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 17 Nov 2019 15:32:11 +0000 Subject: [PATCH 41/97] Support Symfony 5 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 96f5996..46d4f18 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "psr/cache": "^1.0", "php-http/client-common": "^1.9 || ^2.0", "php-http/message-factory": "^1.0", - "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0" + "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0 || ^5.0" }, "require-dev": { "phpspec/phpspec": "^2.5", From 47ac710a987ed49bb15968eca037fcb9b2366d94 Mon Sep 17 00:00:00 2001 From: Till Hildebrandt Date: Mon, 25 Nov 2019 14:09:02 +0100 Subject: [PATCH 42/97] Added blacklisted_paths option - adds a list of strings (regular expressions) to describe patterns of paths not to be cached. --- CHANGELOG.md | 6 ++++++ src/CachePlugin.php | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4491df1..8ec1737 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 1.6.1 - 2019-11-25 + +### Added + +* 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 diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 5ca9b6d..0f0cafb 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -60,6 +60,7 @@ final class CachePlugin implements Plugin * 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. * @var array $methods list of request methods which can be cached + * @var array $blacklisted_paths list of regex patterns of paths explicitly not to be cached. * @var array $respect_response_cache_directives list of cache directives this plugin will respect while caching responses * @var CacheKeyGenerator $cache_key_generator an object to generate the cache key. Defaults to a new instance of SimpleGenerator * @var CacheListener[] $cache_listeners an array of objects to act on the response based on the results of the cache check. @@ -183,7 +184,7 @@ protected function doHandleRequest(RequestInterface $request, callable $next, ca return $this->handleCacheListeners($request, $this->createResponseFromCacheItem($cacheItem), true, $cacheItem); } - if ($this->isCacheable($response)) { + if ($this->isCacheable($request, $response)) { $bodyStream = $response->getBody(); $body = $bodyStream->__toString(); if ($bodyStream->isSeekable()) { @@ -246,16 +247,23 @@ private function calculateResponseExpiresAt($maxAge) /** * Verify that we can cache this response. * + * @param RequestInterface $request * @param ResponseInterface $response * * @return bool */ - protected function isCacheable(ResponseInterface $response) + protected function isCacheable(RequestInterface $request, ResponseInterface $response) { if (!in_array($response->getStatusCode(), [200, 203, 300, 301, 302, 404, 410])) { return false; } + foreach ($this->config['blacklisted_paths'] as $not_to_cache_path) { + if (1 === preg_match('/'.$not_to_cache_path.'/', $request->getRequestTarget())) { + return false; + } + } + $nocacheDirectives = array_intersect($this->config['respect_response_cache_directives'], $this->noCacheFlags); foreach ($nocacheDirectives as $nocacheDirective) { if ($this->getCacheControlDirective($response, $nocacheDirective)) { @@ -353,6 +361,7 @@ private function configureOptions(OptionsResolver $resolver) 'respect_response_cache_directives' => ['no-cache', 'private', 'max-age', 'no-store'], 'cache_key_generator' => null, 'cache_listeners' => [], + 'blacklisted_paths' => [], // restricted for ]); $resolver->setAllowedTypes('cache_lifetime', ['int', 'null']); @@ -360,6 +369,7 @@ private function configureOptions(OptionsResolver $resolver) $resolver->setAllowedTypes('respect_cache_headers', ['bool', 'null']); $resolver->setAllowedTypes('methods', 'array'); $resolver->setAllowedTypes('cache_key_generator', ['null', 'Http\Client\Common\Plugin\Cache\Generator\CacheKeyGenerator']); + $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. */ From 5fcfc394f149a83e257733b754e8fadc791e6306 Mon Sep 17 00:00:00 2001 From: Till Hildebrandt Date: Mon, 25 Nov 2019 14:40:56 +0100 Subject: [PATCH 43/97] Fix CI --- src/CachePlugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 0f0cafb..06b2ea3 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -247,7 +247,7 @@ private function calculateResponseExpiresAt($maxAge) /** * Verify that we can cache this response. * - * @param RequestInterface $request + * @param RequestInterface $request * @param ResponseInterface $response * * @return bool From 0ed5c85b6ac8dd86efd3f79b60252f6c9f58c03f Mon Sep 17 00:00:00 2001 From: Till Hildebrandt Date: Mon, 25 Nov 2019 14:43:26 +0100 Subject: [PATCH 44/97] Fix CI # 2 - honestly, the lengths of a line should be fixed. --- src/CachePlugin.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 06b2ea3..6848e05 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -60,7 +60,7 @@ final class CachePlugin implements Plugin * 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. * @var array $methods list of request methods which can be cached - * @var array $blacklisted_paths list of regex patterns of paths explicitly not to be cached. + * @var array $blacklisted_paths list of regex patterns of paths explicitly not to be cached * @var array $respect_response_cache_directives list of cache directives this plugin will respect while caching responses * @var CacheKeyGenerator $cache_key_generator an object to generate the cache key. Defaults to a new instance of SimpleGenerator * @var CacheListener[] $cache_listeners an array of objects to act on the response based on the results of the cache check. @@ -73,12 +73,10 @@ public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamF $this->streamFactory = $streamFactory; if (isset($config['respect_cache_headers']) && isset($config['respect_response_cache_directives'])) { - throw new \InvalidArgumentException( - 'You can\'t provide config option "respect_cache_headers" and "respect_response_cache_directives". '. - 'Use "respect_response_cache_directives" instead.' - ); + 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); From a9e1d11af4334052365514088385ac700f0048b6 Mon Sep 17 00:00:00 2001 From: Till Hildebrandt Date: Mon, 25 Nov 2019 14:44:42 +0100 Subject: [PATCH 45/97] Fix CI # 3 - that has been a fine restriction --- src/CachePlugin.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 6848e05..d4e5b74 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -76,7 +76,6 @@ public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamF 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); From 005d609442dd14e8e9d11fcd8f7fb5f990957c81 Mon Sep 17 00:00:00 2001 From: Till Hildebrandt Date: Mon, 25 Nov 2019 18:11:37 +0100 Subject: [PATCH 46/97] Fixing Pull Request Requests - outsourced checking of blacklisted_paths to new method "isCacheableRequest" - removed related functionality from "isCacheable" - removed obsolete comment --- CHANGELOG.md | 2 +- src/CachePlugin.php | 29 ++++++++++++++++++++--------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ec1737..ffe9ddd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log -## 1.6.1 - 2019-11-25 +## 1.7.0 - unreleased ### Added diff --git a/src/CachePlugin.php b/src/CachePlugin.php index d4e5b74..f2749da 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -181,7 +181,7 @@ protected function doHandleRequest(RequestInterface $request, callable $next, ca return $this->handleCacheListeners($request, $this->createResponseFromCacheItem($cacheItem), true, $cacheItem); } - if ($this->isCacheable($request, $response)) { + if ($this->isCacheable($response) && $this->isCacheableRequest($request)) { $bodyStream = $response->getBody(); $body = $bodyStream->__toString(); if ($bodyStream->isSeekable()) { @@ -244,26 +244,37 @@ private function calculateResponseExpiresAt($maxAge) /** * Verify that we can cache this response. * - * @param RequestInterface $request * @param ResponseInterface $response * * @return bool */ - protected function isCacheable(RequestInterface $request, ResponseInterface $response) + protected function isCacheable(ResponseInterface $response) { if (!in_array($response->getStatusCode(), [200, 203, 300, 301, 302, 404, 410])) { return false; } - foreach ($this->config['blacklisted_paths'] as $not_to_cache_path) { - if (1 === preg_match('/'.$not_to_cache_path.'/', $request->getRequestTarget())) { + $nocacheDirectives = array_intersect($this->config['respect_response_cache_directives'], $this->noCacheFlags); + foreach ($nocacheDirectives as $nocacheDirective) { + if ($this->getCacheControlDirective($response, $nocacheDirective)) { return false; } } - $nocacheDirectives = array_intersect($this->config['respect_response_cache_directives'], $this->noCacheFlags); - foreach ($nocacheDirectives as $nocacheDirective) { - if ($this->getCacheControlDirective($response, $nocacheDirective)) { + return true; + } + + /** + * Verify that we can cache this request. + * + * @param RequestInterface $request + * + * @return bool + */ + protected function isCacheableRequest(RequestInterface $request) + { + foreach ($this->config['blacklisted_paths'] as $not_to_cache_path) { + if (1 === preg_match('/'.$not_to_cache_path.'/', $request->getRequestTarget())) { return false; } } @@ -358,7 +369,7 @@ private function configureOptions(OptionsResolver $resolver) 'respect_response_cache_directives' => ['no-cache', 'private', 'max-age', 'no-store'], 'cache_key_generator' => null, 'cache_listeners' => [], - 'blacklisted_paths' => [], // restricted for + 'blacklisted_paths' => [], ]); $resolver->setAllowedTypes('cache_lifetime', ['int', 'null']); From 817a4d8d48d23f5e873a20992d8f2e5eeefef72c Mon Sep 17 00:00:00 2001 From: Till Hildebrandt Date: Tue, 26 Nov 2019 10:00:50 +0100 Subject: [PATCH 47/97] Fixing Pull Request Requests - added spec tests for blacklisted paths - it_does_not_store_responses_of_requests_to_blacklisted_paths - it_stores_responses_of_requests_not_in_blacklisted_paths --- spec/CachePluginSpec.php | 92 ++++++++++++++++++++++++++++++++++++++++ src/CachePlugin.php | 2 +- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/spec/CachePluginSpec.php b/spec/CachePluginSpec.php index 7139b97..7e94bc5 100644 --- a/spec/CachePluginSpec.php +++ b/spec/CachePluginSpec.php @@ -400,6 +400,98 @@ function it_caches_private_responses_when_allowed( $this->handleRequest($request, $next, function () {}); } + function it_does_not_store_responses_of_requests_to_blacklisted_paths( + CacheItemPoolInterface $pool, + CacheItemInterface $item, + RequestInterface $request, + ResponseInterface $response, + StreamFactory $streamFactory, + StreamInterface $stream + ) { + $this->beConstructedThrough('clientCache', [$pool, $streamFactory, [ + 'default_ttl' => 60, + 'cache_lifetime' => 1000, + 'blacklisted_paths' => ['\/foo'] + ]]); + + $httpBody = 'body'; + $stream->__toString()->willReturn($httpBody); + $stream->isSeekable()->willReturn(true); + + $request->getMethod()->willReturn('GET'); + $request->getUri()->willReturn('/foo'); + $request->getBody()->shouldBeCalled(); + + $response->getStatusCode()->willReturn(200); + $response->getBody()->willReturn($stream); + $response->getHeader('Cache-Control')->willReturn([])->shouldBeCalled(); + + $pool->getItem('231392a16d98e1cf631845c79b7d45f40bab08f3')->shouldBeCalled()->willReturn($item); + $item->isHit()->willReturn(false); + + $item->set($this->getCacheItemMatcher([ + 'response' => $response->getWrappedObject(), + 'body' => $httpBody, + 'expiresAt' => 0, + 'createdAt' => 0 + ]))->willReturn($item)->shouldNotBeCalled(); + $pool->save(Argument::any())->shouldNotBeCalled(); + + $next = function (RequestInterface $request) use ($response) { + return new FulfilledPromise($response->getWrappedObject()); + }; + + $this->handleRequest($request, $next, function () {}); + } + + function it_stores_responses_of_requests_not_in_blacklisted_paths( + CacheItemPoolInterface $pool, + CacheItemInterface $item, + RequestInterface $request, + ResponseInterface $response, + StreamFactory $streamFactory, + StreamInterface $stream + ) { + $this->beConstructedThrough('clientCache', [$pool, $streamFactory, [ + 'default_ttl' => 60, + 'cache_lifetime' => 1000, + 'blacklisted_paths' => ['\/foo'] + ]]); + + $httpBody = 'body'; + $stream->__toString()->willReturn($httpBody); + $stream->isSeekable()->willReturn(true); + $stream->rewind()->shouldBeCalled(); + + $request->getMethod()->willReturn('GET'); + $request->getUri()->willReturn('/'); + $request->getBody()->shouldBeCalled(); + + $response->getStatusCode()->willReturn(200); + $response->getBody()->willReturn($stream); + $response->getHeader('Cache-Control')->willReturn([])->shouldBeCalled(); + $response->getHeader('Expires')->willReturn([])->shouldBeCalled(); + $response->getHeader('ETag')->willReturn([])->shouldBeCalled(); + + $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); + $item->isHit()->willReturn(false); + $item->expiresAfter(1060)->willReturn($item)->shouldBeCalled(); + + $item->set($this->getCacheItemMatcher([ + 'response' => $response->getWrappedObject(), + 'body' => $httpBody, + 'expiresAt' => 0, + 'createdAt' => 0, + 'etag' => [] + ]))->willReturn($item)->shouldBeCalled(); + $pool->save(Argument::any())->shouldBeCalled(); + + $next = function (RequestInterface $request) use ($response) { + return new FulfilledPromise($response->getWrappedObject()); + }; + + $this->handleRequest($request, $next, function () {}); + } function it_can_be_initialized_with_custom_cache_key_generator( CacheItemPoolInterface $pool, diff --git a/src/CachePlugin.php b/src/CachePlugin.php index f2749da..21f45c5 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -274,7 +274,7 @@ protected function isCacheable(ResponseInterface $response) protected function isCacheableRequest(RequestInterface $request) { foreach ($this->config['blacklisted_paths'] as $not_to_cache_path) { - if (1 === preg_match('/'.$not_to_cache_path.'/', $request->getRequestTarget())) { + if (1 === preg_match('/'.$not_to_cache_path.'/', $request->getUri())) { return false; } } From ba4b1aab7c6c236c052595ad4d47adca5326885f Mon Sep 17 00:00:00 2001 From: Till Hildebrandt Date: Tue, 26 Nov 2019 10:04:13 +0100 Subject: [PATCH 48/97] Fixing CI --- src/CachePlugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 21f45c5..e093d7a 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -267,7 +267,7 @@ protected function isCacheable(ResponseInterface $response) /** * Verify that we can cache this request. * - * @param RequestInterface $request + * @param RequestInterface $request * * @return bool */ From 61aab950a803e9a26df4cf46502df04a936c449b Mon Sep 17 00:00:00 2001 From: Till Hildebrandt Date: Tue, 26 Nov 2019 10:24:54 +0100 Subject: [PATCH 49/97] Implemented Feedback - removed unnecessary string concatenation - set isCacheableRequest to private --- src/CachePlugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CachePlugin.php b/src/CachePlugin.php index e093d7a..805d361 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -73,7 +73,7 @@ public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamF $this->streamFactory = $streamFactory; if (isset($config['respect_cache_headers']) && isset($config['respect_response_cache_directives'])) { - throw new \InvalidArgumentException('You can\'t provide config option "respect_cache_headers" and "respect_response_cache_directives". '.'Use "respect_response_cache_directives" instead.'); + 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(); @@ -271,7 +271,7 @@ protected function isCacheable(ResponseInterface $response) * * @return bool */ - protected function isCacheableRequest(RequestInterface $request) + private function isCacheableRequest(RequestInterface $request) { foreach ($this->config['blacklisted_paths'] as $not_to_cache_path) { if (1 === preg_match('/'.$not_to_cache_path.'/', $request->getUri())) { From 15a81333961431ac23b5727b6991f19c80e98d9b Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Mon, 9 Dec 2019 08:36:36 +0100 Subject: [PATCH 50/97] build with php 7.4 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 61ab88d..015fc22 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ php: - 7.1 - 7.2 - 7.3 + - 7.4 env: global: From 0b3e08b9206e2976b0fea6743c289cfce3d8d631 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Mon, 9 Dec 2019 08:36:41 +0100 Subject: [PATCH 51/97] improve phpdoc --- src/CachePlugin.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 805d361..f637689 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -17,7 +17,9 @@ use Symfony\Component\OptionsResolver\OptionsResolver; /** - * Allow for caching a response. + * 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 */ From a4e5617949091e46b7fb39d65b591a98b4ee85c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20de=20Guillebon?= Date: Wed, 5 Jun 2019 16:44:21 +0200 Subject: [PATCH 52/97] Support PSR-17 StreamFactoryInterface --- src/CachePlugin.php | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/CachePlugin.php b/src/CachePlugin.php index f637689..5dfd7f4 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -13,6 +13,7 @@ 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; @@ -33,7 +34,7 @@ final class CachePlugin implements Plugin private $pool; /** - * @var StreamFactory + * @var StreamFactory|StreamFactoryInterface */ private $streamFactory; @@ -50,9 +51,9 @@ final class CachePlugin implements Plugin private $noCacheFlags = ['no-cache', 'private', 'no-store']; /** - * @param CacheItemPoolInterface $pool - * @param StreamFactory $streamFactory - * @param array $config { + * @param CacheItemPoolInterface $pool + * @param StreamFactory|StreamFactoryInterface $streamFactory + * @param array $config { * * @var bool $respect_cache_headers Whether to look at the cache directives or ignore them * @var int $default_ttl (seconds) If we do not respect cache headers or can't calculate a good ttl, use this @@ -69,8 +70,12 @@ final class CachePlugin implements Plugin * Defaults to an empty array * } */ - public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamFactory, array $config = []) + public function __construct(CacheItemPoolInterface $pool, $streamFactory, array $config = []) { + if (!($streamFactory instanceof StreamFactory) && !($streamFactory instanceof StreamFactoryInterface)) { + throw new \TypeError(\sprintf('Argument 2 passed to %s::__construct() must be of type %s|%s, %s given.', self::class, StreamFactory::class, StreamFactoryInterface::class, \is_object($streamFactory) ? \get_class($streamFactory) : \gettype($streamFactory))); + } + $this->pool = $pool; $this->streamFactory = $streamFactory; @@ -91,13 +96,13 @@ public function __construct(CacheItemPoolInterface $pool, StreamFactory $streamF * 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 CacheItemPoolInterface $pool - * @param StreamFactory $streamFactory - * @param array $config For all possible config options see the constructor docs + * @param CacheItemPoolInterface $pool + * @param StreamFactory|StreamFactoryInterface $streamFactory + * @param array $config For all possible config options see the constructor docs * * @return CachePlugin */ - public static function clientCache(CacheItemPoolInterface $pool, StreamFactory $streamFactory, array $config = []) + public static function clientCache(CacheItemPoolInterface $pool, $streamFactory, array $config = []) { // Allow caching of private requests if (isset($config['respect_response_cache_directives'])) { @@ -115,13 +120,13 @@ public static function clientCache(CacheItemPoolInterface $pool, StreamFactory $ * 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 CacheItemPoolInterface $pool - * @param StreamFactory $streamFactory - * @param array $config For all possible config options see the constructor docs + * @param CacheItemPoolInterface $pool + * @param StreamFactory|StreamFactoryInterface $streamFactory + * @param array $config For all possible config options see the constructor docs * * @return CachePlugin */ - public static function serverCache(CacheItemPoolInterface $pool, StreamFactory $streamFactory, array $config = []) + public static function serverCache(CacheItemPoolInterface $pool, $streamFactory, array $config = []) { return new self($pool, $streamFactory, $config); } From cd77610bb8233b36ba0a180170878e7a0a47904c Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Thu, 26 Dec 2019 17:14:01 +0100 Subject: [PATCH 53/97] Bugfixes with blacklist (#65) * Bugfixes with blacklist * typo --- spec/CachePluginSpec.php | 87 ++++++++++++++++++++++++---------------- src/CachePlugin.php | 7 ++-- 2 files changed, 57 insertions(+), 37 deletions(-) diff --git a/spec/CachePluginSpec.php b/spec/CachePluginSpec.php index 7e94bc5..75b7c33 100644 --- a/spec/CachePluginSpec.php +++ b/spec/CachePluginSpec.php @@ -12,6 +12,7 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; +use Psr\Http\Message\UriInterface; class CachePluginSpec extends ObjectBehavior { @@ -33,7 +34,7 @@ function it_is_a_plugin() $this->shouldImplement('Http\Client\Common\Plugin'); } - function it_caches_responses(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response, StreamInterface $stream) + function it_caches_responses(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, ResponseInterface $response, StreamInterface $stream) { $httpBody = 'body'; $stream->__toString()->willReturn($httpBody); @@ -41,7 +42,8 @@ function it_caches_responses(CacheItemPoolInterface $pool, CacheItemInterface $i $stream->rewind()->shouldBeCalled(); $request->getMethod()->willReturn('GET'); - $request->getUri()->willReturn('/'); + $request->getUri()->willReturn($uri); + $uri->__toString()->willReturn('https://example.com/'); $request->getBody()->shouldBeCalled(); $response->getStatusCode()->willReturn(200); @@ -50,7 +52,7 @@ function it_caches_responses(CacheItemPoolInterface $pool, CacheItemInterface $i $response->getHeader('Expires')->willReturn([])->shouldBeCalled(); $response->getHeader('ETag')->willReturn([])->shouldBeCalled(); - $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); + $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); $item->expiresAfter(1060)->willReturn($item)->shouldBeCalled(); @@ -70,17 +72,18 @@ function it_caches_responses(CacheItemPoolInterface $pool, CacheItemInterface $i $this->handleRequest($request, $next, function () {}); } - function it_doesnt_store_failed_responses(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response) + function it_doesnt_store_failed_responses(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, ResponseInterface $response) { $request->getMethod()->willReturn('GET'); - $request->getUri()->willReturn('/'); + $request->getUri()->willReturn($uri); + $uri->__toString()->willReturn('https://example.com/'); $request->getBody()->shouldBeCalled(); $response->getStatusCode()->willReturn(400); $response->getHeader('Cache-Control')->willReturn([]); $response->getHeader('Expires')->willReturn([]); - $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); + $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); $next = function (RequestInterface $request) use ($response) { @@ -90,10 +93,11 @@ function it_doesnt_store_failed_responses(CacheItemPoolInterface $pool, CacheIte $this->handleRequest($request, $next, function () {}); } - function it_doesnt_store_post_requests_by_default(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response) + function it_doesnt_store_post_requests_by_default(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, ResponseInterface $response) { $request->getMethod()->willReturn('POST'); - $request->getUri()->willReturn('/'); + $request->getUri()->willReturn($uri); + $uri->__toString()->willReturn('https://example.com/'); $next = function (RequestInterface $request) use ($response) { return new FulfilledPromise($response->getWrappedObject()); @@ -106,6 +110,7 @@ function it_stores_post_requests_when_allowed( CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, + UriInterface $uri, ResponseInterface $response, StreamFactory $streamFactory, StreamInterface $stream @@ -122,7 +127,8 @@ function it_stores_post_requests_when_allowed( $stream->rewind()->shouldBeCalled(); $request->getMethod()->willReturn('POST'); - $request->getUri()->willReturn('/post'); + $request->getUri()->willReturn($uri); + $uri->__toString()->willReturn('https://example.com/'); $request->getBody()->willReturn($stream); $response->getStatusCode()->willReturn(200); @@ -131,7 +137,7 @@ function it_stores_post_requests_when_allowed( $response->getHeader('Expires')->willReturn([])->shouldBeCalled(); $response->getHeader('ETag')->willReturn([])->shouldBeCalled(); - $pool->getItem('e4311a9af932c603b400a54efab21b6d7dea7a90')->shouldBeCalled()->willReturn($item); + $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); $item->expiresAfter(1060)->willReturn($item)->shouldBeCalled(); @@ -171,7 +177,7 @@ function it_does_not_allow_invalid_request_methods( ->during('__construct', [$pool, $streamFactory, ['methods' => ['GET', 'head', 'POST']]]); } - function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response, StreamInterface $stream) + function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, ResponseInterface $response, StreamInterface $stream) { $httpBody = 'body'; $stream->__toString()->willReturn($httpBody); @@ -179,7 +185,8 @@ function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemI $stream->rewind()->shouldBeCalled(); $request->getMethod()->willReturn('GET'); - $request->getUri()->willReturn('/'); + $request->getUri()->willReturn($uri); + $uri->__toString()->willReturn('https://example.com/'); $request->getBody()->shouldBeCalled(); $response->getStatusCode()->willReturn(200); @@ -189,7 +196,7 @@ function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemI $response->getHeader('Expires')->willReturn([]); $response->getHeader('ETag')->willReturn([]); - $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); + $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); $item->set($this->getCacheItemMatcher([ @@ -210,7 +217,7 @@ function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemI $this->handleRequest($request, $next, function () {}); } - function it_saves_etag(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response, StreamInterface $stream) + function it_saves_etag(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, ResponseInterface $response, StreamInterface $stream) { $httpBody = 'body'; $stream->__toString()->willReturn($httpBody); @@ -219,14 +226,15 @@ function it_saves_etag(CacheItemPoolInterface $pool, CacheItemInterface $item, R $request->getBody()->shouldBeCalled(); $request->getMethod()->willReturn('GET'); - $request->getUri()->willReturn('/'); + $request->getUri()->willReturn($uri); + $uri->__toString()->willReturn('https://example.com/'); $response->getStatusCode()->willReturn(200); $response->getBody()->willReturn($stream); $response->getHeader('Cache-Control')->willReturn([]); $response->getHeader('Expires')->willReturn([]); $response->getHeader('ETag')->willReturn(['foo_etag']); - $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); + $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); $item->expiresAfter(1060)->willReturn($item); @@ -246,12 +254,13 @@ function it_saves_etag(CacheItemPoolInterface $pool, CacheItemInterface $item, R $this->handleRequest($request, $next, function () {}); } - function it_adds_etag_and_modfied_since_to_request(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response, StreamInterface $stream) + function it_adds_etag_and_modfied_since_to_request(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, ResponseInterface $response, StreamInterface $stream) { $httpBody = 'body'; $request->getMethod()->willReturn('GET'); - $request->getUri()->willReturn('/'); + $request->getUri()->willReturn($uri); + $uri->__toString()->willReturn('https://example.com/'); $request->getBody()->shouldBeCalled(); $request->withHeader('If-Modified-Since', 'Thursday, 01-Jan-70 01:18:31 GMT')->shouldBeCalled()->willReturn($request); @@ -259,7 +268,7 @@ function it_adds_etag_and_modfied_since_to_request(CacheItemPoolInterface $pool, $response->getStatusCode()->willReturn(304); - $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); + $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(true, false); $item->get()->willReturn([ 'response' => $response, @@ -276,15 +285,16 @@ function it_adds_etag_and_modfied_since_to_request(CacheItemPoolInterface $pool, $this->handleRequest($request, $next, function () {}); } - function it_servces_a_cached_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response, StreamInterface $stream, StreamFactory $streamFactory) + function it_servces_a_cached_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, ResponseInterface $response, StreamInterface $stream, StreamFactory $streamFactory) { $httpBody = 'body'; $request->getMethod()->willReturn('GET'); - $request->getUri()->willReturn('/'); + $request->getUri()->willReturn($uri); + $uri->__toString()->willReturn('https://example.com/'); $request->getBody()->shouldBeCalled(); - $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); + $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(true); $item->get()->willReturn([ 'response' => $response, @@ -305,12 +315,13 @@ function it_servces_a_cached_response(CacheItemPoolInterface $pool, CacheItemInt $this->handleRequest($request, $next, function () {}); } - function it_serves_and_resaved_expired_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, ResponseInterface $response, StreamInterface $stream, StreamFactory $streamFactory) + function it_serves_and_resaved_expired_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, ResponseInterface $response, StreamInterface $stream, StreamFactory $streamFactory) { $httpBody = 'body'; $request->getMethod()->willReturn('GET'); - $request->getUri()->willReturn('/'); + $request->getUri()->willReturn($uri); + $uri->__toString()->willReturn('https://example.com/'); $request->getBody()->shouldBeCalled(); $request->withHeader(Argument::any(), Argument::any())->willReturn($request); @@ -323,7 +334,7 @@ function it_serves_and_resaved_expired_response(CacheItemPoolInterface $pool, Ca // Make sure we add back the body $response->withBody($stream)->willReturn($response)->shouldBeCalled(); - $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); + $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(true, true); $item->expiresAfter(1060)->willReturn($item)->shouldBeCalled(); $item->get()->willReturn([ @@ -356,6 +367,7 @@ function it_caches_private_responses_when_allowed( CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, + UriInterface $uri, ResponseInterface $response, StreamFactory $streamFactory, StreamInterface $stream @@ -371,7 +383,8 @@ function it_caches_private_responses_when_allowed( $stream->rewind()->shouldBeCalled(); $request->getMethod()->willReturn('GET'); - $request->getUri()->willReturn('/'); + $request->getUri()->willReturn($uri); + $uri->__toString()->willReturn('https://example.com/'); $request->getBody()->shouldBeCalled(); $response->getStatusCode()->willReturn(200); @@ -380,7 +393,7 @@ function it_caches_private_responses_when_allowed( $response->getHeader('Expires')->willReturn([])->shouldBeCalled(); $response->getHeader('ETag')->willReturn([])->shouldBeCalled(); - $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); + $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); $item->expiresAfter(1060)->willReturn($item)->shouldBeCalled(); @@ -404,6 +417,7 @@ function it_does_not_store_responses_of_requests_to_blacklisted_paths( CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, + UriInterface $uri, ResponseInterface $response, StreamFactory $streamFactory, StreamInterface $stream @@ -411,7 +425,7 @@ function it_does_not_store_responses_of_requests_to_blacklisted_paths( $this->beConstructedThrough('clientCache', [$pool, $streamFactory, [ 'default_ttl' => 60, 'cache_lifetime' => 1000, - 'blacklisted_paths' => ['\/foo'] + 'blacklisted_paths' => ['@/foo@'] ]]); $httpBody = 'body'; @@ -419,14 +433,15 @@ function it_does_not_store_responses_of_requests_to_blacklisted_paths( $stream->isSeekable()->willReturn(true); $request->getMethod()->willReturn('GET'); - $request->getUri()->willReturn('/foo'); + $request->getUri()->willReturn($uri); + $uri->__toString()->willReturn('https://example.com/foo'); $request->getBody()->shouldBeCalled(); $response->getStatusCode()->willReturn(200); $response->getBody()->willReturn($stream); $response->getHeader('Cache-Control')->willReturn([])->shouldBeCalled(); - $pool->getItem('231392a16d98e1cf631845c79b7d45f40bab08f3')->shouldBeCalled()->willReturn($item); + $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); $item->set($this->getCacheItemMatcher([ @@ -448,6 +463,7 @@ function it_stores_responses_of_requests_not_in_blacklisted_paths( CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, + UriInterface $uri, ResponseInterface $response, StreamFactory $streamFactory, StreamInterface $stream @@ -455,7 +471,7 @@ function it_stores_responses_of_requests_not_in_blacklisted_paths( $this->beConstructedThrough('clientCache', [$pool, $streamFactory, [ 'default_ttl' => 60, 'cache_lifetime' => 1000, - 'blacklisted_paths' => ['\/foo'] + 'blacklisted_paths' => ['@/foo@'] ]]); $httpBody = 'body'; @@ -464,7 +480,8 @@ function it_stores_responses_of_requests_not_in_blacklisted_paths( $stream->rewind()->shouldBeCalled(); $request->getMethod()->willReturn('GET'); - $request->getUri()->willReturn('/'); + $request->getUri()->willReturn($uri); + $uri->__toString()->willReturn('https://example.com/'); $request->getBody()->shouldBeCalled(); $response->getStatusCode()->willReturn(200); @@ -473,7 +490,7 @@ function it_stores_responses_of_requests_not_in_blacklisted_paths( $response->getHeader('Expires')->willReturn([])->shouldBeCalled(); $response->getHeader('ETag')->willReturn([])->shouldBeCalled(); - $pool->getItem('d20f64acc6e70b6079845f2fe357732929550ae1')->shouldBeCalled()->willReturn($item); + $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); $item->expiresAfter(1060)->willReturn($item)->shouldBeCalled(); @@ -498,6 +515,7 @@ function it_can_be_initialized_with_custom_cache_key_generator( CacheItemInterface $item, StreamFactory $streamFactory, RequestInterface $request, + UriInterface $uri, ResponseInterface $response, StreamInterface $stream, SimpleGenerator $generator @@ -513,7 +531,8 @@ function it_can_be_initialized_with_custom_cache_key_generator( $streamFactory->createStream(Argument::any())->willReturn($stream); $request->getMethod()->willReturn('GET'); - $request->getUri()->willReturn('/'); + $request->getUri()->willReturn($uri); + $uri->__toString()->willReturn('https://example.com/'); $response->withBody(Argument::any())->willReturn($response); $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 5dfd7f4..34d009c 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -63,7 +63,7 @@ final class CachePlugin implements Plugin * 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. * @var array $methods list of request methods which can be cached - * @var array $blacklisted_paths list of regex patterns of paths explicitly not to be cached + * @var array $blacklisted_paths list of regex for URLs explicitly not to be cached * @var array $respect_response_cache_directives list of cache directives this plugin will respect while caching responses * @var CacheKeyGenerator $cache_key_generator an object to generate the cache key. Defaults to a new instance of SimpleGenerator * @var CacheListener[] $cache_listeners an array of objects to act on the response based on the results of the cache check. @@ -280,8 +280,9 @@ protected function isCacheable(ResponseInterface $response) */ private function isCacheableRequest(RequestInterface $request) { - foreach ($this->config['blacklisted_paths'] as $not_to_cache_path) { - if (1 === preg_match('/'.$not_to_cache_path.'/', $request->getUri())) { + $uri = $request->getUri()->__toString(); + foreach ($this->config['blacklisted_paths'] as $regex) { + if (1 === preg_match($regex, $uri)) { return false; } } From d137d46523343297e340cef697747381b6caeb66 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Thu, 26 Dec 2019 17:14:58 +0100 Subject: [PATCH 54/97] Prepare for 1.7.0 (#64) --- CHANGELOG.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffe9ddd..557e211 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,11 @@ # Change Log -## 1.7.0 - unreleased +## 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 @@ -21,18 +23,18 @@ ### Changed -* Removed check if etag is a string. Etag can never be a string, it is always an array. +* 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 +### 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. +- Issue where deprecation warning always was triggered. Not it is just triggered if `respect_cache_headers` is used. ## 1.3.0 - 2017-03-28 @@ -40,13 +42,13 @@ of this interface: `SimpleGenerator` (default) and `HeaderCacheKeyGenerator`. - 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 +- 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. +- We always rewind the stream after loading response from cache. ### Deprecated From 2895cfb85435e01492b9973cd8678d8ec008efd7 Mon Sep 17 00:00:00 2001 From: Iain Connor Date: Sat, 4 Jul 2020 03:48:28 -0500 Subject: [PATCH 55/97] Updating author attribution. (#66) --- src/Cache/Listener/AddHeaderCacheListener.php | 2 +- src/Cache/Listener/CacheListener.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Cache/Listener/AddHeaderCacheListener.php b/src/Cache/Listener/AddHeaderCacheListener.php index ed306e3..8d51229 100644 --- a/src/Cache/Listener/AddHeaderCacheListener.php +++ b/src/Cache/Listener/AddHeaderCacheListener.php @@ -9,7 +9,7 @@ /** * Adds a header indicating if the response came from cache. * - * @author Iain Connor + * @author Iain Connor */ class AddHeaderCacheListener implements CacheListener { diff --git a/src/Cache/Listener/CacheListener.php b/src/Cache/Listener/CacheListener.php index 3bf6007..5e15cab 100644 --- a/src/Cache/Listener/CacheListener.php +++ b/src/Cache/Listener/CacheListener.php @@ -11,7 +11,7 @@ * 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 + * @author Iain Connor */ interface CacheListener { From 5b3aedd86cdbbe2797e24c4590fabd6cfcdf83f0 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Mon, 13 Jul 2020 09:39:30 +0100 Subject: [PATCH 56/97] PHP 8.0 support --- .gitattributes | 3 -- .github/workflows/tests.yml | 91 +++++++++++++++++++++++++++++++++++++ .travis.yml | 44 ------------------ composer.json | 9 ++-- phpspec.ci.yml | 2 +- 5 files changed, 95 insertions(+), 54 deletions(-) create mode 100644 .github/workflows/tests.yml delete mode 100644 .travis.yml diff --git a/.gitattributes b/.gitattributes index c47225a..533d355 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,11 +5,8 @@ /.php_cs export-ignore /.scrutinizer.yml export-ignore /.styleci.yml export-ignore -/.travis.yml export-ignore /behat.yml.dist export-ignore /features/ export-ignore /phpspec.ci.yml export-ignore /phpspec.yml.dist export-ignore -/phpunit.xml.dist export-ignore /spec/ export-ignore -/tests/ export-ignore diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..e4d0050 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,91 @@ +name: tests + +on: + push: + 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'] + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer:v2 + coverage: none + + - name: Install PHP 7 dependencies + run: composer update --prefer-dist --no-interaction --no-progress + if: "matrix.php != '8.0'" + + - name: Install PHP 8 dependencies + run: | + composer require "phpdocumentor/reflection-docblock:^5.2@dev" --no-interaction --no-update + composer update --prefer-dist --prefer-stable --no-interaction --no-progress --ignore-platform-req=php + if: "matrix.php == '8.0'" + + - name: Execute tests + run: composer test + + lowest: + name: PHP ${{ matrix.php }} Lowest + runs-on: ubuntu-latest + strategy: + matrix: + php: ['7.1', '7.2', '7.3', '7.4'] + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - 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@v2 + + - 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:^4.3.2" --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 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 015fc22..0000000 --- a/.travis.yml +++ /dev/null @@ -1,44 +0,0 @@ -language: php - -sudo: false - -cache: - directories: - - $HOME/.composer/cache/files - -php: - - 7.1 - - 7.2 - - 7.3 - - 7.4 - -env: - global: - - TEST_COMMAND="composer test" - -branches: - except: - - /^analysis-.*$/ - -matrix: - fast_finish: true - include: - - php: 7.1 - env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" COVERAGE=true TEST_COMMAND="composer test-ci" - - php: 7.1 - env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" - -before_install: - - if [[ $COVERAGE != true ]]; then phpenv config-rm xdebug.ini || true; fi - -install: - # To be removed when this issue will be resolved: https://github.com/composer/composer/issues/5355 - - if [[ "$COMPOSER_FLAGS" == *"--prefer-lowest"* ]]; then travis_retry composer update --prefer-dist --no-interaction --prefer-stable --quiet; fi - - composer update ${COMPOSER_FLAGS} --prefer-dist --no-interaction - -script: - - $TEST_COMMAND - -after_success: - - if [[ $COVERAGE = true ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi - - if [[ $COVERAGE = true ]]; then php ocular.phar code-coverage:upload --format=php-clover build/coverage.xml; fi diff --git a/composer.json b/composer.json index 46d4f18..ff4587a 100644 --- a/composer.json +++ b/composer.json @@ -11,15 +11,14 @@ } ], "require": { - "php": "^7.1", + "php": "^7.1 || ^8.0", "psr/cache": "^1.0", "php-http/client-common": "^1.9 || ^2.0", "php-http/message-factory": "^1.0", "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0 || ^5.0" }, "require-dev": { - "phpspec/phpspec": "^2.5", - "henrikbjorn/phpspec-code-coverage" : "^1.0" + "phpspec/phpspec": "^5.1 || ^6.0" }, "autoload": { "psr-4": { @@ -39,7 +38,5 @@ "branch-alias": { "dev-master": "1.6-dev" } - }, - "prefer-stable": true, - "minimum-stability": "dev" + } } diff --git a/phpspec.ci.yml b/phpspec.ci.yml index 4407704..bd039ca 100644 --- a/phpspec.ci.yml +++ b/phpspec.ci.yml @@ -4,7 +4,7 @@ suites: psr4_prefix: Http\Client\Common\Plugin formatter.name: pretty extensions: - - PhpSpec\Extension\CodeCoverageExtension + FriendsOfPhpSpec\PhpSpec\CodeCoverage\CodeCoverageExtension: ~ code_coverage: format: clover output: build/coverage.xml From a205d61d18f7896d647bed9930354af58db7e93f Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Mon, 13 Jul 2020 11:58:37 +0100 Subject: [PATCH 57/97] Release 1.7.1 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 557e211..862e0f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 1.7.1 - 2020-07-13 + +### Fixed + +- Support for PHP 8 + ## 1.7.0 - 2019-12-17 ### Added From de2a0826ebef495cb590b98814973aadfea9b9f6 Mon Sep 17 00:00:00 2001 From: Valentin Nazarov Date: Tue, 13 Apr 2021 17:47:04 +0300 Subject: [PATCH 58/97] [Compatibility] Support psr/cache 2.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ff4587a..1c5636a 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ ], "require": { "php": "^7.1 || ^8.0", - "psr/cache": "^1.0", + "psr/cache": "^1.0 || ^2.0", "php-http/client-common": "^1.9 || ^2.0", "php-http/message-factory": "^1.0", "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0 || ^5.0" From 7d0f0c66ae6c9faa928718b89bcea3197494e8d1 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Wed, 14 Apr 2021 07:58:17 +0200 Subject: [PATCH 59/97] remove setting as it is in the default symfony ruleset --- .styleci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.styleci.yml b/.styleci.yml index 3417c41..1e6efad 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -6,6 +6,3 @@ finder: path: - "src" - "tests" - -enabled: - - short_array_syntax From 952fd02c06c1fd633ed4f58cabe0906bc70f5e52 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Wed, 14 Apr 2021 06:00:12 +0000 Subject: [PATCH 60/97] Apply fixes from StyleCI [ci skip] [skip ci] --- src/Cache/Generator/CacheKeyGenerator.php | 2 -- src/Cache/Listener/AddHeaderCacheListener.php | 2 -- src/Cache/Listener/CacheListener.php | 2 -- src/CachePlugin.php | 24 +------------------ 4 files changed, 1 insertion(+), 29 deletions(-) diff --git a/src/Cache/Generator/CacheKeyGenerator.php b/src/Cache/Generator/CacheKeyGenerator.php index d351e57..3010256 100644 --- a/src/Cache/Generator/CacheKeyGenerator.php +++ b/src/Cache/Generator/CacheKeyGenerator.php @@ -14,8 +14,6 @@ interface CacheKeyGenerator /** * Generate a cache key from a Request. * - * @param RequestInterface $request - * * @return string */ public function generate(RequestInterface $request); diff --git a/src/Cache/Listener/AddHeaderCacheListener.php b/src/Cache/Listener/AddHeaderCacheListener.php index 8d51229..282a8a3 100644 --- a/src/Cache/Listener/AddHeaderCacheListener.php +++ b/src/Cache/Listener/AddHeaderCacheListener.php @@ -27,8 +27,6 @@ public function __construct($headerName = 'X-Cache') /** * Called before the cache plugin returns the response, with information on whether that response came from cache. * - * @param RequestInterface $request - * @param ResponseInterface $response * @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 diff --git a/src/Cache/Listener/CacheListener.php b/src/Cache/Listener/CacheListener.php index 5e15cab..01b953c 100644 --- a/src/Cache/Listener/CacheListener.php +++ b/src/Cache/Listener/CacheListener.php @@ -18,8 +18,6 @@ interface CacheListener /** * Called before the cache plugin returns the response, with information on whether that response came from cache. * - * @param RequestInterface $request - * @param ResponseInterface $response * @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 diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 34d009c..69fd523 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -51,7 +51,6 @@ final class CachePlugin implements Plugin private $noCacheFlags = ['no-cache', 'private', 'no-store']; /** - * @param CacheItemPoolInterface $pool * @param StreamFactory|StreamFactoryInterface $streamFactory * @param array $config { * @@ -96,7 +95,6 @@ public function __construct(CacheItemPoolInterface $pool, $streamFactory, array * 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 CacheItemPoolInterface $pool * @param StreamFactory|StreamFactoryInterface $streamFactory * @param array $config For all possible config options see the constructor docs * @@ -120,7 +118,6 @@ public static function clientCache(CacheItemPoolInterface $pool, $streamFactory, * 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 CacheItemPoolInterface $pool * @param StreamFactory|StreamFactoryInterface $streamFactory * @param array $config For all possible config options see the constructor docs * @@ -251,8 +248,6 @@ private function calculateResponseExpiresAt($maxAge) /** * Verify that we can cache this response. * - * @param ResponseInterface $response - * * @return bool */ protected function isCacheable(ResponseInterface $response) @@ -274,8 +269,6 @@ protected function isCacheable(ResponseInterface $response) /** * Verify that we can cache this request. * - * @param RequestInterface $request - * * @return bool */ private function isCacheableRequest(RequestInterface $request) @@ -293,8 +286,7 @@ private function isCacheableRequest(RequestInterface $request) /** * Get the value of a parameter in the cache control header. * - * @param ResponseInterface $response - * @param string $name The field of Cache-Control to fetch + * @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 */ @@ -316,8 +308,6 @@ private function getCacheControlDirective(ResponseInterface $response, $name) } /** - * @param RequestInterface $request - * * @return string */ private function createCacheKey(RequestInterface $request) @@ -330,8 +320,6 @@ private function createCacheKey(RequestInterface $request) /** * Get a ttl in seconds. It could return null if we do not respect cache headers and got no defaultTtl. * - * @param ResponseInterface $response - * * @return int|null */ private function getMaxAge(ResponseInterface $response) @@ -362,8 +350,6 @@ private function getMaxAge(ResponseInterface $response) /** * Configure an options resolver. - * - * @param OptionsResolver $resolver */ private function configureOptions(OptionsResolver $resolver) { @@ -413,8 +399,6 @@ private function configureOptions(OptionsResolver $resolver) } /** - * @param CacheItemInterface $cacheItem - * * @return ResponseInterface */ private function createResponseFromCacheItem(CacheItemInterface $cacheItem) @@ -439,8 +423,6 @@ private function createResponseFromCacheItem(CacheItemInterface $cacheItem) /** * Get the value of the "If-Modified-Since" header. * - * @param CacheItemInterface $cacheItem - * * @return string|null */ private function getModifiedSinceHeaderValue(CacheItemInterface $cacheItem) @@ -460,8 +442,6 @@ private function getModifiedSinceHeaderValue(CacheItemInterface $cacheItem) /** * Get the ETag from the cached response. * - * @param CacheItemInterface $cacheItem - * * @return string|null */ private function getETag(CacheItemInterface $cacheItem) @@ -482,8 +462,6 @@ private function getETag(CacheItemInterface $cacheItem) /** * Call the cache listeners, if they are set. * - * @param RequestInterface $request - * @param ResponseInterface $response * @param bool $cacheHit * @param CacheItemInterface|null $cacheItem * From 922409f10541b0d581b8ffe5cd810f4efc9e9e32 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Wed, 14 Apr 2021 08:06:08 +0200 Subject: [PATCH 61/97] prepare release --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 862e0f3..008df49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,14 @@ # Change Log +## 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 -### Fixed +### Added - Support for PHP 8 From 99bba506e527d62edb4f419eac9a086b883acf27 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Mon, 25 Oct 2021 08:20:10 +0200 Subject: [PATCH 62/97] add phpstan to ci build --- .github/workflows/.editorconfig | 2 ++ .github/workflows/static.yml | 21 +++++++++++++++++++++ .github/workflows/tests.yml | 2 ++ phpstan.neon.dist | 4 ++++ 4 files changed, 29 insertions(+) create mode 100644 .github/workflows/.editorconfig create mode 100644 .github/workflows/static.yml create mode 100644 phpstan.neon.dist diff --git a/.github/workflows/.editorconfig b/.github/workflows/.editorconfig new file mode 100644 index 0000000..7bd3346 --- /dev/null +++ b/.github/workflows/.editorconfig @@ -0,0 +1,2 @@ +[*.yml] +indent_size = 2 diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 0000000..4e821aa --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,21 @@ +name: Static analysis + +on: + push: + branches: + - master + pull_request: + +jobs: + phpstan: + name: PHPStan + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: PHPStan + uses: docker://oskarstark/phpstan-ga + with: + args: analyze --no-progress diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e4d0050..1bbd75a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,6 +2,8 @@ name: tests on: push: + branches: + - master pull_request: jobs: diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..77c3651 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,4 @@ +parameters: + level: 1 + paths: + - src From 7079a0886a73e52e894afe70a061fdf4ddfa49a4 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Mon, 25 Oct 2021 08:29:26 +0200 Subject: [PATCH 63/97] fixes for phpstan level 1 --- src/CachePlugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 69fd523..38cb335 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -207,7 +207,7 @@ protected function doHandleRequest(RequestInterface $request, callable $next, ca $this->pool->save($cacheItem); } - return $this->handleCacheListeners($request, $response, false, isset($cacheItem) ? $cacheItem : null); + return $this->handleCacheListeners($request, $response, false, $cacheItem); }); } From 66aa9f237f2982140f09b7e0e6aa5f3af43228c7 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Mon, 25 Oct 2021 08:29:43 +0200 Subject: [PATCH 64/97] phpstan level 2 --- phpstan.neon.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 77c3651..093e815 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,4 +1,4 @@ parameters: - level: 1 + level: 2 paths: - src From 297fda539522e4f844ed72155875c709ce0afcf4 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Mon, 25 Oct 2021 08:32:45 +0200 Subject: [PATCH 65/97] fix phpstan level 2 --- src/Cache/Generator/HeaderCacheKeyGenerator.php | 4 ++-- src/CachePlugin.php | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Cache/Generator/HeaderCacheKeyGenerator.php b/src/Cache/Generator/HeaderCacheKeyGenerator.php index 562ed48..16587be 100644 --- a/src/Cache/Generator/HeaderCacheKeyGenerator.php +++ b/src/Cache/Generator/HeaderCacheKeyGenerator.php @@ -14,12 +14,12 @@ class HeaderCacheKeyGenerator implements CacheKeyGenerator /** * The header names we should take into account when creating the cache key. * - * @var array + * @var string[] */ private $headerNames; /** - * @param $headerNames + * @param string[] $headerNames */ public function __construct(array $headerNames) { diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 38cb335..9074a99 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -333,7 +333,7 @@ private function getMaxAge(ResponseInterface $response) if (!is_bool($maxAge)) { $ageHeaders = $response->getHeader('Age'); foreach ($ageHeaders as $age) { - return $maxAge - ((int) $age); + return ((int) $maxAge) - ((int) $age); } return (int) $maxAge; @@ -449,7 +449,7 @@ private function getETag(CacheItemInterface $cacheItem) $data = $cacheItem->get(); // The isset() is to be removed in 2.0. if (!isset($data['etag'])) { - return; + return null; } foreach ($data['etag'] as $etag) { @@ -457,6 +457,8 @@ private function getETag(CacheItemInterface $cacheItem) return $etag; } } + + return null; } /** From 664c7fdf4a7f520a7c5a378aad032cc2269ef02b Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Mon, 25 Oct 2021 08:34:30 +0200 Subject: [PATCH 66/97] phpstan level 3 --- phpstan.neon.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 093e815..acd135a 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,4 +1,4 @@ parameters: - level: 2 + level: 3 paths: - src From aa82a78c80f36ba4c8619b06b1afea20a83e261f Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Mon, 25 Oct 2021 08:35:41 +0200 Subject: [PATCH 67/97] fix phpstan level 3 --- src/CachePlugin.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 9074a99..0112261 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -222,7 +222,7 @@ protected function doHandleRequest(RequestInterface $request, callable $next, ca private function calculateCacheItemExpiresAfter($maxAge) { if (null === $this->config['cache_lifetime'] && null === $maxAge) { - return; + return null; } return $this->config['cache_lifetime'] + $maxAge; @@ -239,7 +239,7 @@ private function calculateCacheItemExpiresAfter($maxAge) private function calculateResponseExpiresAt($maxAge) { if (null === $maxAge) { - return; + return null; } return time() + $maxAge; @@ -430,7 +430,7 @@ private function getModifiedSinceHeaderValue(CacheItemInterface $cacheItem) $data = $cacheItem->get(); // The isset() is to be removed in 2.0. if (!isset($data['createdAt'])) { - return; + return null; } $modified = new \DateTime('@'.$data['createdAt']); From 757753af99ba6435bd7130002bd849118917979b Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Mon, 25 Oct 2021 08:36:00 +0200 Subject: [PATCH 68/97] phpstan level 4 --- phpstan.neon.dist | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index acd135a..c9b813b 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,4 +1,5 @@ parameters: - level: 3 + level: 4 paths: - src + treatPhpDocTypesAsCertain: false From 220a4b5458e4236400c7b8f23014777fbee93845 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Mon, 25 Oct 2021 08:42:30 +0200 Subject: [PATCH 69/97] prefer array_key_exists over isset for more robust code --- src/CachePlugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 0112261..beb4f96 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -78,7 +78,7 @@ public function __construct(CacheItemPoolInterface $pool, $streamFactory, array $this->pool = $pool; $this->streamFactory = $streamFactory; - if (isset($config['respect_cache_headers']) && isset($config['respect_response_cache_directives'])) { + 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.'); } @@ -103,7 +103,7 @@ public function __construct(CacheItemPoolInterface $pool, $streamFactory, array public static function clientCache(CacheItemPoolInterface $pool, $streamFactory, array $config = []) { // Allow caching of private requests - if (isset($config['respect_response_cache_directives'])) { + 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']); From 95a502bebbf4fc3703423813bc1e51679209bc3f Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Mon, 25 Oct 2021 08:42:42 +0200 Subject: [PATCH 70/97] phpstan level 7 --- phpstan.neon.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index c9b813b..63b0db6 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 4 + level: 7 paths: - src treatPhpDocTypesAsCertain: false From cb8755be388faa25a3ba6ae71b43387acf14908f Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Mon, 25 Oct 2021 08:57:54 +0200 Subject: [PATCH 71/97] cleanup type hints and phpdoc --- src/CachePlugin.php | 77 ++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 47 deletions(-) diff --git a/src/CachePlugin.php b/src/CachePlugin.php index beb4f96..8064638 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -9,6 +9,7 @@ use Http\Client\Common\Plugin\Cache\Listener\CacheListener; use Http\Message\StreamFactory; use Http\Promise\FulfilledPromise; +use Http\Promise\Promise; use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; use Psr\Http\Message\RequestInterface; @@ -39,20 +40,20 @@ final class CachePlugin implements Plugin private $streamFactory; /** - * @var array + * @var mixed[] */ private $config; /** * Cache directives indicating if a response can not be cached. * - * @var array + * @var string[] */ private $noCacheFlags = ['no-cache', 'private', 'no-store']; /** * @param StreamFactory|StreamFactoryInterface $streamFactory - * @param array $config { + * @param mixed[] $config { * * @var bool $respect_cache_headers Whether to look at the cache directives or ignore them * @var int $default_ttl (seconds) If we do not respect cache headers or can't calculate a good ttl, use this @@ -61,9 +62,9 @@ final class CachePlugin implements Plugin * @var int $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. - * @var array $methods list of request methods which can be cached - * @var array $blacklisted_paths list of regex for URLs explicitly not to be cached - * @var array $respect_response_cache_directives list of cache directives this plugin will respect while caching responses + * @var string[] $methods list of request methods which can be cached + * @var string[] $blacklisted_paths list of regex for URLs explicitly not to be cached + * @var string[] $respect_response_cache_directives list of cache directives this plugin will respect while caching responses * @var CacheKeyGenerator $cache_key_generator an object to generate the cache key. Defaults to a new instance of SimpleGenerator * @var 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 @@ -96,7 +97,7 @@ public function __construct(CacheItemPoolInterface $pool, $streamFactory, array * cache responses with `private` cache directive. * * @param StreamFactory|StreamFactoryInterface $streamFactory - * @param array $config For all possible config options see the constructor docs + * @param mixed[] $config For all possible config options see the constructor docs * * @return CachePlugin */ @@ -119,7 +120,7 @@ public static function clientCache(CacheItemPoolInterface $pool, $streamFactory, * cache responses with the `private`or `no-cache` directives. * * @param StreamFactory|StreamFactoryInterface $streamFactory - * @param array $config For all possible config options see the constructor docs + * @param mixed[] $config For all possible config options see the constructor docs * * @return CachePlugin */ @@ -128,6 +129,11 @@ public static function serverCache(CacheItemPoolInterface $pool, $streamFactory, 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()); @@ -215,11 +221,9 @@ protected function doHandleRequest(RequestInterface $request, callable $next, ca * Calculate the timestamp when this cache item should be dropped from the cache. The lowest value that can be * returned is $maxAge. * - * @param int|null $maxAge - * * @return int|null Unix system time passed to the PSR-6 cache */ - private function calculateCacheItemExpiresAfter($maxAge) + private function calculateCacheItemExpiresAfter(?int $maxAge): ?int { if (null === $this->config['cache_lifetime'] && null === $maxAge) { return null; @@ -232,11 +236,9 @@ private function calculateCacheItemExpiresAfter($maxAge) * 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. * - * @param int|null $maxAge - * * @return int|null Unix system time. A null value means that the response expires when the cache item expires */ - private function calculateResponseExpiresAt($maxAge) + private function calculateResponseExpiresAt(?int $maxAge): ?int { if (null === $maxAge) { return null; @@ -268,10 +270,8 @@ protected function isCacheable(ResponseInterface $response) /** * Verify that we can cache this request. - * - * @return bool */ - private function isCacheableRequest(RequestInterface $request) + private function isCacheableRequest(RequestInterface $request): bool { $uri = $request->getUri()->__toString(); foreach ($this->config['blacklisted_paths'] as $regex) { @@ -290,7 +290,7 @@ private function isCacheableRequest(RequestInterface $request) * * @return bool|string The value of the directive, true if directive without value, false if directive not present */ - private function getCacheControlDirective(ResponseInterface $response, $name) + private function getCacheControlDirective(ResponseInterface $response, string $name) { $headers = $response->getHeader('Cache-Control'); foreach ($headers as $header) { @@ -307,10 +307,7 @@ private function getCacheControlDirective(ResponseInterface $response, $name) return false; } - /** - * @return string - */ - private function createCacheKey(RequestInterface $request) + private function createCacheKey(RequestInterface $request): string { $key = $this->config['cache_key_generator']->generate($request); @@ -318,11 +315,11 @@ private function createCacheKey(RequestInterface $request) } /** - * Get a ttl in seconds. It could return null if we do not respect cache headers and got no defaultTtl. + * Get a ttl in seconds. * - * @return int|null + * Returns null if we do not respect cache headers and got no defaultTtl. */ - private function getMaxAge(ResponseInterface $response) + private function getMaxAge(ResponseInterface $response): ?int { if (!in_array('max-age', $this->config['respect_response_cache_directives'], true)) { return $this->config['default_ttl']; @@ -351,7 +348,7 @@ private function getMaxAge(ResponseInterface $response) /** * Configure an options resolver. */ - private function configureOptions(OptionsResolver $resolver) + private function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'cache_lifetime' => 86400 * 30, // 30 days @@ -398,10 +395,7 @@ private function configureOptions(OptionsResolver $resolver) }); } - /** - * @return ResponseInterface - */ - private function createResponseFromCacheItem(CacheItemInterface $cacheItem) + private function createResponseFromCacheItem(CacheItemInterface $cacheItem): ResponseInterface { $data = $cacheItem->get(); @@ -415,17 +409,13 @@ private function createResponseFromCacheItem(CacheItemInterface $cacheItem) throw new RewindStreamException('Cannot rewind stream.', 0, $e); } - $response = $response->withBody($stream); - - return $response; + return $response->withBody($stream); } /** - * Get the value of the "If-Modified-Since" header. - * - * @return string|null + * Get the value for the "If-Modified-Since" header. */ - private function getModifiedSinceHeaderValue(CacheItemInterface $cacheItem) + private function getModifiedSinceHeaderValue(CacheItemInterface $cacheItem): ?string { $data = $cacheItem->get(); // The isset() is to be removed in 2.0. @@ -441,10 +431,8 @@ private function getModifiedSinceHeaderValue(CacheItemInterface $cacheItem) /** * Get the ETag from the cached response. - * - * @return string|null */ - private function getETag(CacheItemInterface $cacheItem) + private function getETag(CacheItemInterface $cacheItem): ?string { $data = $cacheItem->get(); // The isset() is to be removed in 2.0. @@ -462,14 +450,9 @@ private function getETag(CacheItemInterface $cacheItem) } /** - * Call the cache listeners, if they are set. - * - * @param bool $cacheHit - * @param CacheItemInterface|null $cacheItem - * - * @return ResponseInterface + * Call the registered cache listeners. */ - private function handleCacheListeners(RequestInterface $request, ResponseInterface $response, $cacheHit, $cacheItem) + 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); From a519bf995d5d4332309269756e623189bda209cb Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Mon, 25 Oct 2021 08:58:45 +0200 Subject: [PATCH 72/97] phpstan level 8 --- phpstan.neon.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 63b0db6..ac454e0 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 7 + level: 8 paths: - src treatPhpDocTypesAsCertain: false From 3f0266a302dec1564fbb9771b926c3b1745fb3e4 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Mon, 25 Oct 2021 09:07:49 +0200 Subject: [PATCH 73/97] be more defensive about the value of the cache hit --- src/CachePlugin.php | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 8064638..8b60a33 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -152,22 +152,24 @@ protected function doHandleRequest(RequestInterface $request, callable $next, ca if ($cacheItem->isHit()) { $data = $cacheItem->get(); - // 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); - } + 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); - } + // 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); + if ($etag = $this->getETag($cacheItem)) { + $request = $request->withHeader('If-None-Match', $etag); + } } } From 6a55c651585756f282760b7d374599ed5dabea81 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Wed, 3 Nov 2021 08:26:41 +0100 Subject: [PATCH 74/97] prepare release --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 008df49..56551e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 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 From 7a72a150bf5757fe52a17dbf1d8355d597fa6f27 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Wed, 3 Nov 2021 08:36:06 +0100 Subject: [PATCH 75/97] adjust to newest phpstan --- src/CachePlugin.php | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 8b60a33..5318dc8 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -53,20 +53,19 @@ final class CachePlugin implements Plugin /** * @param StreamFactory|StreamFactoryInterface $streamFactory - * @param mixed[] $config { + * @param mixed[] $config * - * @var bool $respect_cache_headers Whether to look at the cache directives or ignore them - * @var int $default_ttl (seconds) If we do not respect cache headers or can't calculate a good ttl, use this - * value - * @var string $hash_algo The hashing algorithm to use when generating cache keys - * @var int $cache_lifetime (seconds) To support serving a previous stale response when the server answers 304 + * 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 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. - * @var string[] $methods list of request methods which can be cached - * @var string[] $blacklisted_paths list of regex for URLs explicitly not to be cached - * @var string[] $respect_response_cache_directives list of cache directives this plugin will respect while caching responses - * @var CacheKeyGenerator $cache_key_generator an object to generate the cache key. Defaults to a new instance of SimpleGenerator - * @var CacheListener[] $cache_listeners an array of objects to act on the response based on the results of the cache check. + * 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 * } */ From c74764bb3cd4688e019917fa59354de814a63419 Mon Sep 17 00:00:00 2001 From: Jeroen van den Enden Date: Mon, 29 Nov 2021 22:09:22 +0100 Subject: [PATCH 76/97] Support Symfony 6.x options resolver --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 1c5636a..d6581f7 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "psr/cache": "^1.0 || ^2.0", "php-http/client-common": "^1.9 || ^2.0", "php-http/message-factory": "^1.0", - "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0 || ^5.0" + "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0 || ^5.0 || ^6.0" }, "require-dev": { "phpspec/phpspec": "^5.1 || ^6.0" From 2c00b4c6d41c0c9cad4d901adaf1a7db55f98744 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Tue, 30 Nov 2021 08:06:42 +0100 Subject: [PATCH 77/97] prepare release --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56551e5..418e153 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 1.7.4 - 2021-11-30 + +### Added + +- Allow installation with Symfony 6 + ## 1.7.3 - 2021-11-03 ### Changed From ab362a1da0b68c010e272a438d3341029ef71109 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Mon, 6 Dec 2021 09:29:09 +0100 Subject: [PATCH 78/97] fix readme link to build status --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ce07b09..cef5dab 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Latest Version](https://img.shields.io/github/release/php-http/cache-plugin.svg?style=flat-square)](https://github.com/php-http/cache-plugin/releases) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) -[![Build Status](https://img.shields.io/travis/php-http/cache-plugin.svg?style=flat-square)](https://travis-ci.org/php-http/cache-plugin) +[![Build Status](https://github.com/php-http/cache-plugin/actions/workflows/tests.yml/badge.svg)](https://github.com/php-http/cache-plugin/actions/workflows/tests.yml) [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-http/cache-plugin.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/cache-plugin) [![Quality Score](https://img.shields.io/scrutinizer/g/php-http/cache-plugin.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/cache-plugin) [![Total Downloads](https://img.shields.io/packagist/dt/php-http/cache-plugin.svg?style=flat-square)](https://packagist.org/packages/php-http/cache-plugin) From 9de1da1412317af81c4a7a8f4a0354ed5a97f555 Mon Sep 17 00:00:00 2001 From: Yosh de Vos Date: Sun, 9 Jan 2022 12:51:49 +0100 Subject: [PATCH 79/97] Added psr/cache:^3.0 dependency --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d6581f7..08dc4bf 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ ], "require": { "php": "^7.1 || ^8.0", - "psr/cache": "^1.0 || ^2.0", + "psr/cache": "^1.0 || ^2.0 || ^3.0", "php-http/client-common": "^1.9 || ^2.0", "php-http/message-factory": "^1.0", "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0 || ^5.0 || ^6.0" From 63bc3f7242825c9a817db8f78e4c9703b0c471e2 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Tue, 18 Jan 2022 13:24:56 +0100 Subject: [PATCH 80/97] prepare release --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 418e153..6f00605 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 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 From 46a6d98a1c708366088f6b5df1f1710724a9e822 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 1 Jun 2022 06:10:49 +0000 Subject: [PATCH 81/97] Apply fixes from StyleCI [ci skip] [skip ci] --- src/CachePlugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 5318dc8..77de67c 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -354,7 +354,7 @@ 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 + // 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'], From bab07a55948d6a4f6d1cd1709250557c40ecd50e Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Fri, 28 Apr 2023 11:35:03 +0200 Subject: [PATCH 82/97] upgrade phpspec to return correct types from test doubles --- .github/workflows/static.yml | 2 +- .github/workflows/tests.yml | 19 ++++------ CHANGELOG.md | 5 +++ composer.json | 2 +- .../Generator/HeaderCacheKeyGeneratorSpec.php | 13 ++++--- spec/Cache/Generator/SimpleGeneratorSpec.php | 24 ++++++++----- spec/CachePluginSpec.php | 36 +++++++++++-------- 7 files changed, 59 insertions(+), 42 deletions(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 4e821aa..5762374 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: PHPStan uses: docker://oskarstark/phpstan-ga diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1bbd75a..5dcde71 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,11 +12,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['7.1', '7.2', '7.3', '7.4', '8.0'] + php: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2'] steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -25,15 +25,8 @@ jobs: tools: composer:v2 coverage: none - - name: Install PHP 7 dependencies + - name: Install PHP dependencies run: composer update --prefer-dist --no-interaction --no-progress - if: "matrix.php != '8.0'" - - - name: Install PHP 8 dependencies - run: | - composer require "phpdocumentor/reflection-docblock:^5.2@dev" --no-interaction --no-update - composer update --prefer-dist --prefer-stable --no-interaction --no-progress --ignore-platform-req=php - if: "matrix.php == '8.0'" - name: Execute tests run: composer test @@ -43,11 +36,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['7.1', '7.2', '7.3', '7.4'] + php: ['7.1', '7.4', '8.0'] steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -70,7 +63,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f00605..754de2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## 1.7.6 - 2023-04-23 + +- Test with PHP 8.1 and 8.2 +- Made phpspec tests compatible with PSR-7 2.0 strict typing + ## 1.7.5 - 2022-01-18 - Allow installation with psr/cache 3.0 (1.0 and 2.0 are still allowed too) diff --git a/composer.json b/composer.json index 08dc4bf..4283bb3 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0 || ^5.0 || ^6.0" }, "require-dev": { - "phpspec/phpspec": "^5.1 || ^6.0" + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" }, "autoload": { "psr-4": { diff --git a/spec/Cache/Generator/HeaderCacheKeyGeneratorSpec.php b/spec/Cache/Generator/HeaderCacheKeyGeneratorSpec.php index de20cad..2b2be5a 100644 --- a/spec/Cache/Generator/HeaderCacheKeyGeneratorSpec.php +++ b/spec/Cache/Generator/HeaderCacheKeyGeneratorSpec.php @@ -5,6 +5,9 @@ use PhpSpec\ObjectBehavior; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\StreamInterface; +use Http\Client\Common\Plugin\Cache\Generator\HeaderCacheKeyGenerator; +use Http\Client\Common\Plugin\Cache\Generator\CacheKeyGenerator; +use Psr\Http\Message\UriInterface; class HeaderCacheKeyGeneratorSpec extends ObjectBehavior { @@ -15,18 +18,20 @@ public function let() public function it_is_initializable() { - $this->shouldHaveType('Http\Client\Common\Plugin\Cache\Generator\HeaderCacheKeyGenerator'); + $this->shouldHaveType(HeaderCacheKeyGenerator::class); } public function it_is_a_key_generator() { - $this->shouldImplement('Http\Client\Common\Plugin\Cache\Generator\CacheKeyGenerator'); + $this->shouldImplement(CacheKeyGenerator::class); } - public function it_generates_cache_from_request(RequestInterface $request, StreamInterface $body) + public function it_generates_cache_from_request(RequestInterface $request, UriInterface $uri, StreamInterface $body) { + $uri->__toString()->shouldBeCalled()->willReturn('http://example.com/foo'); + $request->getMethod()->shouldBeCalled()->willReturn('GET'); - $request->getUri()->shouldBeCalled()->willReturn('http://example.com/foo'); + $request->getUri()->shouldBeCalled()->willReturn($uri); $request->getHeaderLine('Authorization')->shouldBeCalled()->willReturn('bar'); $request->getHeaderLine('Content-Type')->shouldBeCalled()->willReturn('application/baz'); $request->getBody()->shouldBeCalled()->willReturn($body); diff --git a/spec/Cache/Generator/SimpleGeneratorSpec.php b/spec/Cache/Generator/SimpleGeneratorSpec.php index 9ee102c..8c5f930 100644 --- a/spec/Cache/Generator/SimpleGeneratorSpec.php +++ b/spec/Cache/Generator/SimpleGeneratorSpec.php @@ -4,33 +4,41 @@ use PhpSpec\ObjectBehavior; use Psr\Http\Message\RequestInterface; +use Http\Client\Common\Plugin\Cache\Generator\CacheKeyGenerator; +use Http\Client\Common\Plugin\Cache\Generator\SimpleGenerator; +use Psr\Http\Message\StreamInterface; +use Psr\Http\Message\UriInterface; class SimpleGeneratorSpec extends ObjectBehavior { public function it_is_initializable() { - $this->shouldHaveType('Http\Client\Common\Plugin\Cache\Generator\SimpleGenerator'); + $this->shouldHaveType(SimpleGenerator::class); } public function it_is_a_key_generator() { - $this->shouldImplement('Http\Client\Common\Plugin\Cache\Generator\CacheKeyGenerator'); + $this->shouldImplement(CacheKeyGenerator::class); } - public function it_generates_cache_from_request(RequestInterface $request) + public function it_generates_cache_from_request(RequestInterface $request, UriInterface $uri, StreamInterface $body) { + $uri->__toString()->shouldBeCalled()->willReturn('http://example.com/foo'); + $body->__toString()->shouldBeCalled()->willReturn('bar'); $request->getMethod()->shouldBeCalled()->willReturn('GET'); - $request->getUri()->shouldBeCalled()->willReturn('http://example.com/foo'); - $request->getBody()->shouldBeCalled()->willReturn('bar'); + $request->getUri()->shouldBeCalled()->willReturn($uri); + $request->getBody()->shouldBeCalled()->willReturn($body); $this->generate($request)->shouldReturn('GET http://example.com/foo bar'); } - public function it_generates_cache_from_request_with_no_body(RequestInterface $request) + public function it_generates_cache_from_request_with_no_body(RequestInterface $request, UriInterface $uri, StreamInterface $body) { + $uri->__toString()->shouldBeCalled()->willReturn('http://example.com/foo'); + $body->__toString()->shouldBeCalled()->willReturn(''); $request->getMethod()->shouldBeCalled()->willReturn('GET'); - $request->getUri()->shouldBeCalled()->willReturn('http://example.com/foo'); - $request->getBody()->shouldBeCalled()->willReturn(''); + $request->getUri()->shouldBeCalled()->willReturn($uri); + $request->getBody()->shouldBeCalled()->willReturn($body); // No extra space after uri $this->generate($request)->shouldReturn('GET http://example.com/foo'); diff --git a/spec/CachePluginSpec.php b/spec/CachePluginSpec.php index 75b7c33..64390cb 100644 --- a/spec/CachePluginSpec.php +++ b/spec/CachePluginSpec.php @@ -13,6 +13,8 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; +use Http\Client\Common\Plugin\CachePlugin; +use Http\Client\Common\Plugin; class CachePluginSpec extends ObjectBehavior { @@ -26,12 +28,12 @@ function let(CacheItemPoolInterface $pool, StreamFactory $streamFactory) function it_is_initializable(CacheItemPoolInterface $pool) { - $this->shouldHaveType('Http\Client\Common\Plugin\CachePlugin'); + $this->shouldHaveType(CachePlugin::class); } function it_is_a_plugin() { - $this->shouldImplement('Http\Client\Common\Plugin'); + $this->shouldImplement(Plugin::class); } function it_caches_responses(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, ResponseInterface $response, StreamInterface $stream) @@ -44,7 +46,7 @@ function it_caches_responses(CacheItemPoolInterface $pool, CacheItemInterface $i $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn($uri); $uri->__toString()->willReturn('https://example.com/'); - $request->getBody()->shouldBeCalled(); + $request->getBody()->shouldBeCalled()->willReturn($stream); $response->getStatusCode()->willReturn(200); $response->getBody()->willReturn($stream); @@ -72,12 +74,13 @@ function it_caches_responses(CacheItemPoolInterface $pool, CacheItemInterface $i $this->handleRequest($request, $next, function () {}); } - function it_doesnt_store_failed_responses(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, ResponseInterface $response) + function it_doesnt_store_failed_responses(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, StreamInterface $requestBody, ResponseInterface $response) { $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn($uri); $uri->__toString()->willReturn('https://example.com/'); - $request->getBody()->shouldBeCalled(); + $request->getBody()->shouldBeCalled()->willReturn($requestBody); + $requestBody->__toString()->shouldBeCalled()->willReturn('body'); $response->getStatusCode()->willReturn(400); $response->getHeader('Cache-Control')->willReturn([]); @@ -187,7 +190,7 @@ function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemI $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn($uri); $uri->__toString()->willReturn('https://example.com/'); - $request->getBody()->shouldBeCalled(); + $request->getBody()->shouldBeCalled()->willReturn($stream); $response->getStatusCode()->willReturn(200); $response->getBody()->willReturn($stream); @@ -223,7 +226,7 @@ function it_saves_etag(CacheItemPoolInterface $pool, CacheItemInterface $item, R $stream->__toString()->willReturn($httpBody); $stream->isSeekable()->willReturn(true); $stream->rewind()->shouldBeCalled(); - $request->getBody()->shouldBeCalled(); + $request->getBody()->shouldBeCalled()->willReturn($stream); $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn($uri); @@ -261,7 +264,8 @@ function it_adds_etag_and_modfied_since_to_request(CacheItemPoolInterface $pool, $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn($uri); $uri->__toString()->willReturn('https://example.com/'); - $request->getBody()->shouldBeCalled(); + $request->getBody()->shouldBeCalled()->willReturn($stream); + $stream->__toString()->shouldBeCalled()->willReturn(''); $request->withHeader('If-Modified-Since', 'Thursday, 01-Jan-70 01:18:31 GMT')->shouldBeCalled()->willReturn($request); $request->withHeader('If-None-Match', 'foo_etag')->shouldBeCalled()->willReturn($request); @@ -285,14 +289,15 @@ function it_adds_etag_and_modfied_since_to_request(CacheItemPoolInterface $pool, $this->handleRequest($request, $next, function () {}); } - function it_servces_a_cached_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, ResponseInterface $response, StreamInterface $stream, StreamFactory $streamFactory) + function it_serves_a_cached_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, StreamInterface $requestBody, ResponseInterface $response, StreamInterface $stream, StreamFactory $streamFactory) { $httpBody = 'body'; $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn($uri); $uri->__toString()->willReturn('https://example.com/'); - $request->getBody()->shouldBeCalled(); + $request->getBody()->shouldBeCalled()->willReturn($requestBody); + $requestBody->__toString()->shouldBeCalled()->willReturn(''); $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(true); @@ -315,14 +320,15 @@ function it_servces_a_cached_response(CacheItemPoolInterface $pool, CacheItemInt $this->handleRequest($request, $next, function () {}); } - function it_serves_and_resaved_expired_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, ResponseInterface $response, StreamInterface $stream, StreamFactory $streamFactory) + function it_serves_and_resaved_expired_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, StreamInterface $requestStream, ResponseInterface $response, StreamInterface $stream, StreamFactory $streamFactory) { $httpBody = 'body'; $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn($uri); $uri->__toString()->willReturn('https://example.com/'); - $request->getBody()->shouldBeCalled(); + $request->getBody()->shouldBeCalled()->willReturn($requestStream); + $requestStream->__toString()->willReturn(''); $request->withHeader(Argument::any(), Argument::any())->willReturn($request); $request->withHeader(Argument::any(), Argument::any())->willReturn($request); @@ -385,7 +391,7 @@ function it_caches_private_responses_when_allowed( $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn($uri); $uri->__toString()->willReturn('https://example.com/'); - $request->getBody()->shouldBeCalled(); + $request->getBody()->shouldBeCalled()->willReturn($stream); $response->getStatusCode()->willReturn(200); $response->getBody()->willReturn($stream); @@ -435,7 +441,7 @@ function it_does_not_store_responses_of_requests_to_blacklisted_paths( $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn($uri); $uri->__toString()->willReturn('https://example.com/foo'); - $request->getBody()->shouldBeCalled(); + $request->getBody()->shouldBeCalled()->willReturn($stream); $response->getStatusCode()->willReturn(200); $response->getBody()->willReturn($stream); @@ -482,7 +488,7 @@ function it_stores_responses_of_requests_not_in_blacklisted_paths( $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn($uri); $uri->__toString()->willReturn('https://example.com/'); - $request->getBody()->shouldBeCalled(); + $request->getBody()->shouldBeCalled()->willReturn($stream); $response->getStatusCode()->willReturn(200); $response->getBody()->willReturn($stream); From 2c4cb06643bc6b8df1b5bdb4cc7c2e4cde2218e3 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Fri, 28 Apr 2023 11:45:45 +0200 Subject: [PATCH 83/97] semantic branch naming --- .github/workflows/static.yml | 2 +- .github/workflows/tests.yml | 2 +- composer.json | 5 ----- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 5762374..6644e71 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -3,7 +3,7 @@ name: Static analysis on: push: branches: - - master + - '*.x' pull_request: jobs: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5dcde71..5574164 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,7 +3,7 @@ name: tests on: push: branches: - - master + - '*.x' pull_request: jobs: diff --git a/composer.json b/composer.json index 4283bb3..d62dd77 100644 --- a/composer.json +++ b/composer.json @@ -33,10 +33,5 @@ "scripts": { "test": "vendor/bin/phpspec run", "test-ci": "vendor/bin/phpspec run -c phpspec.ci.yml" - }, - "extra": { - "branch-alias": { - "dev-master": "1.6-dev" - } } } From 87b9c61bbcf057847342711bcc93310ab1b47a71 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Wed, 1 Jun 2022 08:06:28 +0200 Subject: [PATCH 84/97] explicitly use 0 instead of adding null --- CHANGELOG.md | 1 + src/CachePlugin.php | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 754de2a..2d6048c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - 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 diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 77de67c..e0f2cbc 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -58,7 +58,7 @@ final class CachePlugin implements Plugin * 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 cache_lifetime: (seconds) To support serving a previous stale response when the server answers 304 + * 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 @@ -230,7 +230,7 @@ private function calculateCacheItemExpiresAfter(?int $maxAge): ?int return null; } - return $this->config['cache_lifetime'] + $maxAge; + return ($this->config['cache_lifetime'] ?: 0) + ($maxAge ?: 0); } /** @@ -368,7 +368,7 @@ private function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('default_ttl', ['int', 'null']); $resolver->setAllowedTypes('respect_cache_headers', ['bool', 'null']); $resolver->setAllowedTypes('methods', 'array'); - $resolver->setAllowedTypes('cache_key_generator', ['null', 'Http\Client\Common\Plugin\Cache\Generator\CacheKeyGenerator']); + $resolver->setAllowedTypes('cache_key_generator', ['null', CacheKeyGenerator::class]); $resolver->setAllowedTypes('blacklisted_paths', 'array'); $resolver->setAllowedValues('hash_algo', hash_algos()); $resolver->setAllowedValues('methods', function ($value) { From c0c96de5c82e497aefeaec82068e1d2b66f3314d Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Fri, 28 Apr 2023 12:41:32 +0200 Subject: [PATCH 85/97] prepare release --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d6048c..87bc4c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log -## 1.7.6 - 2023-04-23 +## 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 From f16e8c63f3c7c23bc9f901917975b1f1d003a0ce Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sun, 23 Apr 2023 10:17:54 +0200 Subject: [PATCH 86/97] Detach stream before serializing PSR-7 response --- .github/workflows/tests.yml | 2 +- src/CachePlugin.php | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5574164..887bb05 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -74,7 +74,7 @@ jobs: - name: Install dependencies run: | - composer require "friends-of-phpspec/phpspec-code-coverage:^4.3.2" --no-interaction --no-update + composer require "friends-of-phpspec/phpspec-code-coverage" --no-interaction --no-update composer update --prefer-dist --no-interaction --no-progress - name: Execute tests diff --git a/src/CachePlugin.php b/src/CachePlugin.php index e0f2cbc..ab0ddd7 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -195,11 +195,7 @@ protected function doHandleRequest(RequestInterface $request, callable $next, ca if ($this->isCacheable($response) && $this->isCacheableRequest($request)) { $bodyStream = $response->getBody(); $body = $bodyStream->__toString(); - if ($bodyStream->isSeekable()) { - $bodyStream->rewind(); - } else { - $response = $response->withBody($this->streamFactory->createStream($body)); - } + $bodyStream->detach(); $maxAge = $this->getMaxAge($response); $cacheItem @@ -212,6 +208,13 @@ protected function doHandleRequest(RequestInterface $request, callable $next, ca '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); From 23b3c85dab4f9bdb1bface8be66c5ce0d6bb5b6b Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Fri, 28 Apr 2023 12:16:05 +0200 Subject: [PATCH 87/97] adjust phpspec test doubles to new detach logic --- CHANGELOG.md | 4 ++++ spec/CachePluginSpec.php | 31 +++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87bc4c4..f6f8d50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 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 diff --git a/spec/CachePluginSpec.php b/spec/CachePluginSpec.php index 64390cb..e257514 100644 --- a/spec/CachePluginSpec.php +++ b/spec/CachePluginSpec.php @@ -3,6 +3,7 @@ namespace spec\Http\Client\Common\Plugin; use Http\Client\Common\Plugin\Cache\Generator\SimpleGenerator; +use PhpSpec\Wrapper\Collaborator; use Prophecy\Argument; use Http\Message\StreamFactory; use Http\Promise\FulfilledPromise; @@ -18,8 +19,14 @@ class CachePluginSpec extends ObjectBehavior { + /** + * @var StreamFactory&Collaborator + */ + private $streamFactory; + function let(CacheItemPoolInterface $pool, StreamFactory $streamFactory) { + $this->streamFactory = $streamFactory; $this->beConstructedWith($pool, $streamFactory, [ 'default_ttl' => 60, 'cache_lifetime' => 1000 @@ -42,6 +49,7 @@ function it_caches_responses(CacheItemPoolInterface $pool, CacheItemInterface $i $stream->__toString()->willReturn($httpBody); $stream->isSeekable()->willReturn(true); $stream->rewind()->shouldBeCalled(); + $stream->detach()->shouldBeCalled(); $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn($uri); @@ -53,6 +61,9 @@ function it_caches_responses(CacheItemPoolInterface $pool, CacheItemInterface $i $response->getHeader('Cache-Control')->willReturn([])->shouldBeCalled(); $response->getHeader('Expires')->willReturn([])->shouldBeCalled(); $response->getHeader('ETag')->willReturn([])->shouldBeCalled(); + $response->withBody($stream)->shouldBeCalled()->willReturn($response); + + $this->streamFactory->createStream($httpBody)->shouldBeCalled()->willReturn($stream); $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); @@ -128,6 +139,7 @@ function it_stores_post_requests_when_allowed( $stream->__toString()->willReturn($httpBody); $stream->isSeekable()->willReturn(true); $stream->rewind()->shouldBeCalled(); + $stream->detach()->shouldBeCalled(); $request->getMethod()->willReturn('POST'); $request->getUri()->willReturn($uri); @@ -139,6 +151,9 @@ function it_stores_post_requests_when_allowed( $response->getHeader('Cache-Control')->willReturn([])->shouldBeCalled(); $response->getHeader('Expires')->willReturn([])->shouldBeCalled(); $response->getHeader('ETag')->willReturn([])->shouldBeCalled(); + $response->withBody($stream)->shouldBeCalled()->willReturn($response); + + $this->streamFactory->createStream($httpBody)->shouldBeCalled()->willReturn($stream); $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); @@ -186,6 +201,7 @@ function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemI $stream->__toString()->willReturn($httpBody); $stream->isSeekable()->willReturn(true); $stream->rewind()->shouldBeCalled(); + $stream->detach()->shouldBeCalled(); $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn($uri); @@ -198,6 +214,9 @@ function it_calculate_age_from_response(CacheItemPoolInterface $pool, CacheItemI $response->getHeader('Age')->willReturn(['15']); $response->getHeader('Expires')->willReturn([]); $response->getHeader('ETag')->willReturn([]); + $response->withBody($stream)->shouldBeCalled()->willReturn($response); + + $this->streamFactory->createStream($httpBody)->shouldBeCalled()->willReturn($stream); $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); @@ -226,6 +245,7 @@ function it_saves_etag(CacheItemPoolInterface $pool, CacheItemInterface $item, R $stream->__toString()->willReturn($httpBody); $stream->isSeekable()->willReturn(true); $stream->rewind()->shouldBeCalled(); + $stream->detach()->shouldBeCalled(); $request->getBody()->shouldBeCalled()->willReturn($stream); $request->getMethod()->willReturn('GET'); @@ -236,6 +256,9 @@ function it_saves_etag(CacheItemPoolInterface $pool, CacheItemInterface $item, R $response->getHeader('Cache-Control')->willReturn([]); $response->getHeader('Expires')->willReturn([]); $response->getHeader('ETag')->willReturn(['foo_etag']); + $response->withBody($stream)->shouldBeCalled()->willReturn($response); + + $this->streamFactory->createStream($httpBody)->shouldBeCalled()->willReturn($stream); $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); @@ -387,6 +410,7 @@ function it_caches_private_responses_when_allowed( $stream->__toString()->willReturn($httpBody); $stream->isSeekable()->willReturn(true); $stream->rewind()->shouldBeCalled(); + $stream->detach()->shouldBeCalled(); $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn($uri); @@ -398,6 +422,9 @@ function it_caches_private_responses_when_allowed( $response->getHeader('Cache-Control')->willReturn(['private'])->shouldBeCalled(); $response->getHeader('Expires')->willReturn([])->shouldBeCalled(); $response->getHeader('ETag')->willReturn([])->shouldBeCalled(); + $response->withBody($stream)->shouldBeCalled()->willReturn($response); + + $this->streamFactory->createStream($httpBody)->shouldBeCalled()->willReturn($stream); $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); @@ -484,6 +511,7 @@ function it_stores_responses_of_requests_not_in_blacklisted_paths( $stream->__toString()->willReturn($httpBody); $stream->isSeekable()->willReturn(true); $stream->rewind()->shouldBeCalled(); + $stream->detach()->shouldBeCalled(); $request->getMethod()->willReturn('GET'); $request->getUri()->willReturn($uri); @@ -495,6 +523,9 @@ function it_stores_responses_of_requests_not_in_blacklisted_paths( $response->getHeader('Cache-Control')->willReturn([])->shouldBeCalled(); $response->getHeader('Expires')->willReturn([])->shouldBeCalled(); $response->getHeader('ETag')->willReturn([])->shouldBeCalled(); + $response->withBody($stream)->shouldBeCalled()->willReturn($response); + + $this->streamFactory->createStream($httpBody)->shouldBeCalled()->willReturn($stream); $pool->getItem(Argument::any())->shouldBeCalled()->willReturn($item); $item->isHit()->willReturn(false); From 6bf9fbf66193f61d90c2381b75eb1fa0202fd314 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Fri, 28 Apr 2023 12:56:55 +0200 Subject: [PATCH 88/97] document response serialize issue in the code --- src/CachePlugin.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/CachePlugin.php b/src/CachePlugin.php index ab0ddd7..166b24b 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -193,6 +193,10 @@ protected function doHandleRequest(RequestInterface $request, callable $next, ca } 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(); From a4d05cfa3618c556a7091209be4cf40733258e04 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Tue, 14 Nov 2023 17:25:14 +0100 Subject: [PATCH 89/97] prepare for version 2 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6f8d50..63ff0af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Change Log +# Version 1 + ## 1.8.0 - 2023-04-28 - Avoid PHP warning about serializing resources when serializing the response by detaching the stream. From 35625293aedba53ca49bb4bd06ac7d12239a89d9 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 15 Nov 2023 07:30:24 +0000 Subject: [PATCH 90/97] Psr17: prevent usage of deprecated StreamFactory and use StreamFactoryInterface instead (#87) --- CHANGELOG.md | 7 +++++++ composer.json | 5 +++-- spec/CachePluginSpec.php | 22 +++++++++++----------- src/CachePlugin.php | 23 +++++++---------------- 4 files changed, 28 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63ff0af..f7f231f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +# Version 2 + +## 2.0.0 - (unreleased) + +### Changed +- Drop support of deprecated PHP-HTTP `StreamFactory`, only PSR-17 `StreamFactoryInterface` is now supported. + # Version 1 ## 1.8.0 - 2023-04-28 diff --git a/composer.json b/composer.json index d62dd77..ddb4801 100644 --- a/composer.json +++ b/composer.json @@ -14,11 +14,12 @@ "php": "^7.1 || ^8.0", "psr/cache": "^1.0 || ^2.0 || ^3.0", "php-http/client-common": "^1.9 || ^2.0", - "php-http/message-factory": "^1.0", + "psr/http-factory-implementation": "^1.0", "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0 || ^5.0 || ^6.0" }, "require-dev": { - "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0", + "nyholm/psr7": "^1.6.1" }, "autoload": { "psr-4": { diff --git a/spec/CachePluginSpec.php b/spec/CachePluginSpec.php index e257514..fabd8a2 100644 --- a/spec/CachePluginSpec.php +++ b/spec/CachePluginSpec.php @@ -5,13 +5,13 @@ use Http\Client\Common\Plugin\Cache\Generator\SimpleGenerator; use PhpSpec\Wrapper\Collaborator; use Prophecy\Argument; -use Http\Message\StreamFactory; use Http\Promise\FulfilledPromise; use PhpSpec\ObjectBehavior; 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 Http\Client\Common\Plugin\CachePlugin; @@ -20,11 +20,11 @@ class CachePluginSpec extends ObjectBehavior { /** - * @var StreamFactory&Collaborator + * @var StreamFactoryInterface&Collaborator */ private $streamFactory; - function let(CacheItemPoolInterface $pool, StreamFactory $streamFactory) + function let(CacheItemPoolInterface $pool, StreamFactoryInterface $streamFactory) { $this->streamFactory = $streamFactory; $this->beConstructedWith($pool, $streamFactory, [ @@ -126,7 +126,7 @@ function it_stores_post_requests_when_allowed( RequestInterface $request, UriInterface $uri, ResponseInterface $response, - StreamFactory $streamFactory, + StreamFactoryInterface $streamFactory, StreamInterface $stream ) { $this->beConstructedWith($pool, $streamFactory, [ @@ -181,7 +181,7 @@ function it_does_not_allow_invalid_request_methods( CacheItemInterface $item, RequestInterface $request, ResponseInterface $response, - StreamFactory $streamFactory, + StreamFactoryInterface $streamFactory, StreamInterface $stream ) { $this @@ -312,7 +312,7 @@ function it_adds_etag_and_modfied_since_to_request(CacheItemPoolInterface $pool, $this->handleRequest($request, $next, function () {}); } - function it_serves_a_cached_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, StreamInterface $requestBody, ResponseInterface $response, StreamInterface $stream, StreamFactory $streamFactory) + function it_serves_a_cached_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, StreamInterface $requestBody, ResponseInterface $response, StreamInterface $stream, StreamFactoryInterface $streamFactory) { $httpBody = 'body'; @@ -343,7 +343,7 @@ function it_serves_a_cached_response(CacheItemPoolInterface $pool, CacheItemInte $this->handleRequest($request, $next, function () {}); } - function it_serves_and_resaved_expired_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, StreamInterface $requestStream, ResponseInterface $response, StreamInterface $stream, StreamFactory $streamFactory) + function it_serves_and_resaved_expired_response(CacheItemPoolInterface $pool, CacheItemInterface $item, RequestInterface $request, UriInterface $uri, StreamInterface $requestStream, ResponseInterface $response, StreamInterface $stream, StreamFactoryInterface $streamFactory) { $httpBody = 'body'; @@ -398,7 +398,7 @@ function it_caches_private_responses_when_allowed( RequestInterface $request, UriInterface $uri, ResponseInterface $response, - StreamFactory $streamFactory, + StreamFactoryInterface $streamFactory, StreamInterface $stream ) { $this->beConstructedThrough('clientCache', [$pool, $streamFactory, [ @@ -452,7 +452,7 @@ function it_does_not_store_responses_of_requests_to_blacklisted_paths( RequestInterface $request, UriInterface $uri, ResponseInterface $response, - StreamFactory $streamFactory, + StreamFactoryInterface $streamFactory, StreamInterface $stream ) { $this->beConstructedThrough('clientCache', [$pool, $streamFactory, [ @@ -498,7 +498,7 @@ function it_stores_responses_of_requests_not_in_blacklisted_paths( RequestInterface $request, UriInterface $uri, ResponseInterface $response, - StreamFactory $streamFactory, + StreamFactoryInterface $streamFactory, StreamInterface $stream ) { $this->beConstructedThrough('clientCache', [$pool, $streamFactory, [ @@ -550,7 +550,7 @@ function it_stores_responses_of_requests_not_in_blacklisted_paths( function it_can_be_initialized_with_custom_cache_key_generator( CacheItemPoolInterface $pool, CacheItemInterface $item, - StreamFactory $streamFactory, + StreamFactoryInterface $streamFactory, RequestInterface $request, UriInterface $uri, ResponseInterface $response, diff --git a/src/CachePlugin.php b/src/CachePlugin.php index 166b24b..6afb03e 100644 --- a/src/CachePlugin.php +++ b/src/CachePlugin.php @@ -6,8 +6,6 @@ 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\Client\Common\Plugin\Cache\Listener\CacheListener; -use Http\Message\StreamFactory; use Http\Promise\FulfilledPromise; use Http\Promise\Promise; use Psr\Cache\CacheItemInterface; @@ -35,7 +33,7 @@ final class CachePlugin implements Plugin private $pool; /** - * @var StreamFactory|StreamFactoryInterface + * @var StreamFactoryInterface */ private $streamFactory; @@ -52,8 +50,7 @@ final class CachePlugin implements Plugin private $noCacheFlags = ['no-cache', 'private', 'no-store']; /** - * @param StreamFactory|StreamFactoryInterface $streamFactory - * @param mixed[] $config + * @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 @@ -69,12 +66,8 @@ final class CachePlugin implements Plugin * Defaults to an empty array * } */ - public function __construct(CacheItemPoolInterface $pool, $streamFactory, array $config = []) + public function __construct(CacheItemPoolInterface $pool, StreamFactoryInterface $streamFactory, array $config = []) { - if (!($streamFactory instanceof StreamFactory) && !($streamFactory instanceof StreamFactoryInterface)) { - throw new \TypeError(\sprintf('Argument 2 passed to %s::__construct() must be of type %s|%s, %s given.', self::class, StreamFactory::class, StreamFactoryInterface::class, \is_object($streamFactory) ? \get_class($streamFactory) : \gettype($streamFactory))); - } - $this->pool = $pool; $this->streamFactory = $streamFactory; @@ -95,12 +88,11 @@ public function __construct(CacheItemPoolInterface $pool, $streamFactory, array * 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 StreamFactory|StreamFactoryInterface $streamFactory - * @param mixed[] $config For all possible config options see the constructor docs + * @param mixed[] $config For all possible config options see the constructor docs * * @return CachePlugin */ - public static function clientCache(CacheItemPoolInterface $pool, $streamFactory, array $config = []) + public static function clientCache(CacheItemPoolInterface $pool, StreamFactoryInterface $streamFactory, array $config = []) { // Allow caching of private requests if (\array_key_exists('respect_response_cache_directives', $config)) { @@ -118,12 +110,11 @@ public static function clientCache(CacheItemPoolInterface $pool, $streamFactory, * 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 StreamFactory|StreamFactoryInterface $streamFactory - * @param mixed[] $config For all possible config options see the constructor docs + * @param mixed[] $config For all possible config options see the constructor docs * * @return CachePlugin */ - public static function serverCache(CacheItemPoolInterface $pool, $streamFactory, array $config = []) + public static function serverCache(CacheItemPoolInterface $pool, StreamFactoryInterface $streamFactory, array $config = []) { return new self($pool, $streamFactory, $config); } From 8de82356f8e6f468a466d841d96d6b9f2c973bce Mon Sep 17 00:00:00 2001 From: Jeroen van den Enden Date: Tue, 21 Nov 2023 08:09:26 +0000 Subject: [PATCH 91/97] Support Symfony 7 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d62dd77..7754371 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "psr/cache": "^1.0 || ^2.0 || ^3.0", "php-http/client-common": "^1.9 || ^2.0", "php-http/message-factory": "^1.0", - "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0 || ^5.0 || ^6.0" + "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" }, "require-dev": { "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" From b3e6c25d89ee5e4ac82115ed23b21ba87986d614 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Tue, 21 Nov 2023 09:52:56 +0100 Subject: [PATCH 92/97] prepare release --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63ff0af..d6d47a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ # 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. From 062a75f88ce99edc238c35e3e12905e6b199bf56 Mon Sep 17 00:00:00 2001 From: Tac Tacelosky Date: Tue, 13 Feb 2024 05:55:40 -0600 Subject: [PATCH 93/97] remove $ so gitclip works --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cef5dab..f4f9f43 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Via Composer ``` bash -$ composer require php-http/cache-plugin +composer require php-http/cache-plugin ``` @@ -27,7 +27,7 @@ Please see the [official documentation](http://docs.php-http.org/en/latest/plugi ## Testing ``` bash -$ composer test +composer test ``` From 3d4cdf33610b4048454611a609240b29e4bd86a8 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Mon, 19 Feb 2024 17:56:28 +0100 Subject: [PATCH 94/97] use latest checkout action in github actions --- .github/workflows/static.yml | 2 +- .github/workflows/tests.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 6644e71..d4c7e85 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: PHPStan uses: docker://oskarstark/phpstan-ga diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 887bb05..5a43892 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -40,7 +40,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -63,7 +63,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 From 539b2d1ea0dc1c2f141c8155f888197d4ac5635b Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Mon, 19 Feb 2024 18:02:14 +0100 Subject: [PATCH 95/97] prepare release --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0393a8c..c6c03a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,10 @@ # Version 2 -## 2.0.0 - (unreleased) +## 2.0.0 - 2024-02-19 ### Changed + - Drop support of deprecated PHP-HTTP `StreamFactory`, only PSR-17 `StreamFactoryInterface` is now supported. # Version 1 From 004df3ed5d029f9d85de30ce72510d2eda4ca61f Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Wed, 2 Oct 2024 12:24:43 +0100 Subject: [PATCH 96/97] Test on PHP 8.3 and 8.4 (#92) --- .github/workflows/tests.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5a43892..8e2c0c3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2'] + php: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] steps: - name: Checkout code @@ -25,6 +25,10 @@ jobs: tools: composer:v2 coverage: none + - name: Emulate PHP 8.3 + run: composer config platform.php 8.3.999 + if: matrix.php == '8.4' + - name: Install PHP dependencies run: composer update --prefer-dist --no-interaction --no-progress From 5c591e9e04602cec12307e3e1be3abefeb005e29 Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Wed, 2 Oct 2024 13:25:38 +0200 Subject: [PATCH 97/97] prepare release --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6c03a8..b859565 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ # Version 2 +## 2.0.1 - 2024-10-02 + +- Test with PHP 8.3 and 8.4. + ## 2.0.0 - 2024-02-19 ### Changed