CacheResolver::isStored()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 8
rs 10
cc 2
nc 2
nop 2
1
<?php
2
3
/*
4
 * This file is part of the `liip/LiipImagineBundle` project.
5
 *
6
 * (c) https://github.com/liip/LiipImagineBundle/graphs/contributors
7
 *
8
 * For the full copyright and license information, please view the LICENSE.md
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Liip\ImagineBundle\Imagine\Cache\Resolver;
13
14
use Doctrine\Common\Cache\Cache;
15
use Liip\ImagineBundle\Binary\BinaryInterface;
16
use Symfony\Component\OptionsResolver\OptionsResolver;
17
18
class CacheResolver implements ResolverInterface
19
{
20
    /**
21
     * @var Cache
22
     */
23
    protected $cache;
24
25
    /**
26
     * @var array
27
     */
28
    protected $options = [];
29
30
    /**
31
     * @var ResolverInterface
32
     */
33
    protected $resolver;
34
35
    /**
36
     * Constructor.
37
     *
38
     * Available options:
39
     * * global_prefix
40
     *   A prefix for all keys within the cache. This is useful to avoid colliding keys when using the same cache for different systems.
41
     * * prefix
42
     *   A "local" prefix for this wrapper. This is useful when re-using the same resolver for multiple filters.
43
     * * index_key
44
     *   The name of the index key being used to save a list of created cache keys regarding one image and filter pairing.
45
     *
46
     * @param OptionsResolver $optionsResolver
47
     */
48
    public function __construct(Cache $cache, ResolverInterface $cacheResolver, array $options = [], OptionsResolver $optionsResolver = null)
49
    {
50
        $this->cache = $cache;
51
        $this->resolver = $cacheResolver;
52
53
        if (null === $optionsResolver) {
54
            $optionsResolver = new OptionsResolver();
55
        }
56
57
        $this->configureOptions($optionsResolver);
58
        $this->options = $optionsResolver->resolve($options);
59
    }
60
61
    /**
62
     * {@inheritdoc}
63
     */
64
    public function isStored($path, $filter)
65
    {
66
        $cacheKey = $this->generateCacheKey($path, $filter);
67
68
        return
69
            $this->cache->contains($cacheKey) ||
70
            $this->resolver->isStored($path, $filter);
71
    }
72
73
    /**
74
     * {@inheritdoc}
75
     */
76
    public function resolve($path, $filter)
77
    {
78
        $key = $this->generateCacheKey($path, $filter);
79
        if ($this->cache->contains($key)) {
80
            return $this->cache->fetch($key);
81
        }
82
83
        $resolved = $this->resolver->resolve($path, $filter);
84
85
        $this->saveToCache($key, $resolved);
86
87
        return $resolved;
88
    }
89
90
    /**
91
     * {@inheritdoc}
92
     */
93
    public function store(BinaryInterface $binary, $path, $filter)
