diff --git a/src/BodyStore.php b/src/BodyStore.php new file mode 100644 index 0000000..0ff38a1 --- /dev/null +++ b/src/BodyStore.php @@ -0,0 +1,46 @@ +body = $body; + $this->toRead = mb_strlen($this->body); + } + + /** + * @param int $length + * @return false|string + */ + public function __invoke(int $length) + { + if ($this->toRead <= 0) { + return false; + } + + $length = min($length, $this->toRead); + + $body = mb_substr( + $this->body, + $this->read, + $length + ); + $this->toRead -= $length; + $this->read += $length; + return $body; + } +} diff --git a/src/CacheEntry.php b/src/CacheEntry.php index dc5a8c2..7e3c8bc 100644 --- a/src/CacheEntry.php +++ b/src/CacheEntry.php @@ -2,6 +2,7 @@ namespace Kevinrob\GuzzleCache; +use GuzzleHttp\Psr7\PumpStream; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; @@ -17,14 +18,6 @@ class CacheEntry */ protected $response; - /** - * This field is only used for serialize. - * Response::body is a stream and can't be serialized. - * - * @var string - */ - protected $responseBody; - /** * @var \DateTime */ @@ -265,12 +258,29 @@ public function getAge() public function __sleep() { - // Stream/Resource can't be serialized... So we copy the content + // Stream/Resource can't be serialized... So we copy the content into an implementation of `Psr\Http\Message\StreamInterface` if ($this->response !== null) { - $this->responseBody = (string) $this->response->getBody(); - $this->response->getBody()->rewind(); + $responseBody = (string)$this->response->getBody(); + $this->response = $this->response->withBody( + new PumpStream( + new BodyStore($responseBody), + [ + 'size' => mb_strlen($responseBody), + ] + ) + ); } + $requestBody = (string)$this->request->getBody(); + $this->request = $this->request->withBody( + new PumpStream( + new BodyStore($requestBody), + [ + 'size' => mb_strlen($requestBody) + ] + ) + ); + return array_keys(get_object_vars($this)); } @@ -280,8 +290,13 @@ public function __wakeup() if ($this->response !== null) { $this->response = $this->response ->withBody( - \GuzzleHttp\Psr7\Utils::streamFor($this->responseBody) + \GuzzleHttp\Psr7\Utils::streamFor((string) $this->response->getBody()) ); } + $this->request = $this->request + ->withBody( + \GuzzleHttp\Psr7\Utils::streamFor((string) $this->request->getBody()) + ); } + } diff --git a/tests/BodyStoreTest.php b/tests/BodyStoreTest.php new file mode 100644 index 0000000..7d42408 --- /dev/null +++ b/tests/BodyStoreTest.php @@ -0,0 +1,46 @@ +assertEquals($str, $bodyStore(PHP_INT_MAX)); + } + + public function testBodyStoreReturnsFalseIsAllHasBeenRead() + { + $str = 'Not so long'; + $bodyStore = new BodyStore($str); + $bodyStore(PHP_INT_MAX); + $this->assertFalse($bodyStore(1)); + } + + public function testBodyStoreCanReadAllContentWhenIteratedEnough() + { + $originalString = <<assertEquals($originalString, $got); + } + + public function testBodyStoreReturnsEmptyStringWhenAsking0() + { + $str = 'Not so long'; + $bodyStore = new BodyStore($str); + $this->assertEquals('', $bodyStore(0)); + } +} diff --git a/tests/CacheEntryTest.php b/tests/CacheEntryTest.php index 294fd5b..21ed04f 100644 --- a/tests/CacheEntryTest.php +++ b/tests/CacheEntryTest.php @@ -2,6 +2,8 @@ namespace Kevinrob\GuzzleCache\Tests; +use GuzzleHttp\Psr7\Request; +use GuzzleHttp\Psr7\Response; use Kevinrob\GuzzleCache\CacheEntry; use PHPUnit\Framework\TestCase; use Psr\Http\Message\RequestInterface; @@ -81,6 +83,34 @@ public function testTtlUsesMaximumPossibleLifetime() $this->assertEquals(70, $cacheEntry->getTTL()); } + public function testCacheEntryShouldBeSerializableWithIgBinaryWithoutWarning() + { + $request = new Request( + 'GET', + 'test.local', + [], + 'Sample body' // Always include a body in the request to be sure there is a stream in it + ); + $response = new Response( + 200, [ + 'Cache-Control' => 'max-age=60', + ], + 'Test content' + ); + $cacheEntry = new CacheEntry($request, $response, $this->makeDateTimeOffset(10)); + + if(extension_loaded('igbinary')) { + /** + * @var CacheEntry $cacheEntryPostDeserialization + */ + $cacheEntryPostDeserialization = igbinary_unserialize(igbinary_serialize($cacheEntry)); + $this->assertEquals((string)$cacheEntry->getOriginalRequest()->getBody(), (string)$cacheEntryPostDeserialization->getOriginalRequest()->getBody()); + $this->assertEquals((string)$cacheEntry->getOriginalResponse()->getBody(), (string)$cacheEntryPostDeserialization->getOriginalResponse()->getBody()); + } else { + $this->addWarning('Extension igbinary not loaded, not asserting serialization.'); + } + } + private function setResponseHeader($name, $value) { $this->responseHeaders[$name] = [$value]; diff --git a/tests/PrivateCacheTest.php b/tests/PrivateCacheTest.php index 5be441f..1708bb0 100644 --- a/tests/PrivateCacheTest.php +++ b/tests/PrivateCacheTest.php @@ -5,12 +5,12 @@ use Cache\Adapter\PHPArray\ArrayCachePool; use Cache\Bridge\SimpleCache\SimpleCacheBridge; use Doctrine\Common\Cache\ArrayCache; -use Doctrine\Common\Cache\CacheProvider; use Doctrine\Common\Cache\ChainCache; use Doctrine\Common\Cache\FilesystemCache; use Doctrine\Common\Cache\PhpFileCache; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; +use Kevinrob\GuzzleCache\Storage\CacheStorageInterface; use Kevinrob\GuzzleCache\Storage\CompressedDoctrineCacheStorage; use Kevinrob\GuzzleCache\Storage\DoctrineCacheStorage; use Kevinrob\GuzzleCache\Storage\FlysystemStorage; @@ -23,22 +23,14 @@ class PrivateCacheTest extends TestCase { - public function testCacheProvider() + /** + * @param CacheStorageInterface $cacheProvider + * @param $TMP_DIR + * @return void + * @dataProvider cacheProvider + */ + public function testCacheProvider(CacheStorageInterface $cacheProvider, $TMP_DIR = null) { - $TMP_DIR = __DIR__.'/tmp/'; - - $cacheProviders = [ - new DoctrineCacheStorage(new ArrayCache()), - new DoctrineCacheStorage(new ChainCache([new ArrayCache()])), - new DoctrineCacheStorage(new FilesystemCache($TMP_DIR)), - new DoctrineCacheStorage(new PhpFileCache($TMP_DIR)), - new FlysystemStorage(new Local($TMP_DIR)), - new Psr6CacheStorage(new ArrayCachePool()), - new Psr16CacheStorage(new SimpleCacheBridge(new ArrayCachePool())), - new CompressedDoctrineCacheStorage(new ArrayCache()), - new VolatileRuntimeStorage(), - ]; - $request = new Request('GET', 'test.local'); $response = new Response( 200, [ @@ -53,39 +45,56 @@ public function testCacheProvider() 'Test new content' ); - /** @var CacheProvider $cacheProvider */ - foreach ($cacheProviders as $cacheProvider) { + if ($TMP_DIR !== null) { $this->rrmdir($TMP_DIR); + } + + $cache = new PrivateCacheStrategy( + $cacheProvider + ); + $cache->cache($request, $response); + $entry = $cache->fetch($request); - $cache = new PrivateCacheStrategy( - $cacheProvider - ); - $cache->cache($request, $response); - $entry = $cache->fetch($request); + $this->assertNotNull($entry, get_class($cacheProvider)); + $this->assertEquals( + (string) $response->getBody(), + (string) $entry->getResponse()->getBody(), + get_class($cacheProvider) + ); - $this->assertNotNull($entry, get_class($cacheProvider)); - $this->assertEquals( - (string) $response->getBody(), - (string) $entry->getResponse()->getBody(), - get_class($cacheProvider) - ); + $cache->update($request, $response2); + $entry = $cache->fetch($request); - $cache->update($request, $response2); - $entry = $cache->fetch($request); + $this->assertNotNull($entry, get_class($cacheProvider)); + $this->assertEquals( + (string) $response2->getBody(), + (string) $entry->getResponse()->getBody(), + get_class($cacheProvider) + ); - $this->assertNotNull($entry, get_class($cacheProvider)); - $this->assertEquals( - (string) $response2->getBody(), - (string) $entry->getResponse()->getBody(), - get_class($cacheProvider) - ); + $cache->delete($request); + $entry = $cache->fetch($request); + $this->assertNull($entry, get_class($cacheProvider)); - $cache->delete($request); - $entry = $cache->fetch($request); - $this->assertNull($entry, get_class($cacheProvider)); + if ($TMP_DIR !== null) { + $this->rrmdir($TMP_DIR); } + } - $this->rrmdir($TMP_DIR); + public function cacheProvider() + { + $TMP_DIR = __DIR__.'/tmp/'; + return [ + 'doctrine.arraycache' => [ new DoctrineCacheStorage(new ArrayCache()) ], + 'doctrine.chaincache' => [ new DoctrineCacheStorage(new ChainCache([new ArrayCache()])) ], + 'doctrine.filesystem' => [ new DoctrineCacheStorage(new FilesystemCache($TMP_DIR)), $TMP_DIR ], + 'doctrine.phpfile' => [ new DoctrineCacheStorage(new PhpFileCache($TMP_DIR)), $TMP_DIR ], + 'flysystem' => [ new FlysystemStorage(new Local($TMP_DIR)), $TMP_DIR ], + 'psr6' => [ new Psr6CacheStorage(new ArrayCachePool()) ], + 'psr16' => [ new Psr16CacheStorage(new SimpleCacheBridge(new ArrayCachePool())) ], + 'compressedDoctrineStorage' => [ new CompressedDoctrineCacheStorage(new ArrayCache()) ], + 'volatileruntimeStorage' => [ new VolatileRuntimeStorage() ] + ]; } /**