diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 76f5d983584d892b5d9e80d28d221843cb2bf2f0..8e1d035848bf841e253a4686bd4aedd4d4e8c13d 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Creative\CachingHttpClientBundle\DependencyInjection; -use Creative\CachingHttpClientBundle\Service\CachingHttpClient; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -22,9 +21,14 @@ class Configuration implements ConfigurationInterface $treeBuilder->getRootNode() ->children() - ->scalarNode('http_client')->defaultValue('@http_client')->end() - ->scalarNode('cache_adapter')->defaultValue('@cache.app')->end() - ->integerNode('cache_time')->defaultValue(CachingHttpClient::DEFAULT_CACHE_RESPONSE_TIME)->end() + ->arrayNode('options') + ->children() + ->integerNode('cache_time')->end() + ->arrayNode('allow_content_type') + ->scalarPrototype()->end() + ->end() + ->end() + ->end() ->end() ; diff --git a/README.md b/README.md index 03d34287abd732f03b0895ebf51351b6d1c7a60d..1a8b3c4b78eb6a00e0347e3f3e6810c7419839e7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,59 @@ -Бандл для кэширования json-ответов GET-запросов -- +# Бандл-декоратор http-клиента для кэширования ответов + +Бандл позволяет кэшировать успешные(200) ответы, +даже если были установлены заголовки, запрещающие кэширование. + +## Установка + +1. Добавить в `composer.json` элемент `repositories` либо дополнить его: + ``` + "repositories": [ + { + "type": "git", + "url": "ssh://git@git.crtweb.ru:22681/creative-packages/caching-http-client-bundle.git" + } + ] + ``` +2. Выполнить команду: + ``` + composer require creative/caching-http-client-bundle + ``` + +## Настройка + +Чтобы использовать кэширующий клиент в качестве клиента по умолчанию, укажите следующую конфигурацию: + +```yaml +# config/services.yaml + +Creative\CachingHttpClientBundle\Service\CachingHttpClient: + decorates: http_client + arguments: + - '@Creative\CachingHttpClientBundle\Service\CachingHttpClient.inner' + - '@cache.app' +``` + +Дполнительные настройки: + +```yaml +# config/services.yaml + +Creative\CachingHttpClientBundle\Service\CachingHttpClient: + decorates: http_client + arguments: + - '@Creative\CachingHttpClientBundle\Service\CachingHttpClient.inner' + - '@cache.app' + - + cache_time: 3600 + allow_content_type: ['application/json', 'text/plain', 'text/html'] +``` + +Пример конфигурации компонента через yaml файл + +```yaml +# config/packages/caching_http_client.yaml +caching_http_client: + options: + cache_time: 120 + allow_content_type: ['application/json'] +``` \ No newline at end of file diff --git a/Resources/config/services.yaml b/Resources/config/services.yaml index 27fb70730723a64fbc905c64166d2e2ac17983dc..0d7ce7ea4428ccbea21a0083f8d5441e7fcde291 100644 --- a/Resources/config/services.yaml +++ b/Resources/config/services.yaml @@ -3,3 +3,4 @@ services: arguments: - '@http_client' - '@cache.app' + - '%caching_http_client.options%' diff --git a/Service/CachingHttpClient.php b/Service/CachingHttpClient.php index fc9f6a511bce1bc8d0408ba741659942e5bb0290..12e1cc7ed5a1396ec922bbe6e91e47c796790f89 100644 --- a/Service/CachingHttpClient.php +++ b/Service/CachingHttpClient.php @@ -7,8 +7,12 @@ namespace Creative\CachingHttpClientBundle\Service; use Creative\CachingHttpClientBundle\Service\Response\CacheResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\ItemInterface; +use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; @@ -20,7 +24,7 @@ use Symfony\Contracts\HttpClient\ResponseStreamInterface; class CachingHttpClient implements HttpClientInterface { public const HEADER_CONTENT_TYPE_APPLICATION_JSON = 'application/json'; - public const DEFAULT_CACHE_RESPONSE_TIME = 3600; + protected const DEFAULT_CACHE_RESPONSE_TIME = 3600; /** * @var HttpClientInterface @@ -33,25 +37,40 @@ class CachingHttpClient implements HttpClientInterface private $cacheAdapter; /** - * @var int + * @var array */ - private $cacheTime; + private $options; /** * CachingHttpClient constructor. * * @param HttpClientInterface $decorate * @param CacheInterface $cacheAdapter - * @param int $cacheTime + * @param array $options */ public function __construct( HttpClientInterface $decorate, CacheInterface $cacheAdapter, - int $cacheTime = self::DEFAULT_CACHE_RESPONSE_TIME + array $options = [] ) { $this->decorate = $decorate; $this->cacheAdapter = $cacheAdapter; - $this->cacheTime = $cacheTime; + + $resolver = new OptionsResolver(); + $this->configureOptions($resolver); + + $this->options = $resolver->resolve($options); + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'cache_time' => self::DEFAULT_CACHE_RESPONSE_TIME, + 'allow_content_type' => ['application/json', 'text/plain', 'text/html'], + ]); + + $resolver->setAllowedTypes('cache_time', 'int'); + $resolver->setAllowedTypes('allow_content_type', 'string[]'); } /** @@ -70,14 +89,12 @@ class CachingHttpClient implements HttpClientInterface $cacheResponse = $this->cacheAdapter->get($cacheKey, function (ItemInterface $item) use ($method, $url, $options): array { $response = $this->decorate->request($method, $url, $options); - $mustCache = $this->checkResponseMustCache($response); - - $item->expiresAfter($mustCache ? $this->cacheTime : 0); + $item->expiresAfter($this->shouldBeCached($response) ? $this->options['cache_time'] : 0); return [ - 'content' => $response->getContent(), - 'headers' => $response->getHeaders(), 'statusCode' => $response->getStatusCode(), + 'headers' => $response->getHeaders(false), + 'content' => $response->getContent(false), 'info' => $response->getInfo(), ]; }); @@ -96,24 +113,29 @@ class CachingHttpClient implements HttpClientInterface * @return bool * * @throws TransportExceptionInterface - * @throws \Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface - * @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface - * @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface + * @throws ClientExceptionInterface + * @throws RedirectionExceptionInterface + * @throws ServerExceptionInterface */ - protected function checkResponseMustCache(ResponseInterface $response): bool + protected function shouldBeCached(ResponseInterface $response): bool { - $mustCache = true; + $shouldBeCached = true; if (Response::HTTP_OK !== $response->getStatusCode() || !$response->getContent(false)) { - $mustCache = false; + $shouldBeCached = false; + } + + $contentType = $response->getHeaders(false)['content-type'][0]; + if (!empty($this->options['allow_content_type']) && !\in_array($contentType, $this->options['allow_content_type'], true)) { + $shouldBeCached = false; } - if (self::HEADER_CONTENT_TYPE_APPLICATION_JSON === $response->getHeaders(false)['content-type'][0]) { - if (!(bool) json_decode($response->getContent(), true)) { - $mustCache = false; + if (self::HEADER_CONTENT_TYPE_APPLICATION_JSON === $contentType) { + if (!(bool) json_decode($response->getContent(false), true)) { + $shouldBeCached = false; } } - return $mustCache; + return $shouldBeCached; } /** diff --git a/Service/Response/CacheResponse.php b/Service/Response/CacheResponse.php index 87018c3a98872df08a977b726af328a906d2dab6..8436e1e7c432f603a08576c7701371f05d64a433 100644 --- a/Service/Response/CacheResponse.php +++ b/Service/Response/CacheResponse.php @@ -5,9 +5,13 @@ declare(strict_types=1); namespace Creative\CachingHttpClientBundle\Service\Response; use Creative\CachingHttpClientBundle\Service\CachingHttpClient; -use Creative\CachingHttpClientBundle\Traits\ResponseTrait; +use Symfony\Component\HttpClient\Exception\ClientException; use Symfony\Component\HttpClient\Exception\JsonException; +use Symfony\Component\HttpClient\Exception\RedirectionException; +use Symfony\Component\HttpClient\Exception\ServerException; use Symfony\Component\HttpClient\Exception\TransportException; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use Symfony\Contracts\HttpClient\ResponseInterface; /** @@ -15,8 +19,6 @@ use Symfony\Contracts\HttpClient\ResponseInterface; */ class CacheResponse implements ResponseInterface { - use ResponseTrait; - /** * @var int */ @@ -98,7 +100,7 @@ class CacheResponse implements ResponseInterface } $contentType = $this->getHeaders()['content-type'][0]; - if (CachingHttpClient::HEADER_CONTENT_TYPE_APPLICATION_JSON === $contentType) { + if (CachingHttpClient::HEADER_CONTENT_TYPE_APPLICATION_JSON !== $contentType) { throw new JsonException(sprintf('Response content-type is "%s" while a JSON-compatible one was expected.', $contentType)); } @@ -120,4 +122,24 @@ class CacheResponse implements ResponseInterface { return !$type ? $this->info : $this->info[$type]; } + + /** + * @param ResponseInterface $response + * + * @throws TransportExceptionInterface + */ + private function checkStatusCode(ResponseInterface $response): void + { + if (Response::HTTP_INTERNAL_SERVER_ERROR <= $response->getStatusCode()) { + throw new ServerException($response); + } + + if (Response::HTTP_BAD_REQUEST <= $response->getStatusCode()) { + throw new ClientException($response); + } + + if (Response::HTTP_MULTIPLE_CHOICES <= $response->getStatusCode()) { + throw new RedirectionException($response); + } + } } diff --git a/Tests/CachingHttpClient/CacheTest.php b/Tests/CachingHttpClient/CacheTest.php new file mode 100644 index 0000000000000000000000000000000000000000..90b88be51a875781e02c70af0304509086d0a3b3 --- /dev/null +++ b/Tests/CachingHttpClient/CacheTest.php @@ -0,0 +1,136 @@ +<?php + +declare(strict_types=1); + +namespace Creative\CachingHttpClientBundle\Tests\CachingHttpClient; + +use Creative\CachingHttpClientBundle\Service\CachingHttpClient; +use Creative\CachingHttpClientBundle\Service\Response\CacheResponse; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\ItemInterface; + +/** + * Class CacheTest. + */ +class CacheTest extends TestCase +{ + public function testCacheCallback(): void + { + $testUrl = 'http://test'; + $mockResponse = json_encode(['test' => 'test']); + + $client = new MockHttpClient([ + new MockResponse($mockResponse), + ]); + + $cacheItem = $this->createMock(ItemInterface::class); + $cache = $this->createMock(CacheInterface::class); + $cache + ->expects($this->once()) + ->method('get') + ->with( + $this->isType('string'), + $this->callback(function (\Closure $callback) use ($mockResponse, $cacheItem) { + $callbackResponse = $callback($cacheItem); + + self::assertIsArray($callbackResponse); + self::assertSame($mockResponse, $callbackResponse['content']); + + return true; + }) + ) + ->willReturn([ + 'content' => $mockResponse, + 'headers' => [], + 'statusCode' => Response::HTTP_OK, + 'info' => null, + ]) + ; + + $cachingHttpClient = new CachingHttpClient($client, $cache); + $cachingHttpClient->request(Request::METHOD_GET, $testUrl); + } + + public function testCheckingCacheByBadStatusCode(): void + { + $client = $this->createMock(CachingHttpClient::class); + $response = new CacheResponse( + Response::HTTP_BAD_REQUEST, + ['content-type' => [CachingHttpClient::HEADER_CONTENT_TYPE_APPLICATION_JSON]], + '{"test": "test"}', + [] + ); + + $reflection = new \ReflectionClass(CachingHttpClient::class); + $method = $reflection->getMethod('shouldBeCached'); + $method->setAccessible(true); + + static::assertFalse($method->invoke($client, $response)); + } + + public function testCheckingCacheByEmptyContent(): void + { + $client = $this->createMock(CachingHttpClient::class); + $response = new CacheResponse( + Response::HTTP_OK, + ['content-type' => [CachingHttpClient::HEADER_CONTENT_TYPE_APPLICATION_JSON]], + '', + [] + ); + + $reflection = new \ReflectionClass(CachingHttpClient::class); + $method = $reflection->getMethod('shouldBeCached'); + $method->setAccessible(true); + + static::assertFalse($method->invoke($client, $response)); + } + + public function testCheckingCacheByAllowHeaders(): void + { + $client = $this->createMock(CachingHttpClient::class); + $response = new CacheResponse( + Response::HTTP_OK, + ['content-type' => ['text/plain']], + '{"test": "test"}', + [] + ); + + $reflection = new \ReflectionClass(CachingHttpClient::class); + + $allowContentTypeProperty = $reflection->getProperty('options'); + $allowContentTypeProperty->setAccessible(true); + $allowContentTypeProperty->setValue($client, ['allow_content_type' => ['application/json']]); + + $method = $reflection->getMethod('shouldBeCached'); + $method->setAccessible(true); + + static::assertFalse($method->invoke($client, $response)); + } + + public function testCheckingCacheByJsonDecoding(): void + { + $client = $this->createMock(CachingHttpClient::class); + $response = new CacheResponse( + Response::HTTP_OK, + ['content-type' => [CachingHttpClient::HEADER_CONTENT_TYPE_APPLICATION_JSON]], + 'invalid json', + [] + ); + + $reflection = new \ReflectionClass(CachingHttpClient::class); + + $allowContentTypeProperty = $reflection->getProperty('options'); + $allowContentTypeProperty->setAccessible(true); + $allowContentTypeProperty->setValue($client, ['allow_content_type' => ['application/json']]); + + $method = $reflection->getMethod('shouldBeCached'); + $method->setAccessible(true); + + static::assertFalse($method->invoke($client, $response)); + } +} diff --git a/Tests/CachingHttpClient/OptionsTest.php b/Tests/CachingHttpClient/OptionsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..80518ca56f7161ab4ef317019919d4dc3204d1f1 --- /dev/null +++ b/Tests/CachingHttpClient/OptionsTest.php @@ -0,0 +1,55 @@ +<?php + +declare(strict_types=1); + +namespace Creative\CachingHttpClientBundle\Tests\CachingHttpClient; + +use Creative\CachingHttpClientBundle\Service\CachingHttpClient; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Contracts\Cache\CacheInterface; + +/** + * Class OptionsTest. + */ +class OptionsTest extends TestCase +{ + public function testInvalidCacheTime(): void + { + $client = new MockHttpClient(); + $cache = $this->createMock(CacheInterface::class); + + $this->expectException(InvalidOptionsException::class); + + new CachingHttpClient($client, $cache, [ + 'cache_time' => '1', + ]); + } + + public function testInvalidAllowedType(): void + { + $client = new MockHttpClient(); + $cache = $this->createMock(CacheInterface::class); + + $this->expectException(InvalidOptionsException::class); + + new CachingHttpClient($client, $cache, [ + 'allow_content_type' => '1', + ]); + } + + public function testInvalidAllowedTypeElement(): void + { + $client = new MockHttpClient(); + $cache = $this->createMock(CacheInterface::class); + + $this->expectException(InvalidOptionsException::class); + + new CachingHttpClient($client, $cache, [ + 'allow_content_type' => [ + 1, [], + ], + ]); + } +} diff --git a/Tests/CachingHttpClient/ResponseTest.php b/Tests/CachingHttpClient/ResponseTest.php new file mode 100644 index 0000000000000000000000000000000000000000..f015bff14b43562fee718c4a303e0ff2820fef2b --- /dev/null +++ b/Tests/CachingHttpClient/ResponseTest.php @@ -0,0 +1,77 @@ +<?php + +declare(strict_types=1); + +namespace Creative\CachingHttpClientBundle\Tests\CachingHttpClient; + +use Creative\CachingHttpClientBundle\Service\CachingHttpClient; +use Creative\CachingHttpClientBundle\Service\Response\CacheResponse; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\HttpClient\ResponseStreamInterface; + +/** + * Class ResponseTest. + */ +class ResponseTest extends TestCase +{ + public function testStreamRequest(): void + { + $client = new MockHttpClient([]); + $cache = $this->createMock(CacheInterface::class); + + $cachingHttpClient = new CachingHttpClient($client, $cache); + + self::assertInstanceOf(ResponseStreamInterface::class, $cachingHttpClient->stream([])); + } + + public function testNotCachingHttpMethod(): void + { + $mockResponse = json_encode(['test' => 'test']); + + $client = new MockHttpClient([ + new MockResponse($mockResponse), + new MockResponse($mockResponse), + new MockResponse($mockResponse), + ]); + $cache = $this->createMock(CacheInterface::class); + + $cachingHttpClient = new CachingHttpClient($client, $cache); + + self::assertSame($mockResponse, $cachingHttpClient->request(Request::METHOD_POST, 'http://test')->getContent()); + self::assertSame($mockResponse, $cachingHttpClient->request(Request::METHOD_PUT, 'http://test')->getContent()); + self::assertSame($mockResponse, $cachingHttpClient->request(Request::METHOD_DELETE, 'http://test')->getContent()); + } + + public function testCachingHttpMethod(): void + { + $mockResponse = json_encode(['test' => 'test']); + $mockCacheResponse = [ + 'content' => $mockResponse, + 'headers' => [], + 'statusCode' => Response::HTTP_OK, + 'info' => null, + ]; + + $client = new MockHttpClient([ + new MockResponse($mockResponse), + new MockResponse($mockResponse), + new MockResponse($mockResponse), + ]); + $cache = $this->createMock(CacheInterface::class); + $cache->method('get') + ->willReturn($mockCacheResponse) + ; + + $cachingHttpClient = new CachingHttpClient($client, $cache); + + self::assertInstanceOf(CacheResponse::class, $cachingHttpClient->request(Request::METHOD_GET, 'http://test')); + self::assertInstanceOf(CacheResponse::class, + $cachingHttpClient->request(Request::METHOD_OPTIONS, 'http://test')); + self::assertInstanceOf(CacheResponse::class, $cachingHttpClient->request(Request::METHOD_HEAD, 'http://test')); + } +} diff --git a/Tests/CachingHttpClientTest.php b/Tests/CachingHttpClientTest.php deleted file mode 100644 index 0ca01a8929a5bc615cf2134d1f36207bd1c2861a..0000000000000000000000000000000000000000 --- a/Tests/CachingHttpClientTest.php +++ /dev/null @@ -1,227 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Creative\CachingHttpClientBundle\Tests; - -use Creative\CachingHttpClientBundle\Service\CachingHttpClient; -use Creative\CachingHttpClientBundle\Service\Response\CacheResponse; -use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Contracts\Cache\CacheInterface; -use Symfony\Contracts\Cache\ItemInterface; -use Symfony\Contracts\HttpClient\HttpClientInterface; -use Symfony\Contracts\HttpClient\ResponseInterface; -use Symfony\Contracts\HttpClient\ResponseStreamInterface; - -/** - * Class CachingHttpClientTest. - */ -class CachingHttpClientTest extends TestCase -{ - public function testStreamRequest(): void - { - $client = $this->createMock(HttpClientInterface::class); - $client->method('stream') - ->willReturn($this->createMock(ResponseStreamInterface::class)) - ; - - $cache = $this->createMock(CacheInterface::class); - - $cachingHttpClient = new CachingHttpClient($client, $cache); - - self::assertInstanceOf(ResponseStreamInterface::class, $cachingHttpClient->stream([])); - } - - public function testNotCachingHttpMethod(): void - { - $response = json_encode(['test' => 'test']); - - $client = $this->createMock(HttpClientInterface::class); - - $mockResponse = $this->createMock(ResponseInterface::class); - $mockResponse->method('getContent') - ->willReturn($response) - ; - - $client->method('request') - ->willReturn($mockResponse) - ; - - $cache = $this->createMock(CacheInterface::class); - - $cachingHttpClient = new CachingHttpClient($client, $cache); - - self::assertSame($response, $cachingHttpClient->request(Request::METHOD_POST, 'http://test')->getContent()); - self::assertSame($response, $cachingHttpClient->request(Request::METHOD_PUT, 'http://test')->getContent()); - self::assertSame($response, $cachingHttpClient->request(Request::METHOD_DELETE, 'http://test')->getContent()); - } - - public function testCachingHttpMethod(): void - { - $mockResponse = json_encode(['test' => 'test']); - $mockCacheResponse = [ - 'content' => $mockResponse, - 'headers' => [], - 'statusCode' => Response::HTTP_OK, - 'info' => null, - ]; - - $client = $this->createMock(HttpClientInterface::class); - - $cache = $this->createMock(CacheInterface::class); - $cache->method('get') - ->willReturn($mockCacheResponse) - ; - - $cachingHttpClient = new CachingHttpClient($client, $cache); - - self::assertInstanceOf(CacheResponse::class, $cachingHttpClient->request(Request::METHOD_GET, 'http://test')); - self::assertInstanceOf(CacheResponse::class, - $cachingHttpClient->request(Request::METHOD_OPTIONS, 'http://test')); - self::assertInstanceOf(CacheResponse::class, $cachingHttpClient->request(Request::METHOD_HEAD, 'http://test')); - } - - public function testCacheCallback(): void - { - $testUrl = 'http://test'; - - $mockContent = json_encode(['test' => 'test']); - $mockResponse = new CacheResponse(Response::HTTP_OK, [], $mockContent, null); - - $client = $this->createMock(HttpClientInterface::class); - $client->method('request') - ->willReturn($mockResponse) - ; - - $cacheItem = $this->createMock(ItemInterface::class); - - $cache = $this->createMock(CacheInterface::class); - $cache->expects($this->once()) - ->method('get') - ->with( - filter_var($testUrl, FILTER_SANITIZE_ENCODED), - $this->callback(function (\Closure $callback) use ($cacheItem) { - $callbackResponse = $callback($cacheItem); - - return is_array($callbackResponse); - }) - ) - ->willReturn([ - 'content' => $mockContent, - 'headers' => [], - 'statusCode' => Response::HTTP_OK, - 'info' => null, - ]) - ; - - $cachingHttpClient = new CachingHttpClient($client, $cache); - - self::assertInstanceOf(CacheResponse::class, $cachingHttpClient->request(Request::METHOD_GET, $testUrl)); - } - - public function testCacheCallbackWithBadStatus(): void - { - $testUrl = 'http://test'; - - $mockContent = json_encode(['test' => 'test']); - - $mockResponse = $this->createMock(ResponseInterface::class); - $mockResponse->method('getStatusCode') - ->willReturn(Response::HTTP_BAD_REQUEST) - ; - $mockResponse->method('getContent') - ->willReturn($mockContent) - ; - $mockResponse->method('getHeaders') - ->willReturn([]) - ; - $mockResponse->method('getInfo') - ->willReturn([]) - ; - - $client = $this->createMock(HttpClientInterface::class); - $client->method('request') - ->willReturn($mockResponse) - ; - - $cacheItem = $this->createMock(ItemInterface::class); - - $cache = $this->createMock(CacheInterface::class); - $cache->expects($this->once()) - ->method('get') - ->with( - filter_var($testUrl, FILTER_SANITIZE_ENCODED), - $this->callback(function (\Closure $callback) use ($cacheItem) { - $callbackResponse = $callback($cacheItem); - - return is_array($callbackResponse); - }) - ) - ->willReturn([ - 'content' => $mockContent, - 'headers' => [], - 'statusCode' => Response::HTTP_OK, - 'info' => null, - ]) - ; - - $cachingHttpClient = new CachingHttpClient($client, $cache); - - self::assertInstanceOf(CacheResponse::class, $cachingHttpClient->request(Request::METHOD_GET, $testUrl)); - } - - public function testCacheCallbackWithBadJsonContent(): void - { - $testUrl = 'http://test'; - - $mockContent = 'test'; - - $mockResponse = $this->createMock(ResponseInterface::class); - $mockResponse->method('getStatusCode') - ->willReturn(Response::HTTP_OK) - ; - $mockResponse->method('getContent') - ->willReturn($mockContent) - ; - $mockResponse->method('getHeaders') - ->willReturn([ - 'content-type' => [CachingHttpClient::HEADER_CONTENT_TYPE_APPLICATION_JSON], - ]) - ; - $mockResponse->method('getInfo') - ->willReturn([]) - ; - - $client = $this->createMock(HttpClientInterface::class); - $client->method('request') - ->willReturn($mockResponse) - ; - - $cacheItem = $this->createMock(ItemInterface::class); - - $cache = $this->createMock(CacheInterface::class); - $cache->expects($this->once()) - ->method('get') - ->with( - filter_var($testUrl, FILTER_SANITIZE_ENCODED), - $this->callback(function (\Closure $callback) use ($cacheItem) { - $callbackResponse = $callback($cacheItem); - - return is_array($callbackResponse); - }) - ) - ->willReturn([ - 'content' => $mockContent, - 'headers' => [], - 'statusCode' => Response::HTTP_OK, - 'info' => null, - ]) - ; - - $cachingHttpClient = new CachingHttpClient($client, $cache); - - self::assertInstanceOf(CacheResponse::class, $cachingHttpClient->request(Request::METHOD_GET, $testUrl)); - } -} diff --git a/Tests/Response/CacheResponseTest.php b/Tests/Response/CacheResponseTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1367df8d7c283ce7f7a090bc51df8f794d69b58f --- /dev/null +++ b/Tests/Response/CacheResponseTest.php @@ -0,0 +1,110 @@ +<?php + +declare(strict_types=1); + +namespace Creative\CachingHttpClientBundle\Tests\Response; + +use Creative\CachingHttpClientBundle\Service\CachingHttpClient; +use Creative\CachingHttpClientBundle\Service\Response\CacheResponse; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpClient\Exception\ClientException; +use Symfony\Component\HttpClient\Exception\JsonException; +use Symfony\Component\HttpClient\Exception\RedirectionException; +use Symfony\Component\HttpClient\Exception\ServerException; +use Symfony\Component\HttpClient\Exception\TransportException; +use Symfony\Component\HttpClient\Response\MockResponse; +use Symfony\Component\HttpFoundation\Response; + +/** + * Class CacheResponseTest. + */ +class CacheResponseTest extends TestCase +{ + public function testStatusCode(): void + { + $response = new CacheResponse(Response::HTTP_OK, [], '', []); + + self::assertSame(Response::HTTP_OK, $response->getStatusCode()); + } + + public function testHeaders(): void + { + $testHeaders = ['test' => 'test']; + $response = new CacheResponse(Response::HTTP_OK, $testHeaders, '', []); + + self::assertSame($testHeaders, $response->getHeaders(false)); + } + + public function testHeadersWithRedirectionCode(): void + { + $mockResponse = new MockResponse(); + + $response = new CacheResponse(Response::HTTP_MULTIPLE_CHOICES, [], '', $mockResponse->getInfo()); + + $this->expectException(RedirectionException::class); + $response->getHeaders(true); + } + + public function testHeadersWithServerErrorCode(): void + { + $mockResponse = new MockResponse(); + + $response = new CacheResponse(Response::HTTP_INTERNAL_SERVER_ERROR, [], '', $mockResponse->getInfo()); + + $this->expectException(ServerException::class); + $response->getHeaders(true); + } + + public function testHeadersWithClientErrorCode(): void + { + $mockResponse = new MockResponse(); + + $response = new CacheResponse(Response::HTTP_BAD_REQUEST, [], '', $mockResponse->getInfo()); + + $this->expectException(ClientException::class); + $response->getHeaders(true); + } + + public function testContent(): void + { + $testContent = json_encode(['test' => 'test']); + $response = new CacheResponse(Response::HTTP_OK, [], $testContent, []); + + self::assertSame($testContent, $response->getContent(false)); + } + + public function testJsonDecoding(): void + { + $mockResponse = new MockResponse(); + + $testContent = ['test' => 'test']; + $response = new CacheResponse( + Response::HTTP_OK, + ['content-type' => [CachingHttpClient::HEADER_CONTENT_TYPE_APPLICATION_JSON]], + json_encode($testContent), + $mockResponse->getInfo() + ); + + self::assertSame($testContent, $response->toArray()); + } + + public function testJsonDecodingWithEmptyContent(): void + { + $mockResponse = new MockResponse(); + + $response = new CacheResponse(Response::HTTP_OK, [], '', $mockResponse->getInfo()); + + $this->expectException(TransportException::class); + $response->toArray(); + } + + public function testJsonDecodingWithNotJsonContentType(): void + { + $mockResponse = new MockResponse(); + + $response = new CacheResponse(Response::HTTP_OK, [], 'json', $mockResponse->getInfo()); + + $this->expectException(JsonException::class); + $response->toArray(); + } +} diff --git a/Traits/ResponseTrait.php b/Traits/ResponseTrait.php deleted file mode 100644 index 249f471418fdce2d0d8975bdccb9e09bd79ebaa2..0000000000000000000000000000000000000000 --- a/Traits/ResponseTrait.php +++ /dev/null @@ -1,38 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Creative\CachingHttpClientBundle\Traits; - -use Symfony\Component\HttpClient\Exception\ClientException; -use Symfony\Component\HttpClient\Exception\RedirectionException; -use Symfony\Component\HttpClient\Exception\ServerException; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; -use Symfony\Contracts\HttpClient\ResponseInterface; - -/** - * Trait ResponseTrait. - */ -trait ResponseTrait -{ - /** - * @param ResponseInterface $response - * - * @throws TransportExceptionInterface - */ - private function checkStatusCode(ResponseInterface $response): void - { - if (Response::HTTP_INTERNAL_SERVER_ERROR <= $response->getStatusCode()) { - throw new ServerException($response); - } - - if (Response::HTTP_BAD_REQUEST <= $response->getStatusCode()) { - throw new ClientException($response); - } - - if (Response::HTTP_MULTIPLE_CHOICES <= $response->getStatusCode()) { - throw new RedirectionException($response); - } - } -} diff --git a/composer.json b/composer.json index 7e63e7ac8d55bc0de238996544ba2cbfc0b34ec7..9a7249e6d5d86112cf3fc58fedc463c59b26f93c 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,8 @@ "symfony/framework-bundle": "^5.0", "symfony/http-client": "^5.0", "ext-json": "*", - "symfony/config": "^5.0" + "symfony/config": "^5.0", + "symfony/options-resolver": "^5.0" }, "autoload": { "psr-4": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 2cc8ab9119def3cc1d7345347883a74349c46b4a..d23c692251c3bcb3703fa0a79027eaa2969dfaa5 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -10,7 +10,6 @@ <filter> <whitelist> <directory>./Service</directory> - <directory>./Traits</directory> <exclude> <directory>./Resources</directory> <directory>./Tests</directory>