94
    {
95
        $this->resolver->store($binary, $path, $filter);
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     */
101
    public function remove(array $paths, array $filters)
102
    {
103
        $this->resolver->remove($paths, $filters);
104
105
        foreach ($filters as $filter) {
106
            if (empty($paths)) {
107
                $this->removePathAndFilter(null, $filter);
108
            } else {
109
                foreach ($paths as $path) {
110
                    $this->removePathAndFilter($path, $filter);
111
                }
112
            }
113
        }
114
    }
115
116
    /**
117
     * Generate a unique cache key based on the given parameters.
118
     *
119
     * When overriding this method, ensure generateIndexKey is adjusted accordingly.
120
     *
121
     * @param string $path   The image path in use
122
     * @param string $filter The filter in use
123
     *
124
     * @return string
125
     */
126
    public function generateCacheKey($path, $filter)
127
    {
128
        return implode('.', [
129
            $this->sanitizeCacheKeyPart($this->options['global_prefix']),
130
            $this->sanitizeCacheKeyPart($this->options['prefix']),
131
            $this->sanitizeCacheKeyPart($filter),
132
            $this->sanitizeCacheKeyPart($path),
133
        ]);
134
    }
135
136
    protected function removePathAndFilter($path, $filter)
137
    {
138
        $indexKey = $this->generateIndexKey($this->generateCacheKey($path, $filter));
139
        if (!$this->cache->contains($indexKey)) {
140
            return;
141
        }
142
143
        $index = $this->cache->fetch($indexKey);
144
145
        if (null === $path) {
146
            foreach ($index as $eachCacheKey) {
147
                $this->cache->delete($eachCacheKey);
148
            }
149
150
            $index = [];
151
        } else {
152
            $cacheKey = $this->generateCacheKey($path, $filter);
153
            if (false !== $indexIndex = array_search($cacheKey, $index, true)) {
154
                unset($index[$indexIndex]);
155
                $this->cache->delete($cacheKey);
156
            }
157
        }
158
159
        if (empty($index)) {
160
            $this->cache->delete($indexKey);
161
        } else {
162
            $this->cache->save($indexKey, $index);
163
        }
164
    }
165
166
    /**
167
     * Generate the index key for the given cacheKey.
168
     *
169
     * The index contains a list of cache keys related to an image and a filter.
170
     *
171
     * @param string $cacheKey
172
     *
173
     * @return string
174
     */
175
    protected function generateIndexKey($cacheKey)
176
    {
177
        $cacheKeyStack = explode('.', $cacheKey);
178
179
        return implode('.', [
180
            $this->sanitizeCacheKeyPart($this->options['global_prefix']),
181
            $this->sanitizeCacheKeyPart($this->options['prefix']),
182
            $this->sanitizeCacheKeyPart($this->options['index_key']),
183
            $this->sanitizeCacheKeyPart($cacheKeyStack[2]), // filter
184
        ]);
185
    }
186
187
    /**
188
     * @param string $cacheKeyPart
189
     *
190
     * @return string
191
     */
192
    protected function sanitizeCacheKeyPart($cacheKeyPart)
193
    {
194
        return str_replace('.', '_', $cacheKeyPart);
195
    }
196
197
    /**
198
     * Save the given content to the cache and update the cache index.
199
     *
200
     * @param string $cacheKey
201
     * @param mixed  $content
202
     *
203
     * @return bool
204
     */
205
    protected function saveToCache($cacheKey, $content)
206
    {
207
        // Create or update the index list containing all cache keys for a given image and filter pairing.
208
        $indexKey = $this->generateIndexKey($cacheKey);
209
        if ($this->cache->contains($indexKey)) {
210
            $index = (array) $this->cache->fetch($indexKey);
211
212
            if (!\in_array($cacheKey, $index, true)) {
213
                $index[] = $cacheKey;
214
            }
215
        } else {
216
            $index = [$cacheKey];
217
        }
218
219
        /*
220
         * Only save the content, if the index has been updated successfully.
221
         * This is required to have a (hopefully) synchron state between cache and backend.
222
         *
223
         * "Hopefully" because there are caches (like Memcache) which will remove keys by themselves.
224
         */
225
        if ($this->cache->save($indexKey, $index)) {
226
            return $this->cache->save($cacheKey, $content);
227
        }
228
229
        return false;
230
    }
231
232
    protected function configureOptions(OptionsResolver $resolver)
233
    {
234
        $resolver->setDefaults([
235
            'global_prefix' => 'liip_imagine.resolver_cache',
236
            'prefix' => \get_class($this->resolver),
237
            'index_key' => 'index',
238
        ]);
239
240
        $allowedTypesList = [
241
          'global_prefix' => 'string',
242
          'prefix' => 'string',
243
          'index_key' => 'string',
244
        ];
245
246
        foreach ($allowedTypesList as $option => $allowedTypes) {
247
            $resolver->setAllowedTypes($option, $allowedTypes);
248
        }
249
    }
250
251
    protected function setDefaultOptions(OptionsResolver $resolver)
252
    {
253
        $this->configureOptions($resolver);
254
    }
255
}
256