diff --git a/src/LazyImage/composer.json b/src/LazyImage/composer.json index 414640f0fe1..df2fbba23d9 100644 --- a/src/LazyImage/composer.json +++ b/src/LazyImage/composer.json @@ -36,6 +36,7 @@ "require-dev": { "intervention/image": "^2.5", "kornrunner/blurhash": "^1.1", + "symfony/cache-contracts": "^2.2", "symfony/framework-bundle": "^5.4|^6.0|^7.0", "symfony/phpunit-bridge": "^5.2|^6.0|^7.0", "symfony/twig-bundle": "^5.4|^6.0|^7.0", diff --git a/src/LazyImage/doc/index.rst b/src/LazyImage/doc/index.rst index 3e5f8598f44..eff62572592 100644 --- a/src/LazyImage/doc/index.rst +++ b/src/LazyImage/doc/index.rst @@ -103,11 +103,28 @@ The ``data_uri_thumbnail`` function receives 3 arguments: - the width of the BlurHash to generate - the height of the BlurHash to generate +Performance considerations +~~~~~~~~~~~~~~~~~~~~~~~~~~ + You should try to generate small BlurHash images as generating the image can be CPU-intensive. Instead, you can rely on the browser scaling abilities by generating a small image and using the ``width`` and ``height`` HTML attributes to scale up the image. +You can also configure a cache pool to store the generated BlurHash, +this way you can avoid generating the same BlurHash multiple times: + +.. code-block:: yaml + + # config/packages/lazy_image.yaml + framework: + cache: + pools: + cache.lazy_image: cache.adapter.redis # or any other cache adapter depending on your needs + + lazy_image: + cache: cache.lazy_image # the cache pool to use + Extend the default behavior ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/LazyImage/src/BlurHash/BlurHash.php b/src/LazyImage/src/BlurHash/BlurHash.php index 26092225056..320d64913ad 100644 --- a/src/LazyImage/src/BlurHash/BlurHash.php +++ b/src/LazyImage/src/BlurHash/BlurHash.php @@ -21,11 +21,9 @@ */ class BlurHash implements BlurHashInterface { - private $imageManager; - - public function __construct(?ImageManager $imageManager = null) - { - $this->imageManager = $imageManager; + public function __construct( + private ?ImageManager $imageManager = null, + ) { } public function createDataUriThumbnail(string $filename, int $width, int $height, int $encodingWidth = 75, int $encodingHeight = 75): string diff --git a/src/LazyImage/src/BlurHash/CachedBlurHash.php b/src/LazyImage/src/BlurHash/CachedBlurHash.php new file mode 100644 index 00000000000..e5dad760ee7 --- /dev/null +++ b/src/LazyImage/src/BlurHash/CachedBlurHash.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\LazyImage\BlurHash; + +use Symfony\Contracts\Cache\CacheInterface; + +/** + * Decorate a BlurHashInterface to cache the result of the encoding, for performance purposes. + * + * @author Hugo Alliaume + * + * @final + */ +class CachedBlurHash implements BlurHashInterface +{ + public function __construct( + private BlurHashInterface $blurHash, + private CacheInterface $cache, + ) { + } + + public function createDataUriThumbnail(string $filename, int $width, int $height, int $encodingWidth = 75, int $encodingHeight = 75): string + { + return $this->blurHash->createDataUriThumbnail($filename, $width, $height, $encodingWidth, $encodingHeight); + } + + public function encode(string $filename, int $encodingWidth = 75, int $encodingHeight = 75): string + { + return $this->cache->get( + 'blurhash.'.hash('xxh3', $filename.$encodingWidth.$encodingHeight), + fn () => $this->blurHash->encode($filename, $encodingWidth, $encodingHeight) + ); + } +} diff --git a/src/LazyImage/src/DependencyInjection/Configuration.php b/src/LazyImage/src/DependencyInjection/Configuration.php new file mode 100644 index 00000000000..ed7568e4448 --- /dev/null +++ b/src/LazyImage/src/DependencyInjection/Configuration.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\LazyImage\DependencyInjection; + +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\ConfigurationInterface; + +/** + * @author Hugo Alliaume + * + * @internal + */ +final class Configuration implements ConfigurationInterface +{ + public function getConfigTreeBuilder(): TreeBuilder + { + $treeBuilder = new TreeBuilder('ux_lazy_image'); + $rootNode = $treeBuilder->getRootNode(); + $rootNode + ->children() + ->scalarNode('cache')->end() + ->end() + ; + + return $treeBuilder; + } +} diff --git a/src/LazyImage/src/DependencyInjection/LazyImageExtension.php b/src/LazyImage/src/DependencyInjection/LazyImageExtension.php index 58ff683e536..be5686f8400 100644 --- a/src/LazyImage/src/DependencyInjection/LazyImageExtension.php +++ b/src/LazyImage/src/DependencyInjection/LazyImageExtension.php @@ -21,6 +21,7 @@ use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\UX\LazyImage\BlurHash\BlurHash; use Symfony\UX\LazyImage\BlurHash\BlurHashInterface; +use Symfony\UX\LazyImage\BlurHash\CachedBlurHash; use Symfony\UX\LazyImage\Twig\BlurHashExtension; /** @@ -32,6 +33,9 @@ class LazyImageExtension extends Extension implements PrependExtensionInterface { public function load(array $configs, ContainerBuilder $container) { + $configuration = new Configuration(); + $config = $this->processConfiguration($configuration, $configs); + if (class_exists(ImageManager::class)) { $container ->setDefinition('lazy_image.image_manager', new Definition(ImageManager::class)) @@ -47,6 +51,17 @@ public function load(array $configs, ContainerBuilder $container) $container->setAlias(BlurHashInterface::class, 'lazy_image.blur_hash')->setPublic(false); + if (isset($config['cache'])) { + $container + ->setDefinition('lazy_image.cached_blur_hash', new Definition(CachedBlurHash::class)) + ->setDecoratedService('lazy_image.blur_hash') + ->addArgument(new Reference('lazy_image.cached_blur_hash.inner')) + ->addArgument(new Reference($config['cache'])) + ; + + $container->setAlias(BlurHashInterface::class, 'lazy_image.blur_hash')->setPublic(false); + } + $container ->setDefinition('twig.extension.blur_hash', new Definition(BlurHashExtension::class)) ->addArgument(new Reference('lazy_image.blur_hash')) diff --git a/src/LazyImage/tests/BlurHash/BlurHashTest.php b/src/LazyImage/tests/BlurHash/BlurHashTest.php index ea3cbff56bf..56ad512b89c 100644 --- a/src/LazyImage/tests/BlurHash/BlurHashTest.php +++ b/src/LazyImage/tests/BlurHash/BlurHashTest.php @@ -12,7 +12,11 @@ namespace Symfony\UX\LazyImage\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\UX\LazyImage\BlurHash\BlurHashInterface; +use Symfony\UX\LazyImage\BlurHash\CachedBlurHash; use Symfony\UX\LazyImage\Tests\Kernel\TwigAppKernel; /** @@ -37,6 +41,76 @@ public function testEncode() ); } + public function testEnsureCacheIsNotUsedWhenNotConfigured() + { + $kernel = new TwigAppKernel('test', true); + $kernel->boot(); + $container = $kernel->getContainer()->get('test.service_container'); + + /** @var BlurHashInterface $blurHash */ + $blurHash = $container->get('test.lazy_image.blur_hash'); + + static::assertNotInstanceOf(CachedBlurHash::class, $blurHash); + } + + public function testEnsureCacheIsUsedWhenConfigured() + { + $kernel = new class('test', true) extends TwigAppKernel { + public function registerContainerConfiguration(LoaderInterface $loader) + { + parent::registerContainerConfiguration($loader); + + $loader->load(static function (ContainerBuilder $container) { + $container->loadFromExtension('framework', [ + 'cache' => [ + 'pools' => [ + 'cache.lazy_image' => [ + 'adapter' => 'cache.adapter.array', + ], + ], + ], + ]); + + $container->loadFromExtension('lazy_image', [ + 'cache' => 'cache.lazy_image', + ]); + + $container->setAlias('test.cache.lazy_image', 'cache.lazy_image')->setPublic(true); + }); + } + }; + + $kernel->boot(); + $container = $kernel->getContainer()->get('test.service_container'); + + /** @var BlurHashInterface $blurHash */ + $blurHash = $container->get('test.lazy_image.blur_hash'); + + static::assertInstanceOf(CachedBlurHash::class, $blurHash); + } + + public function testEncodeShouldBeCalledOnceWhenCached() + { + $blurHash = $this->createMock(BlurHashInterface::class); + $blurHash->expects($this->once())->method('encode')->with(__DIR__.'/../Fixtures/logo.png')->willReturn('L54ec*~q_3?bofoffQWB9F9FD%IU'); + $cache = new ArrayAdapter(); + $cachedBlurHash = new CachedBlurHash($blurHash, $cache); + + $this->assertEmpty($cache->getValues()); + + $this->assertSame( + 'L54ec*~q_3?bofoffQWB9F9FD%IU', + $cachedBlurHash->encode(__DIR__.'/../Fixtures/logo.png') + ); + + $this->assertNotEmpty($cache->getValues()); + + $this->assertSame( + 'L54ec*~q_3?bofoffQWB9F9FD%IU', + $cachedBlurHash->encode(__DIR__.'/../Fixtures/logo.png') + ); + } + public function testCreateDataUriThumbnail() { $kernel = new TwigAppKernel('test', true);