Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a createIfDeferred() method to the ImageResult class #2368

Merged
merged 8 commits into from Oct 14, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
44 changes: 44 additions & 0 deletions core-bundle/src/Image/Studio/ImageResult.php
Expand Up @@ -14,6 +14,8 @@

use Contao\CoreBundle\Image\ImageFactoryInterface;
use Contao\CoreBundle\Image\PictureFactoryInterface;
use Contao\Image\DeferredImageInterface;
use Contao\Image\DeferredResizerInterface;
use Contao\Image\Image;
use Contao\Image\ImageDimensions;
use Contao\Image\ImageInterface;
Expand Down Expand Up @@ -151,6 +153,48 @@ public function getFilePath($absolute = false): string
return $absolute ? $path : Path::makeRelative($path, $this->projectDir);
}

/**
* Synchronously processes images if they are deferred.
*
* This will make sure that the target files physically exist instead of
* being generated by the Contao\CoreBundle\Controller\ImagesController
* on first access.
*/
public function createIfDeferred(): void
{
$picture = $this->getPicture();
$candidates = [];

foreach (array_merge([$picture->getImg()], $picture->getSources()) as $source) {
$candidates[] = $source['src'] ?? null;

foreach ($source['srcset'] ?? [] as $srcset) {
$candidates[] = $srcset[0] ?? null;
}
}

$deferredImages = array_filter(
$candidates,
static function ($image): bool {
return $image instanceof DeferredImageInterface;
}
);
m-vo marked this conversation as resolved.
Show resolved Hide resolved

if (empty($deferredImages)) {
return;
}

$resizer = $this->locator->get('contao.image.resizer');

if (!$resizer instanceof DeferredResizerInterface) {
throw new \RuntimeException('The "contao.image.resizer" service does not support deferred resizing.');
}

foreach ($deferredImages as $deferredImage) {
$resizer->resizeDeferredImage($deferredImage);
}
}

private function imageFactory(): ImageFactoryInterface
{
return $this->locator->get('contao.image.image_factory');
Expand Down
2 changes: 2 additions & 0 deletions core-bundle/src/Image/Studio/Studio.php
Expand Up @@ -18,6 +18,7 @@
use Contao\CoreBundle\Image\PictureFactoryInterface;
use Contao\Image\ImageInterface;
use Contao\Image\PictureConfiguration;
use Contao\Image\ResizerInterface;
use Psr\Container\ContainerInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;

Expand Down Expand Up @@ -79,6 +80,7 @@ public static function getSubscribedServices(): array
self::class,
'contao.image.picture_factory' => PictureFactoryInterface::class,
'contao.image.image_factory' => ImageFactoryInterface::class,
'contao.image.resizer' => ResizerInterface::class,
'contao.assets.files_context' => ContaoContext::class,
'contao.framework' => ContaoFramework::class,
];
Expand Down
1 change: 1 addition & 0 deletions core-bundle/src/Resources/config/services.yml
Expand Up @@ -353,6 +353,7 @@ services:
public: true
tags:
- { name: container.service_subscriber, id: contao.assets.files_context }
- { name: container.service_subscriber, id: contao.image.resizer }

Contao\CoreBundle\Mailer\AvailableTransports:
arguments:
Expand Down
Expand Up @@ -2157,6 +2157,7 @@ public function testRegistersTheImageStudio(): void
[
'container.service_subscriber' => [
['id' => 'contao.assets.files_context'],
['id' => 'contao.image.resizer'],
],
],
$definition->getTags()
Expand Down
217 changes: 216 additions & 1 deletion core-bundle/tests/Image/Studio/ImageResultTest.php
Expand Up @@ -17,10 +17,14 @@
use Contao\CoreBundle\Image\PictureFactoryInterface;
use Contao\CoreBundle\Image\Studio\ImageResult;
use Contao\CoreBundle\Tests\TestCase;
use Contao\Image\DeferredImage;
use Contao\Image\DeferredImageInterface;
use Contao\Image\DeferredResizerInterface;
use Contao\Image\Image;
use Contao\Image\ImageDimensions;
use Contao\Image\ImageInterface;
use Contao\Image\PictureInterface;
use Contao\Image\Resizer;
use Imagine\Image\ImagineInterface;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Container\ContainerInterface;
Expand Down Expand Up @@ -230,6 +234,218 @@ public function testGetFilePathFromImageResource(): void
$this->assertSame('project/dir/file/path', $imageResult->getFilePath(true));
}

/**
* @dataProvider provideDeferredImages
*/
public function testCreateIfDeferred(array $img, array $sources, array $expectedDeferredImages): void
{
$picture = $this->createMock(PictureInterface::class);
$picture
->expects($this->once())
->method('getSources')
->with()
->willReturn($sources)
;

$picture
->expects($this->once())
->method('getImg')
->with()
->willReturn($img)
;

$pictureFactory = $this->createMock(PictureFactoryInterface::class);
$pictureFactory
->method('create')
->willReturn($picture)
;

$deferredResizer = $this->createMock(DeferredResizerInterface::class);
$deferredResizer
->expects(empty($expectedDeferredImages) ? $this->never() : $this->atLeast(\count($expectedDeferredImages)))
->method('resizeDeferredImage')
->with($this->callback(
static function ($deferredImage) use (&$expectedDeferredImages) {
if (false !== ($key = array_search($deferredImage, $expectedDeferredImages, true))) {
unset($expectedDeferredImages[$key]);
}

return true;
}
))
;

$locator = $this->createMock(ContainerInterface::class);
$locator
->method('get')
->willReturnMap([
['contao.image.picture_factory', $pictureFactory],
['contao.image.resizer', $deferredResizer],
])
;

$imageResult = new ImageResult($locator, '/project/dir', '/project/dir/image.jpg');
$imageResult->createIfDeferred();

$this->assertEmpty($expectedDeferredImages, 'test all images were processed');
}

