Skip to content

Commit

Permalink
Merge pull request #1395 from liip/alcaeus-allow-doctrine-cache-2
Browse files Browse the repository at this point in the history
Allow installing doctrine/cache 2.0
  • Loading branch information
dbu authored Oct 6, 2021
2 parents c6ac422 + 11e01f5 commit 45f09d8
Show file tree
Hide file tree
Showing 8 changed files with 595 additions and 41 deletions.
1 change: 1 addition & 0 deletions Imagine/Cache/Resolver/CacheResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Liip\ImagineBundle\Binary\BinaryInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

/** @deprecated Deprecated in favour of the PsrCacheResolver class */
class CacheResolver implements ResolverInterface
{
/**
Expand Down
267 changes: 267 additions & 0 deletions Imagine/Cache/Resolver/PsrCacheResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
<?php

/*
* This file is part of the `liip/LiipImagineBundle` project.
*
* (c) https://github.com/liip/LiipImagineBundle/graphs/contributors
*
* For the full copyright and license information, please view the LICENSE.md
* file that was distributed with this source code.
*/

namespace Liip\ImagineBundle\Imagine\Cache\Resolver;

use Liip\ImagineBundle\Binary\BinaryInterface;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use function str_replace;
use Symfony\Component\OptionsResolver\OptionsResolver;

final class PsrCacheResolver implements ResolverInterface
{
private const RESERVED_CHARACTERS = [
'{',
'}',
'(',
')',
'/',
'\\',
'@',
':',
'.',
];

/**
* @var CacheItemPoolInterface
*/
private $cache;

/**
* @var array
*/
private $options = [];

/**
* @var ResolverInterface
*/
private $resolver;

/**
* Constructor.
*
* Available options:
* * global_prefix
* A prefix for all keys within the cache. This is useful to avoid colliding keys when using the same cache for different systems.
* * prefix
* A "local" prefix for this wrapper. This is useful when re-using the same resolver for multiple filters.
* * index_key
* The name of the index key being used to save a list of created cache keys regarding one image and filter pairing.
*
* @param OptionsResolver $optionsResolver
*/
public function __construct(CacheItemPoolInterface $cache, ResolverInterface $cacheResolver, array $options = [], OptionsResolver $optionsResolver = null)
{
$this->cache = $cache;
$this->resolver = $cacheResolver;

if (null === $optionsResolver) {
$optionsResolver = new OptionsResolver();
}

$this->configureOptions($optionsResolver);
$this->options = $optionsResolver->resolve($options);
}

/**
* {@inheritdoc}
*/
public function isStored($path, $filter)
{
$cacheKey = $this->generateCacheKey($path, $filter);

return
$this->cache->hasItem($cacheKey) ||
$this->resolver->isStored($path, $filter);
}

/**
* {@inheritdoc}
*/
public function resolve($path, $filter)
{
$key = $this->generateCacheKey($path, $filter);
$item = $this->cache->getItem($key);
if ($item->isHit()) {
return $item->get();
}

$resolved = $this->resolver->resolve($path, $filter);

$item->set($resolved);
$this->saveToCache($item);

return $resolved;
}

/**
* {@inheritdoc}
*/
public function store(BinaryInterface $binary, $path, $filter)
{
$this->resolver->store($binary, $path, $filter);
}

/**
* {@inheritdoc}
*/
public function remove(array $paths, array $filters)
{
$this->resolver->remove($paths, $filters);

foreach ($filters as $filter) {
if (empty($paths)) {
$this->removePathAndFilter(null, $filter);
} else {
foreach ($paths as $path) {
$this->removePathAndFilter($path, $filter);
}
}
}
}

/**
* Generate a unique cache key based on the given parameters.
*
* When overriding this method, ensure generateIndexKey is adjusted accordingly.
*
* @param string $path The image path in use
* @param string $filter The filter in use
*
* @return string
*/
public function generateCacheKey($path, $filter)
{
return implode('.', [
$this->sanitizeCacheKeyPart($this->options['global_prefix']),
$this->sanitizeCacheKeyPart($this->options['prefix']),
$this->sanitizeCacheKeyPart($filter),
$this->sanitizeCacheKeyPart($path),
]);
}

private function removePathAndFilter($path, $filter)
{
$indexKey = $this->generateIndexKey($this->generateCacheKey($path, $filter));
$indexItem = $this->cache->getItem($indexKey);
if (!$indexItem->isHit()) {
return;
}

$index = $indexItem->get();

if (null === $path) {
foreach ($index as $eachCacheKey) {
$this->cache->deleteItem($eachCacheKey);
}

$index = [];
} else {
$cacheKey = $this->generateCacheKey($path, $filter);
if (false !== $indexIndex = array_search($cacheKey, $index, true)) {
unset($index[$indexIndex]);
$this->cache->deleteItem($cacheKey);
}
}

if (empty($index)) {
$this->cache->deleteItem($indexKey);
} else {
$indexItem->set($index);
$this->cache->save($indexItem);
}
}

/**
* Generate the index key for the given cacheKey.
*
* The index contains a list of cache keys related to an image and a filter.
*
* @param string $cacheKey
*
* @return string
*/
private function generateIndexKey($cacheKey)
{
$cacheKeyStack = explode('.', $cacheKey);

return implode('.', [
$this->sanitizeCacheKeyPart($this->options['global_prefix']),
$this->sanitizeCacheKeyPart($this->options['prefix']),
$this->sanitizeCacheKeyPart($this->options['index_key']),
$this->sanitizeCacheKeyPart($cacheKeyStack[2]), // filter
]);
}

/**
* @param string $cacheKeyPart
*
* @return string
*/
private function sanitizeCacheKeyPart($cacheKeyPart)
{
return str_replace(self::RESERVED_CHARACTERS, '_', $cacheKeyPart);
}

/**
* Save the given content to the cache and update the cache index.
*
* @return bool
*/
private function saveToCache(CacheItemInterface $item)
{
$cacheKey = $item->getKey();

// Create or update the index list containing all cache keys for a given image and filter pairing.
$indexKey = $this->generateIndexKey($cacheKey);
$indexItem = $this->cache->getItem($indexKey);
if ($indexItem->isHit()) {
$index = (array) $indexItem->get();

if (!\in_array($cacheKey, $index, true)) {
$index[] = $cacheKey;
}
} else {
$index = [$cacheKey];
}

$indexItem->set($index);
$this->cache->saveDeferred($indexItem);
$this->cache->saveDeferred($item);

return $this->cache->commit();
}

private function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'global_prefix' => 'liip_imagine.resolver_psr_cache',
'prefix' => \get_class($this->resolver),
'index_key' => 'index',
]);

