diff --git a/Async/ResolveCacheProcessor.php b/Async/ResolveCacheProcessor.php index ef5a673b2..32158ca6c 100644 --- a/Async/ResolveCacheProcessor.php +++ b/Async/ResolveCacheProcessor.php @@ -57,10 +57,9 @@ public function process(Message $psrMessage, Context $psrContext) $filters = $message->getFilters() ?: array_keys($this->filterManager->getFilterConfiguration()->all()); $path = $message->getPath(); $results = []; + foreach ($filters as $filter) { - if ($message->isForce()) { - $this->filterService->bustCache($path, $filter); - } + $this->filterService->warmUpCache($path, $filter, null, $message->isForce()); $results[$filter] = $this->filterService->getUrlOfFilteredImage($path, $filter); } diff --git a/Command/RemoveCacheCommand.php b/Command/RemoveCacheCommand.php index 68786a676..17725fdd3 100644 --- a/Command/RemoveCacheCommand.php +++ b/Command/RemoveCacheCommand.php @@ -13,6 +13,7 @@ use Liip\ImagineBundle\Imagine\Cache\CacheManager; use Liip\ImagineBundle\Imagine\Filter\FilterManager; +use Liip\ImagineBundle\Service\FilterService; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -22,14 +23,21 @@ class RemoveCacheCommand extends Command { use CacheCommandTrait; + protected static $defaultName = 'liip:imagine:cache:remove'; - public function __construct(CacheManager $cacheManager, FilterManager $filterManager) + /** + * @var FilterService + */ + private $filterService; + + public function __construct(CacheManager $cacheManager, FilterManager $filterManager, FilterService $filterService) { parent::__construct(); $this->cacheManager = $cacheManager; $this->filterManager = $filterManager; + $this->filterService = $filterService; } protected function configure(): void @@ -79,9 +87,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (empty($images)) { $this->cacheManager->remove(null, $filters); } else { - foreach ($images as $i) { - foreach ($filters as $f) { - $this->runCacheImageRemove($i, $f); + foreach ($images as $image) { + foreach ($filters as $filter) { + $this->runCacheImageRemove($image, $filter); } } } @@ -99,8 +107,7 @@ private function runCacheImageRemove(string $image, string $filter): void $this->io->group($image, $filter, 'blue'); - if ($this->cacheManager->isStored($image, $filter)) { - $this->cacheManager->remove($image, $filter); + if ($this->filterService->bustCache($image, $filter)) { $this->io->status('removed', 'green'); } else { $this->io->status('skipped', 'yellow'); diff --git a/Command/ResolveCacheCommand.php b/Command/ResolveCacheCommand.php index 3c2634f7c..d16b3c52e 100644 --- a/Command/ResolveCacheCommand.php +++ b/Command/ResolveCacheCommand.php @@ -12,8 +12,8 @@ namespace Liip\ImagineBundle\Command; use Liip\ImagineBundle\Imagine\Cache\CacheManager; -use Liip\ImagineBundle\Imagine\Data\DataManager; use Liip\ImagineBundle\Imagine\Filter\FilterManager; +use Liip\ImagineBundle\Service\FilterService; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -23,20 +23,21 @@ class ResolveCacheCommand extends Command { use CacheCommandTrait; + protected static $defaultName = 'liip:imagine:cache:resolve'; /** - * @var DataManager + * @var FilterService */ - private $dataManager; + private $filterService; - public function __construct(DataManager $dataManager, CacheManager $cacheManager, FilterManager $filterManager) + public function __construct(CacheManager $cacheManager, FilterManager $filterManager, FilterService $filterService) { parent::__construct(); - $this->dataManager = $dataManager; $this->cacheManager = $cacheManager; $this->filterManager = $filterManager; + $this->filterService = $filterService; } protected function configure(): void @@ -90,9 +91,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $forced = $input->getOption('force'); [$images, $filters] = $this->resolveInputFiltersAndPaths($input); - foreach ($images as $i) { - foreach ($filters as $f) { - $this->runCacheImageResolve($i, $f, $forced); + foreach ($images as $image) { + foreach ($filters as $filter) { + $this->runCacheImageResolve($image, $filter, $forced); } } @@ -110,8 +111,7 @@ private function runCacheImageResolve(string $image, string $filter, bool $force $this->io->group($image, $filter, 'blue'); try { - if ($forced || !$this->cacheManager->isStored($image, $filter)) { - $this->cacheManager->store($this->filterManager->applyFilter($this->dataManager->find($filter, $image), $filter), $image, $filter); + if ($this->filterService->warmUpCache($image, $filter, null, $forced)) { $this->io->status('resolved', 'green'); } else { $this->io->status('cached', 'white'); diff --git a/Message/Handler/WarmupCacheHandler.php b/Message/Handler/WarmupCacheHandler.php index f64d8142d..fdbc74b7b 100644 --- a/Message/Handler/WarmupCacheHandler.php +++ b/Message/Handler/WarmupCacheHandler.php @@ -41,9 +41,7 @@ public function __invoke(WarmupCache $message): void $path = $message->getPath(); foreach ($filters as $filter) { - if ($message->isForce()) { - $this->filterService->bustCache($path, $filter); - } + $this->filterService->warmUpCache($path, $filter, null, $message->isForce()); $this->filterService->getUrlOfFilteredImage($path, $filter); } diff --git a/Resources/config/commands.xml b/Resources/config/commands.xml index 9001349eb..4b8fac819 100644 --- a/Resources/config/commands.xml +++ b/Resources/config/commands.xml @@ -8,13 +8,14 @@ + - + diff --git a/Service/FilterService.php b/Service/FilterService.php index 86149e293..ca8df8ae8 100644 --- a/Service/FilterService.php +++ b/Service/FilterService.php @@ -47,7 +47,7 @@ class FilterService private $webpGenerate; /** - * @var array + * @var mixed[] */ private $webpOptions; @@ -70,21 +70,44 @@ public function __construct( /** * @param string $path * @param string $filter + * + * @return bool Returns true if we removed at least one cached image */ public function bustCache($path, $filter) { - $basePathContainer = new FilterPathContainer($path); - $filterPathContainers = [$basePathContainer]; - - if ($this->webpGenerate) { - $filterPathContainers[] = $basePathContainer->createWebp($this->webpOptions); - } + $busted = false; - foreach ($filterPathContainers as $filterPathContainer) { + foreach ($this->buildFilterPathContainers($path) as $filterPathContainer) { if ($this->cacheManager->isStored($filterPathContainer->getTarget(), $filter)) { $this->cacheManager->remove($filterPathContainer->getTarget(), $filter); + + $busted = true; } } + + return $busted; + } + + /** + * @param bool $forced Force warm up cache + * + * @return bool Returns true if the cache is warmed up + */ + public function warmUpCache( + string $path, + string $filter, + ?string $resolver = null, + bool $forced = false + ): bool { + $warmedUp = false; + + foreach ($this->buildFilterPathContainers($path) as $filterPathContainer) { + if ($this->warmUpCacheFilterPathContainer($filterPathContainer, $filter, $resolver, $forced)) { + $warmedUp = true; + } + } + + return $warmedUp; } /** @@ -96,9 +119,11 @@ public function bustCache($path, $filter) */ public function getUrlOfFilteredImage($path, $filter, $resolver = null, bool $webpSupported = false) { - $basePathContainer = new FilterPathContainer($path); + foreach ($this->buildFilterPathContainers($path) as $filterPathContainer) { + $this->warmUpCacheFilterPathContainer($filterPathContainer, $filter, $resolver); + } - return $this->getUrlOfFilteredImageByContainer($basePathContainer, $filter, $resolver, $webpSupported); + return $this->resolveFilterPathContainer(new FilterPathContainer($path), $filter, $resolver, $webpSupported); } /** @@ -116,42 +141,77 @@ public function getUrlOfFilteredImageWithRuntimeFilters( bool $webpSupported = false ) { $runtimePath = $this->cacheManager->getRuntimePath($path, $runtimeFilters); - $basePathContainer = new FilterPathContainer($path, $runtimePath, [ + $runtimeOptions = [ 'filters' => $runtimeFilters, - ]); + ]; + + foreach ($this->buildFilterPathContainers($path, $runtimePath, $runtimeOptions) as $filterPathContainer) { + $this->warmUpCacheFilterPathContainer($filterPathContainer, $filter, $resolver); + } + + return $this->resolveFilterPathContainer( + new FilterPathContainer($path, $runtimePath, $runtimeOptions), + $filter, + $resolver, + $webpSupported + ); + } + + /** + * @param mixed[] $options + * + * @return FilterPathContainer[] + */ + private function buildFilterPathContainers(string $source, string $target = '', array $options = []): array + { + $basePathContainer = new FilterPathContainer($source, $target, $options); + $filterPathContainers = [$basePathContainer]; + + if ($this->webpGenerate) { + $filterPathContainers[] = $basePathContainer->createWebp($this->webpOptions); + } - return $this->getUrlOfFilteredImageByContainer($basePathContainer, $filter, $resolver, $webpSupported); + return $filterPathContainers; } - private function getUrlOfFilteredImageByContainer( - FilterPathContainer $basePathContainer, + private function resolveFilterPathContainer( + FilterPathContainer $filterPathContainer, string $filter, ?string $resolver = null, bool $webpSupported = false ): string { - $filterPathContainers = [$basePathContainer]; + $path = $filterPathContainer->getTarget(); - if ($this->webpGenerate) { - $webpPathContainer = $basePathContainer->createWebp($this->webpOptions); - $filterPathContainers[] = $webpPathContainer; + if ($webpSupported) { + $path = $filterPathContainer->createWebp($this->webpOptions)->getTarget(); } - foreach ($filterPathContainers as $filterPathContainer) { - if (!$this->cacheManager->isStored($filterPathContainer->getTarget(), $filter, $resolver)) { - $this->cacheManager->store( - $this->createFilteredBinary($filterPathContainer, $filter), - $filterPathContainer->getTarget(), - $filter, - $resolver - ); - } - } + return $this->cacheManager->resolve($path, $filter, $resolver); + } + + /** + * @param bool $forced Force warm up cache + * + * @return bool Returns true if the cache is warmed up + */ + private function warmUpCacheFilterPathContainer( + FilterPathContainer $filterPathContainer, + string $filter, + ?string $resolver = null, + bool $forced = false + ): bool { + if ($forced || !$this->cacheManager->isStored($filterPathContainer->getTarget(), $filter, $resolver)) { + $this->cacheManager->store( + $this->createFilteredBinary($filterPathContainer, $filter), + $filterPathContainer->getTarget(), + $filter, + $resolver + ); - if ($webpSupported && isset($webpPathContainer)) { - return $this->cacheManager->resolve($webpPathContainer->getTarget(), $filter, $resolver); + return true; } - return $this->cacheManager->resolve($basePathContainer->getTarget(), $filter, $resolver); + return false; } /** diff --git a/Tests/Async/ResolveCacheProcessorTest.php b/Tests/Async/ResolveCacheProcessorTest.php index a880ba514..13365c565 100644 --- a/Tests/Async/ResolveCacheProcessorTest.php +++ b/Tests/Async/ResolveCacheProcessorTest.php @@ -294,6 +294,7 @@ public function testShouldCreateOneImagePerRequestedFilter(): void public function testShouldBurstCacheWhenResolvingForced(): void { + $force = true; $filterName = 'fooFilter'; $imagePath = 'theImagePath'; @@ -308,8 +309,8 @@ public function testShouldBurstCacheWhenResolvingForced(): void $filterServiceMock = $this->createFilterServiceMock(); $filterServiceMock ->expects($this->once()) - ->method('bustCache') - ->with($imagePath, $filterName); + ->method('warmUpCache') + ->with($imagePath, $filterName, null, $force); $processor = new ResolveCacheProcessor( $filterManagerMock, @@ -318,7 +319,7 @@ public function testShouldBurstCacheWhenResolvingForced(): void ); $message = new NullMessage(); - $message->setBody(json_encode(['path' => $imagePath, 'force' => true])); + $message->setBody(json_encode(['path' => $imagePath, 'force' => $force])); $result = $processor->process($message, new NullContext()); diff --git a/Tests/Service/FilterPathContainerTest.php b/Tests/Service/FilterPathContainerTest.php new file mode 100644 index 000000000..5ab6e7ef2 --- /dev/null +++ b/Tests/Service/FilterPathContainerTest.php @@ -0,0 +1,114 @@ +assertSame($source, $container->getSource()); + $this->assertSame($source, $container->getTarget()); + $this->assertSame($options, $container->getOptions()); + } + + public function testCustomTarget(): void + { + $source = 'images/cats.jpeg'; + $target = 'images/cats.jpeg.webp'; + $options = [ + 'format' => 'webp', + ]; + + $container = new FilterPathContainer($source, $target, $options); + + $this->assertSame($source, $container->getSource()); + $this->assertSame($target, $container->getTarget()); + $this->assertSame($options, $container->getOptions()); + } + + public function provideWebpOptions(): \Traversable + { + yield 'empty options' => [ + [], + [], + [ + 'format' => 'webp', + ], + ]; + + yield 'custom webp options' => [ + [], + [ + 'quality' => 100, + ], + [ + 'format' => 'webp', + 'quality' => 100, + ], + ]; + + yield 'overwrite base options' => [ + [ + 'format' => 'jpeg', + 'quality' => 80, + 'jpeg_quality' => 80, + 'post_processors' => [ + 'jpegoptim' => [ + 'strip_all' => true, + 'max' => 80, + 'progressive' => true, + ], + ], + ], + [ + 'quality' => 100, + 'post_processors' => [ + 'my_custom_webp_post_processor' => [], + ], + ], + [ + 'format' => 'webp', + 'quality' => 100, + 'post_processors' => [ + 'my_custom_webp_post_processor' => [], + ], + 'jpeg_quality' => 80, + ], + ]; + } + + /** + * @dataProvider provideWebpOptions + */ + public function testCreateWebp(array $baseOptions, array $webpOptions, array $expectedOptions): void + { + $source = 'images/cats.jpeg'; + $target = 'images/cats.jpeg.webp'; + + $container = (new FilterPathContainer($source, '', $baseOptions))->createWebp($webpOptions); + + $this->assertSame($source, $container->getSource()); + $this->assertSame($target, $container->getTarget()); + $this->assertSame($expectedOptions, $container->getOptions()); + } +} diff --git a/Tests/Service/FilterServiceTest.php b/Tests/Service/FilterServiceTest.php new file mode 100644 index 000000000..a2983c45f --- /dev/null +++ b/Tests/Service/FilterServiceTest.php @@ -0,0 +1,930 @@ + [ + 'size' => [50, 50], + ], + ]; + private const WEBP_OPTIONS = [ + 'quality' => 100, + 'post_processors' => [ + 'my_custom_webp_post_processor' => [], + ], + ]; + + /** + * @var MockObject|DataManager + */ + private $dataManager; + + /** + * @var MockObject|FilterManager + */ + private $filterManager; + + /** + * @var MockObject|CacheManager + */ + private $cacheManager; + + /** + * @var MockObject|LoggerInterface + */ + private $logger; + + protected function setUp(): void + { + parent::setUp(); + + $this->dataManager = $this + ->getMockBuilder(DataManager::class) + ->disableOriginalConstructor() + ->getMock(); + $this->filterManager = $this + ->getMockBuilder(FilterManager::class) + ->disableOriginalConstructor() + ->getMock(); + $this->cacheManager = $this + ->getMockBuilder(CacheManager::class) + ->disableOriginalConstructor() + ->getMock(); + $this->logger = $this + ->getMockBuilder(LoggerInterface::class) + ->getMock(); + } + + public function provideWebpGeneration(): \Traversable + { + yield 'WebP generation enabled' => [true]; + + yield 'WebP generation disabled' => [false]; + } + + /** + * @dataProvider provideWebpGeneration + */ + public function testBustCache(bool $webpGenerate): void + { + $service = $this->createFilterService($webpGenerate); + + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('isStored') + ->withConsecutive( + [self::SOURCE_IMAGE], + [self::WEBP_IMAGE] + ) + ->willReturn(true); + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('remove') + ->withConsecutive( + [self::SOURCE_IMAGE, self::FILTER], + [self::WEBP_IMAGE, self::FILTER] + ); + + $this->assertTrue($service->bustCache(self::SOURCE_IMAGE, self::FILTER)); + } + + /** + * @dataProvider provideWebpGeneration + */ + public function testNothingBustCache(bool $webpGenerate): void + { + $service = $this->createFilterService($webpGenerate); + + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('isStored') + ->withConsecutive( + [self::SOURCE_IMAGE], + [self::WEBP_IMAGE] + ) + ->willReturn(false); + $this->cacheManager + ->expects($this->never()) + ->method('remove'); + + $this->assertFalse($service->bustCache(self::SOURCE_IMAGE, self::FILTER)); + } + + /** + * @dataProvider provideWebpGeneration + */ + public function testWarmsUpCache(bool $webpGenerate): void + { + $resolver = null; + + $service = $this->createFilterService($webpGenerate); + + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('isStored') + ->withConsecutive( + [self::SOURCE_IMAGE, self::FILTER, $resolver], + [self::WEBP_IMAGE, self::FILTER, $resolver] + ) + ->willReturn(true); + $this->cacheManager + ->expects($this->never()) + ->method('store'); + + $this->dataManager + ->expects($this->never()) + ->method('find'); + + $this->filterManager + ->expects($this->never()) + ->method('applyFilter'); + + $this->assertFalse($service->warmUpCache(self::SOURCE_IMAGE, self::FILTER, $resolver)); + } + + /** + * @dataProvider provideWebpGeneration + */ + public function testWarmsUpCacheForced(bool $webpGenerate): void + { + $resolver = null; + $binary = $this + ->getMockBuilder(BinaryInterface::class) + ->getMock(); + $filteredBinary = $this + ->getMockBuilder(BinaryInterface::class) + ->getMock(); + + $service = $this->createFilterService($webpGenerate); + + $this->cacheManager + ->expects($this->never()) + ->method('isStored'); + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('store') + ->withConsecutive( + [$filteredBinary, self::SOURCE_IMAGE, self::FILTER, $resolver], + [$filteredBinary, self::WEBP_IMAGE, self::FILTER, $resolver] + ); + + $this->dataManager + ->expects($this->atLeastOnce()) + ->method('find') + ->with(self::FILTER, self::SOURCE_IMAGE) + ->willReturn($binary); + + $this->filterManager + ->expects($this->atLeastOnce()) + ->method('applyFilter') + ->withConsecutive( + [$binary, self::FILTER, []], + [$binary, self::FILTER, [ + 'format' => 'webp', + ] + self::WEBP_OPTIONS] + ) + ->willReturn($binary); + + $this->assertTrue($service->warmUpCache(self::SOURCE_IMAGE, self::FILTER, $resolver, true)); + } + + /** + * @dataProvider provideWebpGeneration + */ + public function testWarmsUpCacheNotStored(bool $webpGenerate): void + { + $resolver = null; + $binary = $this + ->getMockBuilder(BinaryInterface::class) + ->getMock(); + $filteredBinary = $this + ->getMockBuilder(BinaryInterface::class) + ->getMock(); + + $service = $this->createFilterService($webpGenerate); + + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('isStored') + ->withConsecutive( + [self::SOURCE_IMAGE, self::FILTER, $resolver], + [self::WEBP_IMAGE, self::FILTER, $resolver] + ) + ->willReturn(false); + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('store') + ->withConsecutive( + [$filteredBinary, self::SOURCE_IMAGE, self::FILTER, $resolver], + [$filteredBinary, self::WEBP_IMAGE, self::FILTER, $resolver] + ); + + $this->dataManager + ->expects($this->atLeastOnce()) + ->method('find') + ->with(self::FILTER, self::SOURCE_IMAGE) + ->willReturn($binary); + + $this->filterManager + ->expects($this->atLeastOnce()) + ->method('applyFilter') + ->withConsecutive( + [$binary, self::FILTER, []], + [$binary, self::FILTER, [ + 'format' => 'webp', + ] + self::WEBP_OPTIONS] + ) + ->willReturn($binary); + + $this->assertTrue($service->warmUpCache(self::SOURCE_IMAGE, self::FILTER, $resolver)); + } + + /** + * @dataProvider provideWebpGeneration + */ + public function testWarmsUpCacheNotStoredForced(bool $webpGenerate): void + { + $resolver = null; + $binary = $this + ->getMockBuilder(BinaryInterface::class) + ->getMock(); + $filteredBinary = $this + ->getMockBuilder(BinaryInterface::class) + ->getMock(); + + $service = $this->createFilterService($webpGenerate); + + $this->cacheManager + ->expects($this->never()) + ->method('isStored'); + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('store') + ->withConsecutive( + [$filteredBinary, self::SOURCE_IMAGE, self::FILTER, $resolver], + [$filteredBinary, self::WEBP_IMAGE, self::FILTER, $resolver] + ); + + $this->dataManager + ->expects($this->atLeastOnce()) + ->method('find') + ->with(self::FILTER, self::SOURCE_IMAGE) + ->willReturn($binary); + + $this->filterManager + ->expects($this->atLeastOnce()) + ->method('applyFilter') + ->withConsecutive( + [$binary, self::FILTER, []], + [$binary, self::FILTER, [ + 'format' => 'webp', + ] + self::WEBP_OPTIONS] + ) + ->willReturn($binary); + + $this->assertTrue($service->warmUpCache(self::SOURCE_IMAGE, self::FILTER, $resolver, true)); + } + + /** + * @dataProvider provideWebpGeneration + */ + public function testWarmsUpCacheNonExistingFilter(bool $webpGenerate): void + { + $this->expectException(NonExistingFilterException::class); + + $resolver = null; + $binary = $this + ->getMockBuilder(BinaryInterface::class) + ->getMock(); + $exception = new NonExistingFilterException('Filter not found'); + + $service = $this->createFilterService($webpGenerate); + + $this->dataManager + ->expects($this->atLeastOnce()) + ->method('find') + ->with(self::FILTER, self::SOURCE_IMAGE) + ->willReturn($binary); + + $this->filterManager + ->expects($this->atLeastOnce()) + ->method('applyFilter') + ->withConsecutive( + [$binary, self::FILTER, []], + [$binary, self::FILTER, [ + 'format' => 'webp', + ] + self::WEBP_OPTIONS] + ) + ->willThrowException($exception); + + $this->logger + ->expects($this->atLeastOnce()) + ->method('debug') + ->with(sprintf( + 'Could not locate filter "%s" for path "%s". Message was "%s"', + self::FILTER, + self::SOURCE_IMAGE, + $exception->getMessage() + )); + + $service->warmUpCache(self::SOURCE_IMAGE, self::FILTER, $resolver, true); + } + + /** + * @dataProvider provideWebpGeneration + */ + public function testGetUrlOfFilteredImage(bool $webpGenerate): void + { + $resolver = null; + $url = 'https://example.com/cache'.self::SOURCE_IMAGE; + + $service = $this->createFilterService($webpGenerate); + + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('isStored') + ->withConsecutive( + [self::SOURCE_IMAGE, self::FILTER, $resolver], + [self::WEBP_IMAGE, self::FILTER, $resolver] + ) + ->willReturn(true); + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('resolve') + ->with(self::SOURCE_IMAGE, self::FILTER, $resolver) + ->willReturn($url); + $this->cacheManager + ->expects($this->never()) + ->method('store'); + + $this->dataManager + ->expects($this->never()) + ->method('find'); + + $this->filterManager + ->expects($this->never()) + ->method('applyFilter'); + + $this->assertSame($url, $service->getUrlOfFilteredImage(self::SOURCE_IMAGE, self::FILTER, $resolver)); + } + + /** + * @dataProvider provideWebpGeneration + */ + public function testGetUrlOfFilteredImageWebpSupported(bool $webpGenerate): void + { + $resolver = null; + $url = 'https://example.com/cache'.self::WEBP_IMAGE; + + $service = $this->createFilterService($webpGenerate); + + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('isStored') + ->withConsecutive( + [self::SOURCE_IMAGE, self::FILTER, $resolver], + [self::WEBP_IMAGE, self::FILTER, $resolver] + ) + ->willReturn(true); + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('resolve') + ->with(self::WEBP_IMAGE, self::FILTER, $resolver) + ->willReturn($url); + + $this->cacheManager + ->expects($this->never()) + ->method('store'); + + $this->dataManager + ->expects($this->never()) + ->method('find'); + + $this->filterManager + ->expects($this->never()) + ->method('applyFilter'); + + $this->assertSame($url, $service->getUrlOfFilteredImage(self::SOURCE_IMAGE, self::FILTER, $resolver, true)); + } + + /** + * @dataProvider provideWebpGeneration + */ + public function testGetUrlOfFilteredImageNotStored(bool $webpGenerate): void + { + $resolver = null; + $url = 'https://example.com/cache'.self::SOURCE_IMAGE; + $binary = $this + ->getMockBuilder(BinaryInterface::class) + ->getMock(); + $filteredBinary = $this + ->getMockBuilder(BinaryInterface::class) + ->getMock(); + + $service = $this->createFilterService($webpGenerate); + + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('isStored') + ->withConsecutive( + [self::SOURCE_IMAGE, self::FILTER, $resolver], + [self::WEBP_IMAGE, self::FILTER, $resolver] + ) + ->willReturn(false); + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('store') + ->withConsecutive( + [$filteredBinary, self::SOURCE_IMAGE, self::FILTER, $resolver], + [$filteredBinary, self::WEBP_IMAGE, self::FILTER, $resolver] + ); + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('resolve') + ->with(self::SOURCE_IMAGE, self::FILTER, $resolver) + ->willReturn($url); + + $this->dataManager + ->expects($this->atLeastOnce()) + ->method('find') + ->with(self::FILTER, self::SOURCE_IMAGE) + ->willReturn($binary); + + $this->filterManager + ->expects($this->atLeastOnce()) + ->method('applyFilter') + ->withConsecutive( + [$binary, self::FILTER, []], + [$binary, self::FILTER, [ + 'format' => 'webp', + ] + self::WEBP_OPTIONS] + ) + ->willReturn($binary); + + $this->assertSame($url, $service->getUrlOfFilteredImage(self::SOURCE_IMAGE, self::FILTER, $resolver)); + } + + /** + * @dataProvider provideWebpGeneration + */ + public function testGetUrlOfFilteredImageNotStoredWebpSupported(bool $webpGenerate): void + { + $resolver = null; + $url = 'https://example.com/cache'.self::WEBP_IMAGE; + $binary = $this + ->getMockBuilder(BinaryInterface::class) + ->getMock(); + $filteredBinary = $this + ->getMockBuilder(BinaryInterface::class) + ->getMock(); + + $service = $this->createFilterService($webpGenerate); + + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('isStored') + ->withConsecutive( + [self::SOURCE_IMAGE, self::FILTER, $resolver], + [self::WEBP_IMAGE, self::FILTER, $resolver] + ) + ->willReturn(false); + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('store') + ->withConsecutive( + [$filteredBinary, self::SOURCE_IMAGE, self::FILTER, $resolver], + [$filteredBinary, self::WEBP_IMAGE, self::FILTER, $resolver] + ); + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('resolve') + ->with(self::WEBP_IMAGE, self::FILTER, $resolver) + ->willReturn($url); + + $this->dataManager + ->expects($this->atLeastOnce()) + ->method('find') + ->with(self::FILTER, self::SOURCE_IMAGE) + ->willReturn($binary); + + $this->filterManager + ->expects($this->atLeastOnce()) + ->method('applyFilter') + ->withConsecutive( + [$binary, self::FILTER, []], + [$binary, self::FILTER, [ + 'format' => 'webp', + ] + self::WEBP_OPTIONS] + ) + ->willReturn($binary); + + $this->assertSame($url, $service->getUrlOfFilteredImage(self::SOURCE_IMAGE, self::FILTER, $resolver, true)); + } + + /** + * @dataProvider provideWebpGeneration + */ + public function testGetUrlOfFilteredImageNotExistingFilter(bool $webpGenerate): void + { + $this->expectException(NonExistingFilterException::class); + + $resolver = null; + $binary = $this + ->getMockBuilder(BinaryInterface::class) + ->getMock(); + $exception = new NonExistingFilterException('Filter not found'); + + $service = $this->createFilterService($webpGenerate); + + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('isStored') + ->withConsecutive( + [self::SOURCE_IMAGE, self::FILTER, $resolver], + [self::WEBP_IMAGE, self::FILTER, $resolver] + ) + ->willReturn(false); + + $this->dataManager + ->expects($this->atLeastOnce()) + ->method('find') + ->with(self::FILTER, self::SOURCE_IMAGE) + ->willReturn($binary); + + $this->filterManager + ->expects($this->atLeastOnce()) + ->method('applyFilter') + ->withConsecutive( + [$binary, self::FILTER, []], + [$binary, self::FILTER, [ + 'format' => 'webp', + ] + self::WEBP_OPTIONS] + ) + ->willThrowException($exception); + + $this->logger + ->expects($this->atLeastOnce()) + ->method('debug') + ->with(sprintf( + 'Could not locate filter "%s" for path "%s". Message was "%s"', + self::FILTER, + self::SOURCE_IMAGE, + $exception->getMessage() + )); + + $service->getUrlOfFilteredImage(self::SOURCE_IMAGE, self::FILTER, $resolver); + } + + /** + * @dataProvider provideWebpGeneration + */ + public function testGetUrlOfFilteredWithRuntimeFiltersImage(bool $webpGenerate): void + { + $resolver = null; + $url = 'https://example.com/cache'.self::RUNTIME_IMAGE; + + $service = $this->createFilterService($webpGenerate); + + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('isStored') + ->withConsecutive( + [self::RUNTIME_IMAGE, self::FILTER, $resolver], + [self::RUNTIME_WEBP_IMAGE, self::FILTER, $resolver] + ) + ->willReturn(true); + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('resolve') + ->with(self::RUNTIME_IMAGE, self::FILTER, $resolver) + ->willReturn($url); + $this->cacheManager + ->expects($this->never()) + ->method('store'); + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('getRuntimePath') + ->with(self::SOURCE_IMAGE, self::RUNTIME_FILTERS) + ->willReturn(self::RUNTIME_IMAGE); + + $this->dataManager + ->expects($this->never()) + ->method('find'); + + $this->filterManager + ->expects($this->never()) + ->method('applyFilter'); + + $result = $service->getUrlOfFilteredImageWithRuntimeFilters( + self::SOURCE_IMAGE, + self::FILTER, + self::RUNTIME_FILTERS, + $resolver + ); + + $this->assertSame($url, $result); + } + + /** + * @dataProvider provideWebpGeneration + */ + public function testGetUrlOfFilteredImageWithRuntimeFiltersWebpSupported(bool $webpGenerate): void + { + $resolver = null; + $url = 'https://example.com/cache'.self::RUNTIME_WEBP_IMAGE; + + $service = $this->createFilterService($webpGenerate); + + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('isStored') + ->withConsecutive( + [self::RUNTIME_IMAGE, self::FILTER, $resolver], + [self::RUNTIME_WEBP_IMAGE, self::FILTER, $resolver] + ) + ->willReturn(true); + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('resolve') + ->with(self::RUNTIME_WEBP_IMAGE, self::FILTER, $resolver) + ->willReturn($url); + $this->cacheManager + ->expects($this->never()) + ->method('store'); + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('getRuntimePath') + ->with(self::SOURCE_IMAGE, self::RUNTIME_FILTERS) + ->willReturn(self::RUNTIME_IMAGE); + + $this->dataManager + ->expects($this->never()) + ->method('find'); + + $this->filterManager + ->expects($this->never()) + ->method('applyFilter'); + + $result = $service->getUrlOfFilteredImageWithRuntimeFilters( + self::SOURCE_IMAGE, + self::FILTER, + self::RUNTIME_FILTERS, + $resolver, + true + ); + + $this->assertSame($url, $result); + } + + /** + * @dataProvider provideWebpGeneration + */ + public function testGetUrlOfFilteredImageWithRuntimeFiltersNotStored(bool $webpGenerate): void + { + $resolver = null; + $url = 'https://example.com/cache'.self::RUNTIME_IMAGE; + $runtimeOptions = [ + 'filters' => self::RUNTIME_FILTERS, + ]; + $binary = $this + ->getMockBuilder(BinaryInterface::class) + ->getMock(); + $filteredBinary = $this + ->getMockBuilder(BinaryInterface::class) + ->getMock(); + + $service = $this->createFilterService($webpGenerate); + + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('isStored') + ->withConsecutive( + [self::RUNTIME_IMAGE, self::FILTER, $resolver], + [self::RUNTIME_WEBP_IMAGE, self::FILTER, $resolver] + ) + ->willReturn(false); + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('store') + ->withConsecutive( + [$filteredBinary, self::RUNTIME_IMAGE, self::FILTER, $resolver], + [$filteredBinary, self::RUNTIME_WEBP_IMAGE, self::FILTER, $resolver] + ); + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('resolve') + ->with(self::RUNTIME_IMAGE, self::FILTER, $resolver) + ->willReturn($url); + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('getRuntimePath') + ->with(self::SOURCE_IMAGE, self::RUNTIME_FILTERS) + ->willReturn(self::RUNTIME_IMAGE); + + $this->dataManager + ->expects($this->atLeastOnce()) + ->method('find') + ->with(self::FILTER, self::SOURCE_IMAGE) + ->willReturn($binary); + + $this->filterManager + ->expects($this->atLeastOnce()) + ->method('applyFilter') + ->withConsecutive( + [$binary, self::FILTER, $runtimeOptions], + [$binary, self::FILTER, [ + 'format' => 'webp', + ] + self::WEBP_OPTIONS + $runtimeOptions] + ) + ->willReturn($binary); + + $result = $service->getUrlOfFilteredImageWithRuntimeFilters( + self::SOURCE_IMAGE, + self::FILTER, + self::RUNTIME_FILTERS, + $resolver + ); + + $this->assertSame($url, $result); + } + + /** + * @dataProvider provideWebpGeneration + */ + public function testGetUrlOfFilteredImageWithRuntimeFiltersNotStoredWebpSupported(bool $webpGenerate): void + { + $resolver = null; + $url = 'https://example.com/cache'.self::RUNTIME_WEBP_IMAGE; + $runtimeOptions = [ + 'filters' => self::RUNTIME_FILTERS, + ]; + $binary = $this + ->getMockBuilder(BinaryInterface::class) + ->getMock(); + $filteredBinary = $this + ->getMockBuilder(BinaryInterface::class) + ->getMock(); + + $service = $this->createFilterService($webpGenerate); + + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('isStored') + ->withConsecutive( + [self::RUNTIME_IMAGE, self::FILTER, $resolver], + [self::RUNTIME_WEBP_IMAGE, self::FILTER, $resolver] + ) + ->willReturn(false); + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('store') + ->withConsecutive( + [$filteredBinary, self::RUNTIME_IMAGE, self::FILTER, $resolver], + [$filteredBinary, self::RUNTIME_WEBP_IMAGE, self::FILTER, $resolver] + ); + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('resolve') + ->with(self::RUNTIME_WEBP_IMAGE, self::FILTER, $resolver) + ->willReturn($url); + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('getRuntimePath') + ->with(self::SOURCE_IMAGE, self::RUNTIME_FILTERS) + ->willReturn(self::RUNTIME_IMAGE); + + $this->dataManager + ->expects($this->atLeastOnce()) + ->method('find') + ->with(self::FILTER, self::SOURCE_IMAGE) + ->willReturn($binary); + + $this->filterManager + ->expects($this->atLeastOnce()) + ->method('applyFilter') + ->withConsecutive( + [$binary, self::FILTER, $runtimeOptions], + [$binary, self::FILTER, [ + 'format' => 'webp', + ] + self::WEBP_OPTIONS + $runtimeOptions] + ) + ->willReturn($binary); + + $result = $service->getUrlOfFilteredImageWithRuntimeFilters( + self::SOURCE_IMAGE, + self::FILTER, + self::RUNTIME_FILTERS, + $resolver, + true + ); + + $this->assertSame($url, $result); + } + + /** + * @dataProvider provideWebpGeneration + */ + public function testGetUrlOfFilteredImageWithRuntimeFiltersNotExistingFilter(bool $webpGenerate): void + { + $this->expectException(NonExistingFilterException::class); + $resolver = null; + $runtimeOptions = [ + 'filters' => self::RUNTIME_FILTERS, + ]; + $binary = $this + ->getMockBuilder(BinaryInterface::class) + ->getMock(); + $exception = new NonExistingFilterException('Filter not found'); + + $service = $this->createFilterService($webpGenerate); + + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('isStored') + ->withConsecutive( + [self::RUNTIME_IMAGE, self::FILTER, $resolver], + [self::RUNTIME_WEBP_IMAGE, self::FILTER, $resolver] + ) + ->willReturn(false); + $this->cacheManager + ->expects($this->atLeastOnce()) + ->method('getRuntimePath') + ->with(self::SOURCE_IMAGE, self::RUNTIME_FILTERS) + ->willReturn(self::RUNTIME_IMAGE); + + $this->dataManager + ->expects($this->atLeastOnce()) + ->method('find') + ->with(self::FILTER, self::SOURCE_IMAGE) + ->willReturn($binary); + + $this->filterManager + ->expects($this->atLeastOnce()) + ->method('applyFilter') + ->withConsecutive( + [$binary, self::FILTER, $runtimeOptions], + [$binary, self::FILTER, [ + 'format' => 'webp', + ] + self::WEBP_OPTIONS + $runtimeOptions] + ) + ->willThrowException($exception); + + $this->logger + ->expects($this->atLeastOnce()) + ->method('debug') + ->with(sprintf( + 'Could not locate filter "%s" for path "%s". Message was "%s"', + self::FILTER, + self::SOURCE_IMAGE, + $exception->getMessage() + )); + + $service->getUrlOfFilteredImageWithRuntimeFilters( + self::SOURCE_IMAGE, + self::FILTER, + self::RUNTIME_FILTERS, + $resolver + ); + } + + private function createFilterService(bool $webpGenerate): FilterService + { + return new FilterService( + $this->dataManager, + $this->filterManager, + $this->cacheManager, + $webpGenerate, + self::WEBP_OPTIONS, + $this->logger + ); + } +}