public function provideDeferredImages(): \Generator
{
$imagine = $this->createMock(ImagineInterface::class);
$dimensions = $this->createMock(ImageDimensions::class);

$filesystem = $this->createMock(Filesystem::class);
$filesystem
->method('exists')
->willReturn(true)
;

$image = new Image('/project/dir/assets/image0.jpg', $imagine, $filesystem);
$deferredImage1 = new DeferredImage('/project/dir/assets/image1.jpg', $imagine, $dimensions);
$deferredImage2 = new DeferredImage('/project/dir/assets/image2.jpg', $imagine, $dimensions);
$deferredImage3 = new DeferredImage('/project/dir/assets/image3.jpg', $imagine, $dimensions);
$deferredImage4 = new DeferredImage('/project/dir/assets/image4.jpg', $imagine, $dimensions);

yield 'no deferred images' => [
['src' => $image],
[],
[],
];

yield 'img and sources with deferred images' => [
[
'src' => $deferredImage1,
'srcset' => [[$deferredImage2, 'foo'], [$deferredImage3]],
],
[
[
'src' => $deferredImage3,
'srcset' => [[$deferredImage2], [$deferredImage4]],
],
[
'src' => $deferredImage2,
'srcset' => [[$deferredImage4]],
],
],
[$deferredImage1, $deferredImage2, $deferredImage3, $deferredImage4],
];

yield 'img and sources with both deferred and non-deferred images' => [
[
'src' => $deferredImage1,
],
[
[
'src' => $image,
],
[
'src' => $deferredImage2,
'srcset' => [[$deferredImage3]],
],
],
[$deferredImage1, $deferredImage2, $deferredImage3],
];

yield 'elements without src or srcset key' => [
[
'foo' => 'bar',
],
[
[
'bar' => 'foo',
],
[
'srcset' => [['foo'], [$deferredImage2]],
],
[
'src' => $deferredImage1,
],
],
[$deferredImage1, $deferredImage2],
];
}

public function testCreateIfDeferredFailsWithoutDeferredResizer(): void
{
$picture = $this->createMock(PictureInterface::class);
$picture
->expects($this->once())
->method('getSources')
->with()
->willReturn([])
;

$picture
->expects($this->once())
->method('getImg')
->with()
->willReturn(['src' => $this->createMock(DeferredImageInterface::class)])
;

$pictureFactory = $this->createMock(PictureFactoryInterface::class);
$pictureFactory
->method('create')
->willReturn($picture)
;

$nonDeferredResizer = $this->createMock(Resizer::class);

$locator = $this->createMock(ContainerInterface::class);
$locator
->method('get')
->willReturnMap([
['contao.image.picture_factory', $pictureFactory],
['contao.image.resizer', $nonDeferredResizer],
])
;

$imageResult = new ImageResult($locator, '/project/dir', '/project/dir/image.jpg');

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('The "contao.image.resizer" service does not support deferred resizing.');

$imageResult->createIfDeferred();
}

public function testCreateIfDeferredDoesNotFailWithoutDeferredResizerIfThereAreNoDeferredImages(): void
{
$picture = $this->createMock(PictureInterface::class);
$picture
->expects($this->once())
->method('getSources')
->with()
->willReturn([])
;

$picture
->expects($this->once())
->method('getImg')
->with()
->willReturn([])
;

$pictureFactory = $this->createMock(PictureFactoryInterface::class);
$pictureFactory
->method('create')
->willReturn($picture)
;

$nonDeferredResizer = $this->createMock(Resizer::class);

$locator = $this->createMock(ContainerInterface::class);
$locator
->method('get')
->willReturnMap([
['contao.image.picture_factory', $pictureFactory],
['contao.image.resizer', $nonDeferredResizer],
])
;

$imageResult = new ImageResult($locator, '/project/dir', '/project/dir/image.jpg');
$imageResult->createIfDeferred();
}

/**
* @return PictureFactoryInterface&MockObject
*/
Expand All @@ -252,7 +468,6 @@ private function getPictureFactoryMock($filePathOrImage, $sizeConfiguration, Pic
private function getLocatorMock(?PictureFactoryInterface $pictureFactory = null, string $staticUrl = null)
{
$locator = $this->createMock(ContainerInterface::class);

$context = null;

if (null !== $staticUrl) {
Expand Down
2 changes: 2 additions & 0 deletions core-bundle/tests/Image/Studio/StudioTest.php
Expand Up @@ -18,6 +18,7 @@
use Contao\CoreBundle\Image\PictureFactoryInterface;
use Contao\CoreBundle\Image\Studio\Studio;
use Contao\CoreBundle\Tests\TestCase;
use Contao\Image\ResizerInterface;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Container\ContainerInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
Expand All @@ -39,6 +40,7 @@ public function testSubscribedServices(): void
Studio::class,
PictureFactoryInterface::class,
ImageFactoryInterface::class,
ResizerInterface::class,
ContaoFramework::class,
ContaoContext::class,
];
Expand Down