Completed
Pull Request — 2.x (#1375)
by Andreas
16:06
created

PsrCacheResolver::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
cc 2
nc 2
nop 4
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 Liip\ImagineBundle\Binary\BinaryInterface;
15
use Psr\Cache\CacheItemInterface;
16
use Psr\Cache\CacheItemPoolInterface;
17
use Symfony\Component\OptionsResolver\OptionsResolver;
18
use function str_replace;
19
20
class PsrCacheResolver implements ResolverInterface
21
{
22
    private const RESERVED_CHARACTERS = [
23
        '{',
24
        '}',
25
        '(',
26
        ')',
27
        '/',
28
        '\\',
29
        '@',
30
        ':',
31
        '.',
32
    ];
33
34
    /**
35
     * @var CacheItemPoolInterface
36
     */
37
    protected $cache;
38
39
    /**
40
     * @var array
41
     */
42
    protected $options = [];
43
44
    /**
45
     * @var ResolverInterface
46
     */
47
    protected $resolver;
48
49
    /**
50
     * Constructor.
51
     *
52
     * Available options:
53
     * * global_prefix
54
     *   A prefix for all keys within the cache. This is useful to avoid colliding keys when using the same cache for different systems.
55
     * * prefix
56
     *   A "local" prefix for this wrapper. This is useful when re-using the same resolver for multiple filters.
57
     * * index_key
58
     *   The name of the index key being used to save a list of created cache keys regarding one image and filter pairing.
59
     *
60
     * @param OptionsResolver $optionsResolver
61
     */
62
    public function __construct(CacheItemPoolInterface $cache, ResolverInterface $cacheResolver, array $options = [], OptionsResolver $optionsResolver = null)
63
    {
64
        $this->cache = $cache;
65
        $this->resolver = $cacheResolver;
66
67
        if (null === $optionsResolver) {
68
            $optionsResolver = new OptionsResolver();
69
        }
70
71
        $this->configureOptions($optionsResolver);
72
        $this->options = $optionsResolver->resolve($options);
73
    }
74
75
    /**
76
     * {@inheritdoc}
77
     */
78
    public function isStored($path, $filter)
79
    {
80
        $cacheKey = $this->generateCacheKey($path, $filter);
81
82
        return
83
            $this->cache->hasItem($cacheKey) ||
84
            $this->resolver->isStored($path, $filter);
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90
    public function resolve($path, $filter)
91
    {
92
        $key = $this->generateCacheKey($path, $filter);
93
        $item = $this->cache->getItem($key);
94
        if ($item->isHit()) {
95
            return $item->get();
96
        }
97
98
        $resolved = $this->resolver->resolve($path, $filter);
99
100
        $item->set($resolved);
101
        $this->saveToCache($item);
102
103
        return $resolved;
104
    }
105
106
    /**
107
     * {@inheritdoc}
108
     */
109
    public function store(BinaryInterface $binary, $path, $filter)
110
    {
111
        $this->resolver->store($binary, $path, $filter);
112
    }
113
114
    /**
115
     * {@inheritdoc}
116
     */
117
    public function remove(array $paths, array $filters)
118
    {
119
        $this->resolver->remove($paths, $filters);
120
121
        foreach ($filters as $filter) {
122
            if (empty($paths)) {
123
                $this->removePathAndFilter(null, $filter);
124
            } else {
125
                foreach ($paths as $path) {
126
                    $this->removePathAndFilter($path, $filter);
127
                }
128
            }
129
        }
130
    }
131
132
    /**
133
     * Generate a unique cache key based on the given parameters.
134
     *
135
     * When overriding this method, ensure generateIndexKey is adjusted accordingly.
136
     *
137
     * @param string $path   The image path in use
138
     * @param string $filter The filter in use
139
     *
140
     * @return string
141
     */
142
    public function generateCacheKey($path, $filter)
143
    {
144
        return implode('.', [
145
            $this->sanitizeCacheKeyPart($this->options['global_prefix']),
146
            $this->sanitizeCacheKeyPart($this->options['prefix']),
147
            $this->sanitizeCacheKeyPart($filter),
148
            $this->sanitizeCacheKeyPart($path),
149
        ]);
150
    }
151
152
    protected function removePathAndFilter($path, $filter)
153
    {
154
        $indexKey = $this->generateIndexKey($this->generateCacheKey($path, $filter));
155
        $indexItem = $this->cache->getItem($indexKey);
156
        if (!$indexItem->isHit()) {
157
            return;
158
        }
159
160
        $index = $indexItem->get();
161
162
        if (null === $path) {
163
            foreach ($index as $eachCacheKey) {
164
                $this->cache->deleteItem($eachCacheKey);
165
            }
166
167
            $index = [];
168
        } else {
169
            $cacheKey = $this->generateCacheKey($path, $filter);
170
            if (false !== $indexIndex = array_search($cacheKey, $index, true)) {
171
                unset($index[$indexIndex]);
172
                $this->cache->deleteItem($cacheKey);
173
            }
174
        }
175
176
        if (empty($index)) {
177
            $this->cache->deleteItem($indexKey);
178
        } else {
179
            $indexItem->set($index);
180
            $this->cache->save($indexItem);
181
        }
182
    }
183
184
    /**
185
     * Generate the index key for the given cacheKey.
186
     *
187
     * The index contains a list of cache keys related to an image and a filter.
188
     *
189
     * @param string $cacheKey
190
     *
191
     * @return string
192
     */
193
    protected function generateIndexKey($cacheKey)
194
    {
195
        $cacheKeyStack = explode('.', $cacheKey);
196
197
        return implode('.', [
198
            $this->sanitizeCacheKeyPart($this->options['global_prefix']),
199
            $this->sanitizeCacheKeyPart($this->options['prefix']),
200
            $this->sanitizeCacheKeyPart($this->options['index_key']),
201
            $this->sanitizeCacheKeyPart($cacheKeyStack[2]), // filter
202
        ]);
203
    }
204
205
    /**
206
     * @param string $cacheKeyPart
207
     *
208
     * @return string
209
     */
210
    protected function sanitizeCacheKeyPart($cacheKeyPart)
211
    {
212
        return str_replace(self::RESERVED_CHARACTERS, '_', $cacheKeyPart);
213
    }
214
215
    /**
216
     * Save the given content to the cache and update the cache index.
217
     *
218
     * @return bool
219
     */
220
    protected function saveToCache(CacheItemInterface $item)
221
    {
222
        $cacheKey = $item->getKey();
223
224
        // Create or update the index list containing all cache keys for a given image and filter pairing.
225
        $indexKey = $this->generateIndexKey($cacheKey);
226
        $indexItem = $this->cache->getItem($indexKey);
227
        if ($indexItem->isHit()) {
228
            $index = (array) $indexItem->get();
229
230
            if (!\in_array($cacheKey, $index, true)) {
231
                $index[] = $cacheKey;
232
            }
233
        } else {
234
            $index = [$cacheKey];
235
        }
236
237
        $indexItem->set($index);
238
        $this->cache->saveDeferred($indexItem);
239
        $this->cache->saveDeferred($item);
240
241
        return $this->cache->commit();
242
    }
243
244
    protected function configureOptions(OptionsResolver $resolver)
245
    {
246
        $resolver->setDefaults([
247
            'global_prefix' => 'liip_imagine.resolver_psr_cache',
248
            'prefix' => \get_class($this->resolver),
249
            'index_key' => 'index',
250
        ]);
251
252
        $allowedTypesList = [
253
          'global_prefix' => 'string',
254
          'prefix' => 'string',
255
          'index_key' => 'string',
256
        ];
257
258
        foreach ($allowedTypesList as $option => $allowedTypes) {
259
            $resolver->setAllowedTypes($option, $allowedTypes);
260
        }
261
    }
262
263
    protected function setDefaultOptions(OptionsResolver $resolver)
264
    {
265
        $this->configureOptions($resolver);
266
    }
267
}
268