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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ for a given releases. Unreleased, upcoming changes will be updated here periodic

## 2.18.0 (unreleased)

- Fall back to the `default_image` when a filter does not exist or the image can not be generated, unless the new `controller.debug` option is enabled ([Amoifr](https://github.com/liip/LiipImagineBundle/pull/1665))
- Dispatch pre/post store and pre/post remove events on the CacheManager, so applications can react to the cache storage lifecycle ([ousamabenyounes](https://github.com/liip/LiipImagineBundle/pull/1664))
- Fix bug that we did not delete the webp variant when enabled, when deleting an image through the CacheManager ([ousamabenyounes](https://github.com/liip/LiipImagineBundle/pull/1661))
- Drop support for PHP 8.0.
Expand Down
16 changes: 15 additions & 1 deletion Config/Controller/ControllerConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,31 @@ final class ControllerConfig

private $redirectResponseCode;

public function __construct(int $redirectResponseCode)
/**
* @var bool
*/
private $debug;

public function __construct(int $redirectResponseCode, bool $debug = true)
{
if (!\in_array($redirectResponseCode, self::REDIRECT_RESPONSE_CODES, true)) {
throw new InvalidArgumentException(\sprintf('Invalid redirect response code "%s" (must be 201, 301, 302, 303, 307, or 308).', $redirectResponseCode));
}

$this->redirectResponseCode = $redirectResponseCode;
$this->debug = $debug;
}

public function getRedirectResponseCode(): int
{
return $this->redirectResponseCode;
}

/**
* Whether failures to generate an image must surface as an error rather than fall back to the default image.
*/
public function isDebug(): bool
{
return $this->debug;
}
}
52 changes: 49 additions & 3 deletions Controller/ImagineController.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
use Liip\ImagineBundle\Imagine\Cache\SignerInterface;
use Liip\ImagineBundle\Imagine\Data\DataManager;
use Liip\ImagineBundle\Service\FilterService;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
Expand Down Expand Up @@ -47,15 +49,22 @@ class ImagineController
*/
private $controllerConfig;

/**
* @var LoggerInterface
*/
private $logger;

public function __construct(
FilterService $filterService,
DataManager $dataManager,
SignerInterface $signer,
?ControllerConfig $controllerConfig = null
?ControllerConfig $controllerConfig = null,
?LoggerInterface $logger = null
) {
$this->filterService = $filterService;
$this->dataManager = $dataManager;
$this->signer = $signer;
$this->logger = $logger ?: new NullLogger();

if (null === $controllerConfig) {
@trigger_error(\sprintf(
Expand Down Expand Up @@ -151,18 +160,55 @@ private function createRedirectResponse(\Closure $url, string $path, string $fil
try {
return new RedirectResponse($url(), $this->controllerConfig->getRedirectResponseCode());
} catch (NotLoadableException $exception) {
if (null !== $this->dataManager->getDefaultImageUrl($filter)) {
return new RedirectResponse($this->dataManager->getDefaultImageUrl($filter));
// this fallback predates the debug mode check, so it keeps applying in debug mode
Comment thread
dbu marked this conversation as resolved.
Resolved
// TODO next major version: change to throw exception in debug mode and get rid fo the evenInDebug flag
if (null !== $response = $this->createDefaultImageResponse($filter, $exception, true)) {
return $response;
}

throw new NotFoundHttpException(\sprintf('Source image for path "%s" could not be found', $path), $exception);
} catch (NonExistingFilterException $exception) {
if (null !== $response = $this->createDefaultImageResponse($filter, $exception)) {
return $response;
}

throw new NotFoundHttpException(\sprintf('Requested non-existing filter "%s"', $filter), $exception);
} catch (RuntimeException $exception) {
if (null !== $response = $this->createDefaultImageResponse($filter, $exception)) {
return $response;
}

throw new \RuntimeException(vsprintf('Unable to create image for path "%s" and filter "%s". Message was "%s"', [$hash ? \sprintf('%s/%s', $hash, $path) : $path, $filter, $exception->getMessage()]), 0, $exception);
}
}

/**
* Outside of debug mode, an image that can not be generated at all is replaced by the default image rather
* than by an error page. The exception is logged so that the problem is not silently swallowed.
*
* @param bool $evenInDebug Whether to fall back in debug mode too, for the callers that already did so
*/
private function createDefaultImageResponse(string $filter, \Throwable $exception, bool $evenInDebug = false): ?RedirectResponse
{
if (!$evenInDebug && $this->controllerConfig->isDebug()) {
return null;
}

$defaultImageUrl = $this->dataManager->getDefaultImageUrl($filter);

if (null === $defaultImageUrl) {
return null;
}

$this->logger->warning('Failed to create image for filter "{filter}", falling back to the default image. Message was "{message}"', [
'filter' => $filter,
'message' => $exception->getMessage(),
'exception' => $exception,
]);

return new RedirectResponse($defaultImageUrl);
}

private function isWebpSupported(Request $request): bool
{
return false !== mb_stripos($request->headers->get('accept', ''), 'image/webp');
Expand Down
4 changes: 4 additions & 0 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ public function getConfigTreeBuilder(): TreeBuilder
->thenInvalid('Invalid redirect response code "%s" (must be 201, 301, 302, 303, 307, or 308).')
->end()
->end()
->booleanNode('debug')
->defaultNull()
->info('Whether to report images that can not be generated as an error. Defaults to the kernel debug mode. When disabled, the default image is served instead, if one is configured.')
->end()
->end()
->end()
->arrayNode('filter_sets')
Expand Down
3 changes: 2 additions & 1 deletion DependencyInjection/LiipImagineExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ public function load(array $configs, ContainerBuilder $container): void

$container
->getDefinition('liip_imagine.controller.config')
->replaceArgument(0, $config['controller']['redirect_response_code']);
->replaceArgument(0, $config['controller']['redirect_response_code'])
->replaceArgument(1, $config['controller']['debug'] ?? '%kernel.debug%');

$container->setAlias('liip_imagine', new Alias("liip_imagine.{$driver}"));
$container->setAlias(CacheManager::class, new Alias('liip_imagine.cache.manager', false));
Expand Down
3 changes: 2 additions & 1 deletion Resources/config/imagine.php
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@
// Config
$services->set('liip_imagine.controller.config', ControllerConfig::class)
->private()
->args(['']);
->args(['', true]);

// Controller
$services->set(ImagineController::class)
Expand All @@ -274,6 +274,7 @@
service('liip_imagine.data.manager'),
service('liip_imagine.cache.signer'),
service('liip_imagine.controller.config'),
service('logger')->ignoreOnInvalid(),
]);

$services->alias('liip_imagine.controller', ImagineController::class)
Expand Down
35 changes: 35 additions & 0 deletions Resources/doc/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ The default configuration for the bundle looks like this:
filter_action: liip_imagine.controller::filterAction
filter_runtime_action: liip_imagine.controller::filterRuntimeAction
redirect_response_code: 302
debug: ~ # %kernel.debug%
webp:
generate: false
quality: 100
Expand Down Expand Up @@ -79,6 +80,8 @@ There are several configuration options available:
* ``data_loader`` - name of a custom data loader. Default value: ``filesystem``
(which means the standard filesystem loader is used). Built-in loaders include:
``filesystem``, ``chain``, ``flysystem``, ``stream``, and ``asset_mapper``.
* ``default_image`` - image served when the requested one can not be delivered, for every filter set
that does not define its own. Default value: ``null``. See `Default image`_ below.
* ``twig.mode`` - Twig filter integration. ``none`` disables the twig filters, ``lazy`` enables
Twig using the Twig runtime for lazy loading. The default value is ``legacy`` and enables the
old Twig integration that is loaded on each request. Version 3 will drop ``legacy`` and default
Expand All @@ -94,6 +97,8 @@ There are several configuration options available:
* ``redirect_response_code`` - The HTTP redirect response code to return from the imagine controller,
one of ``201``, ``301``, ``302``, ``303``, ``307``, or ``308``. Default value: ``302``
See :doc:`optimizations/avoid-redirects` if you want to change this configuration.
* ``debug`` - Whether an image that can not be generated is reported as an error. Default value:
``%kernel.debug%``. See `Default image`_ below.
* ``webp``
* ``generate`` - enabling the generation a copy of the image in the WebP format.
* ``quality`` - override the quality from filter option.
Expand Down Expand Up @@ -126,6 +131,7 @@ Each filter set that you specify has the following options:
``filters`` parameter for ``imagepng`` function in `PHP manual`_ for more details)
* ``cache`` - override the default cache setting.
* ``data_loader`` - override the default data loader.
* ``default_image`` - override the default image of this filter set. See `Default image`_ below.
* ``route`` - optional list of route requirements, defaults and options using in
the route loader. Add array with keys ``requirements``, ``defaults`` or ``options``.
Default value: empty array.
Expand All @@ -135,4 +141,33 @@ Each filter set that you specify has the following options:
Imagine (PR pending))


Default image
-------------

When the imagine controller can not deliver an image, it redirects to the ``default_image`` of the
requested filter set, falling back to the global ``default_image``. Both are URLs to an existing
image, and both default to ``null``, which disables the mechanism:

.. code-block:: yaml

liip_imagine:
default_image: /images/placeholder.png
filter_sets:
avatar:
default_image: /images/anonymous.png
filters:
thumbnail: { size: [100, 100], mode: outbound }

Which failures are replaced by that image depends on ``controller.debug``:

* A source image that can not be loaded always leads to the default image. This is the common case
of a path that points at nothing.
* A filter set that does not exist, or an image that the driver fails to process, leads to the
default image only when ``controller.debug`` is disabled, which by default means outside of the
Symfony debug mode. The exception is then logged with the ``warning`` level, so that a broken
filter or an unsupported source file does not go unnoticed.

In debug mode those two remain exceptions, so that a mistake in the configuration or a file the
driver chokes on is visible while developing instead of silently turning into a placeholder.

.. _`PHP Manual`: https://php.net/imagepng
11 changes: 11 additions & 0 deletions Tests/Config/Controller/ControllerConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,15 @@ public function testInvalidRedirectResponseCode(int $redirectResponseCode): void
));
$this->assertSame($redirectResponseCode, (new ControllerConfig($redirectResponseCode))->getRedirectResponseCode());
}

public function testDebugDefaultsToEnabled(): void
{
$this->assertTrue((new ControllerConfig(302))->isDebug());
}

public function testDebug(): void
{
$this->assertFalse((new ControllerConfig(302, false))->isDebug());
$this->assertTrue((new ControllerConfig(302, true))->isDebug());
}
}
Loading
Loading