Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG-6.x.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
6.1.0
-----

* Fix usage with AssetMapper

6.0.0
-----

Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
1 change: 1 addition & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
42 changes: 40 additions & 2 deletions src/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -14,12 +15,18 @@ class Manager
protected string $publicPath;
protected string $cachePath;

/**
* @var array<string, string>|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);
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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.
*/
Expand Down
58 changes: 58 additions & 0 deletions tests/ManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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
{
Expand Down