From f16e8c63f3c7c23bc9f901917975b1f1d003a0ce Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sun, 23 Apr 2023 10:17:54 +0200 Subject: [PATCH 1/2] 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 2/2] 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);