From 8d85b6e39c28ec69b805799078c7d509929b6199 Mon Sep 17 00:00:00 2001 From: jmsche Date: Sun, 30 Aug 2026 17:22:06 +0200 Subject: [PATCH] Fix usage with AssetMapper --- CHANGELOG-6.x.md | 5 ++++ composer.json | 1 + config/services.php | 1 + src/Manager.php | 42 +++++++++++++++++++++++++++++-- tests/ManagerTest.php | 58 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 105 insertions(+), 2 deletions(-) diff --git a/CHANGELOG-6.x.md b/CHANGELOG-6.x.md index e174405..794570d 100644 --- a/CHANGELOG-6.x.md +++ b/CHANGELOG-6.x.md @@ -1,3 +1,8 @@ +6.1.0 +----- + +* Fix usage with AssetMapper + 6.0.0 ----- diff --git a/composer.json b/composer.json index 9ad376f..5f1aa64 100644 --- a/composer.json +++ b/composer.json @@ -44,6 +44,7 @@ "phpstan/phpstan": "^2.1.40", "phpstan/phpstan-deprecation-rules": "^2.0.4", "phpunit/phpunit": "^13.0.5", + "symfony/asset-mapper": "^7.4 || ^8.0", "symfony/browser-kit": "^7.4 || ^8.0", "symplify/easy-coding-standard": "^13.0.4" }, diff --git a/config/services.php b/config/services.php index a3a9d7e..48fedc3 100644 --- a/config/services.php +++ b/config/services.php @@ -34,6 +34,7 @@ ->arg('$publicPath', param('leapt_im.public_path')) ->arg('$cachePath', param('leapt_im.cache_path')) ->arg('$formats', param('leapt_im.formats')) + ->arg('$assetMapper', service('asset_mapper')->nullOnInvalid()) ->set(MogrifySubscriber::class) ->arg('$imManager', service(Manager::class)) diff --git a/src/Manager.php b/src/Manager.php index f08c748..a03805d 100644 --- a/src/Manager.php +++ b/src/Manager.php @@ -6,6 +6,7 @@ use Leapt\ImBundle\Exception\InvalidArgumentException; use Leapt\ImBundle\Exception\NotFoundException; +use Symfony\Component\AssetMapper\AssetMapperInterface; use Symfony\Component\HttpKernel\Exception\HttpException; class Manager @@ -14,12 +15,18 @@ class Manager protected string $publicPath; protected string $cachePath; + /** + * @var array|null Map of asset mapper public paths (without leading slash) to their filesystem source path + */ + private ?array $assetMapperIndex = null; + public function __construct( protected Wrapper $wrapper, string $projectDir, string $publicPath, string $cachePath, protected array $formats = [], + private ?AssetMapperInterface $assetMapper = null, ) { $this->setProjectDir($projectDir); $this->setPublicPath($publicPath); @@ -104,9 +111,10 @@ public function getUrl(string $format, string $path): string public function convert(string|array $format, string $file): string { $file = ltrim($file, '/'); - $this->checkImage($file); + $sourceFile = $this->resolveSourceFile($file); + $this->checkImage($sourceFile); - return $this->wrapper->run('convert', $this->getPublicDirectory() . '/' . $file, $this->convertFormat($format), $this->getCacheDirectory() . '/' . $this->pathify($format) . '/' . $file); + return $this->wrapper->run('convert', $sourceFile, $this->convertFormat($format), $this->getCacheDirectory() . '/' . $this->pathify($format) . '/' . $file); } /** @@ -169,6 +177,36 @@ private function convertFormat(mixed $format): array throw new InvalidArgumentException(\sprintf('Unknown IM format: %s', $format)); } + /** + * Resolves a public path relative to the public directory to its filesystem source path. + * + * Files served through Symfony's AssetMapper (e.g. `asset('images/photo.jpg')` resolving to + * `/assets/images/photo-abc123.jpg`) are not necessarily written to the public directory, + * so their source file has to be looked up through the asset mapper instead. + */ + private function resolveSourceFile(string $file): string + { + $publicFile = $this->getPublicDirectory() . '/' . $file; + if (is_file($publicFile)) { + return $publicFile; + } + + if (null !== $this->assetMapper) { + if (null === $this->assetMapperIndex) { + $this->assetMapperIndex = []; + foreach ($this->assetMapper->allAssets() as $asset) { + $this->assetMapperIndex[ltrim($asset->publicPath, '/')] = $asset->sourcePath; + } + } + + if (isset($this->assetMapperIndex[$file])) { + return $this->assetMapperIndex[$file]; + } + } + + return $publicFile; + } + /** * Validates that an image exists. */ diff --git a/tests/ManagerTest.php b/tests/ManagerTest.php index 8db1fbe..23bc7bc 100644 --- a/tests/ManagerTest.php +++ b/tests/ManagerTest.php @@ -13,6 +13,8 @@ use org\bovigo\vfs\vfsStreamDirectory; use PHPUnit\Framework\Attributes\Depends; use PHPUnit\Framework\TestCase; +use Symfony\Component\AssetMapper\AssetMapperInterface; +use Symfony\Component\AssetMapper\MappedAsset; final class ManagerTest extends TestCase { @@ -168,6 +170,62 @@ public function testCheckImageException(Manager $manager): void $method->invoke($manager, 'someinexistantfile'); } + public function testResolveSourceFileFallsBackToPublicDirectoryWhenFileExists(): void + { + vfsStream::create([ + 'app' => [], + 'public' => [ + 'images' => ['photo.jpg' => 'content'], + ], + ], $this->root); + + $wrapper = new Wrapper(Process::class); + $manager = new Manager($wrapper, $this->projectDir, $this->publicPath, $this->cachePath); + + $method = new \ReflectionMethod($manager, 'resolveSourceFile'); + + $this->assertEquals( + $this->projectDir . '/../public/images/photo.jpg', + $method->invoke($manager, 'images/photo.jpg'), + ); + } + + public function testResolveSourceFileUsesAssetMapperWhenFileIsNotInPublicDirectory(): void + { + $sourcePath = 'vfs://root/assets/images/photo.jpg'; + vfsStream::create([ + 'assets' => ['images' => ['photo.jpg' => 'content']], + ], $this->root); + + $asset = new MappedAsset('images/photo.jpg', sourcePath: $sourcePath, publicPath: '/assets/images/photo-abc123.jpg'); + + $assetMapper = $this->createStub(AssetMapperInterface::class); + $assetMapper->method('allAssets')->willReturn([$asset]); + + $wrapper = new Wrapper(Process::class); + $manager = new Manager($wrapper, $this->projectDir, $this->publicPath, $this->cachePath, assetMapper: $assetMapper); + + $method = new \ReflectionMethod($manager, 'resolveSourceFile'); + + $this->assertEquals($sourcePath, $method->invoke($manager, 'assets/images/photo-abc123.jpg')); + } + + public function testResolveSourceFileFallsBackToPublicDirectoryWhenAssetMapperHasNoMatch(): void + { + $assetMapper = $this->createStub(AssetMapperInterface::class); + $assetMapper->method('allAssets')->willReturn([]); + + $wrapper = new Wrapper(Process::class); + $manager = new Manager($wrapper, $this->projectDir, $this->publicPath, $this->cachePath, assetMapper: $assetMapper); + + $method = new \ReflectionMethod($manager, 'resolveSourceFile'); + + $this->assertEquals( + $this->projectDir . '/../public/unknown.jpg', + $method->invoke($manager, 'unknown.jpg'), + ); + } + #[Depends('testConstruct')] public function testPathify(Manager $manager): void {