$allowedTypesList = [
'global_prefix' => 'string',
'prefix' => 'string',
'index_key' => 'string',
];

foreach ($allowedTypesList as $option => $allowedTypes) {
$resolver->setAllowedTypes($option, $allowedTypes);
}
}

private function setDefaultOptions(OptionsResolver $resolver)
{
$this->configureOptions($resolver);
}
}
5 changes: 5 additions & 0 deletions Resources/config/imagine.xml
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,11 @@
<argument><!-- will be injected by AwsS3ResolverFactory --></argument>
</service>

<service id="liip_imagine.cache.resolver.prototype.psr_cache" class="Liip\ImagineBundle\Imagine\Cache\Resolver\PsrCacheResolver" public="true" abstract="true">
<argument><!-- will be injected by a ResolverFactory --></argument>
<argument><!-- will be injected by a ResolverFactory --></argument>
</service>

<service id="liip_imagine.cache.resolver.no_cache_web_path" class="Liip\ImagineBundle\Imagine\Cache\Resolver\NoCacheWebPathResolver" public="true">
<argument type="service" id="router.request_context" />
<tag name="liip_imagine.cache.resolver" resolver="no_cache" />
Expand Down
104 changes: 104 additions & 0 deletions Resources/doc/cache-resolver/psr_cache.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@

.. _cache-resolver-cache:

PSR Cache Resolver
==============

A caching resolver based on the PSR-6 cache standard.

The ``PsrCacheResolver`` cannot be used by itself. Instead, it is a "wrapper" for
another resolver.


Dependencies
------------

This cache resolver requires a PSR-6 implementation, e.g. `symfony/cache`, which can be installed
by executing the following command in your project directory:

.. code-block:: bash
$ composer require symfony/cache
Configuration
-------------

First, you need to setup the required services. In this example we're wrapping an
instance of ``AmazonS3Resolver`` inside this resolver.

See the `Symfony documentation on configuring caching`_ for how to configure the PSR-6 caching implementation.

.. code-block:: yaml
# app/config/services.yml
services:
acme.amazon_s3:
class: AmazonS3
arguments:
-
key: "%amazon_s3.key%"
secret: "%amazon_s3.secret%"
acme.imagine.cache.resolver.amazon_s3:
class: Liip\ImagineBundle\Imagine\Cache\Resolver\AmazonS3Resolver
arguments:
- "@acme.amazon_s3"
- "%amazon_s3.bucket%"
acme.imagine.psr_cache.resolver.amazon_s3.cache:
class: Liip\ImagineBundle\Imagine\Cache\Resolver\PsrCacheResolver
arguments:
- "@cache.adapter.memcached"
- "@acme.imagine.cache.resolver.amazon_s3"
- { prefix: "amazon_s3" }
tags:
- { name: "liip_imagine.cache.resolver", resolver: "cached_amazon_s3" }
There are three options available:

* ``global_prefix``: A prefix for all keys within the cache. This is useful to
avoid colliding keys when using the same cache for different systems.
* ``prefix``: A "local" prefix for this wrapper. This is useful when re-using the
same resolver for multiple filters. This mainly affects the clear method.
* ``index_key``: The name of the index key being used to save a list of created
cache keys regarding one image and filter pairing.


Usage
-----

After configuring ``PsrCacheResolver``, you can set it as the default cache resolver
for ``LiipImagineBundle`` using the following configuration.

.. code-block:: yaml
# app/config/config.yml
liip_imagine:
cache: cached_amazon_s3
Usage on a Specific Filter
~~~~~~~~~~~~~~~~~~~~~~~~~~

Alternatively, you can set ``CacheResolver`` as the cache resolver for a specific
filter set using the following configuration.

.. code-block:: yaml
# app/config/config.yml
liip_imagine:
filter_sets:
cache: ~
my_thumb:
cache: cached_amazon_s3
filters:
# the filter list
.. _`Doctrine Cache`: https://github.com/doctrine/cache
.. _`Composer`: https://getcomposer.org/
.. _`installation documentation`: https://getcomposer.org/doc/00-intro.md
.. _`Symfony documentation on configuring caching`: https://symfony.com/doc/current/cache.html#configuring-cache-with-frameworkbundle
Loading

0 comments on commit 45f09d8

Please sign in to comment.