Completed
Pull Request — 15.x (#165)
by Jitendra
02:17
created

GenericCacheAdapter::toCache()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 39
ccs 0
cts 26
cp 0
rs 8.6737
c 0
b 0
f 0
cc 6
nc 7
nop 6
crap 42
1
<?php
2
3
/**
4
 * TechDivision\Import\Cache\GenericCacheAdapter
5
 *
6
 * NOTICE OF LICENSE
7
 *
8
 * This source file is subject to the Open Software License (OSL 3.0)
9
 * that is available through the world-wide-web at this URL:
10
 * http://opensource.org/licenses/osl-3.0.php
11
 *
12
 * PHP version 5
13
 *
14
 * @author    Tim Wagner <[email protected]>
15
 * @copyright 2019 TechDivision GmbH <[email protected]>
16
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
17
 * @link      https://github.com/techdivision/import
18
 * @link      http://www.techdivision.com
19
 */
20
21
namespace TechDivision\Import\Cache;
22
23
use Cache\TagInterop\TaggableCacheItemInterface;
24
use Psr\Cache\CacheItemPoolInterface;
25
use Symfony\Contracts\Cache\ItemInterface;
26
use TechDivision\Import\Utils\CacheKeys;
27
use TechDivision\Import\Utils\CacheKeyUtilInterface;
28
29
/**
30
 * Generic cache adapter that wrappes any PSR-6 compatible cache implementation and can be
31
 * used in a distributed environment.
32
 *
33
 * If you're searching for a maximum performance consider using the LocalCacheAdapter
34
 * implementation.
35
 *
36
 * ATTENTION: Please be aware, that this cache adapter is NOT multiprocess or -threadsafe!
37
 *
38
 * @author    Tim Wagner <[email protected]>
39
 * @copyright 2019 TechDivision GmbH <[email protected]>
40
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
41
 * @link      https://github.com/techdivision/import
42
 * @link      http://www.techdivision.com
43
 * @see       \TechDivision\Import\Cache\LocalCacheAdapter
44
 */
45
class GenericCacheAdapter implements CacheAdapterInterface
46
{
47
48
    /**
49
     * Trait that provides custom cache adapter functionality.
50
     *
51
     * @var TechDivision\Import\Cache\CacheAdapterTrait
52
     */
53
    use CacheAdapterTrait;
54
55
    /**
56
     * The cache for the query results.
57
     *
58
     * @var \Psr\Cache\CacheItemPoolInterface
59
     */
60
    protected $cache;
61
62
    /**
63
     * The cache key utility instance.
64
     *
65
     * @var \TechDivision\Import\Utils\CacheKeyUtilInterface
66
     */
67
    protected $cacheKeyUtil;
68
69
    /**
70
     * Initialize the cache handler with the passed cache and configuration instances.
71
     * .
72
     * @param \Psr\Cache\CacheItemPoolInterface                $cache        The cache instance
73
     * @param \TechDivision\Import\Utils\CacheKeyUtilInterface $cacheKeyUtil The cache key utility instance
74
     */
75
    public function __construct(
76
        CacheItemPoolInterface $cache,
77
        CacheKeyUtilInterface $cacheKeyUtil
78
    ) {
79
80
        // set the cache and the cache key utility instance
81
        $this->cache = $cache;
82
        $this->cacheKeyUtil = $cacheKeyUtil;
83
    }
84
85
    /**
86
     * Resolve's the cache key.
87
     *
88
     * @param string $from The cache key to resolve
89
     *
90
     * @return string The resolved reference
91
     */
92
    protected function resolveReference($from)
93
    {
94
95
        // try to load the load the references
96
        if ($this->cache->hasItem(CacheKeys::REFERENCES)) {
97
            // load the array with references from the cache
98
            $references = $this->cache->getItem(CacheKeys::REFERENCES)->get();
99
100
            // query whether a reference is available
101
            if (isset($references[$from])) {
102
                return $references[$from];
103
            }
104
        }
105
106
        // return the passed reference
107
        return $from;
108
    }
109
110
    /**
111
     * Creates a unique cache key from the passed data.
112
     *
113
     * @param mixed   $data      The date to create the cache key from
114
     * @param boolean $usePrefix Flag to signal using the prefix or not
115
     *
116
     * @return string The generated cache key
117
     */
118
    public function cacheKey($data, $usePrefix = true)
119
    {
120
        return $this->cacheKeyUtil->cacheKey($data, $usePrefix);
121
    }
122
123
    /**
124
     * Query whether or not a cache value for the passed cache key is available.
125
     *
126
     * @param string $key The cache key to query for
127
     *
128
     * @return boolean TRUE if the a value is available, else FALSE
129
     */
130
    public function isCached($key)
131
    {
132
133
        // query whether or not the item has been cached, and if yes if the cache is valid
134
        if ($this->cache->hasItem($resolvedKey = $this->resolveReference($this->cacheKey($key)))) {
135
            return $this->cache->getItem($resolvedKey)->isHit();
136
        }
137
138
        // return FALSE in all other cases
139
        return false;
140
    }
141
142
    /**
143
     * Inversion of the isCached() method.
144
     *
145
     * @param string $key The cache key to query for
146
     *
147
     * @return boolean TRUE if the value is not available, else FALSE
148
     */
149
    public function notCached($key)
150
    {
151
        return !$this->isCached($key);
152
    }
153
154
    /**
155
     * Add's a cache reference from one key to another.
156
     *
157
     * @param string $from The key to reference from
158
     * @param string $to   The key to reference to
159
     *
160
     * @return void
161
     */
162
    public function addReference($from, $to)
163
    {
164
165
        // initialize the array with references
166
        $references = array();
167
168
        // try to load the references from the cache
169
        if ($this->isCached(CacheKeys::REFERENCES)) {
170
            $references = $this->fromCache(CacheKeys::REFERENCES);
171
        }
172
173
        // add the reference to the array
174
        $references[$this->cacheKey($from)] = $this->cacheKey($to);
175
176
        // add the references back to the cache
177
        $this->toCache(CacheKeys::REFERENCES, $references);
178
    }
179
180
    /**
181
     * Add the passed item to the cache.
182
     *
183
     * @param string  $key        The cache key to use
184
     * @param mixed   $value      The value that has to be cached
185
     * @param array   $references An array with references to add
186
     * @param array   $tags       An array with tags to add
187
     * @param boolean $override   Flag that allows to override an exising cache entry
188
     * @param integer $time       The TTL in seconds for the passed item
189
     *
190
     * @return void
191
     */
192
    public function toCache($key, $value, array $references = array(), array $tags = array(), $override = true, $time = null)
193
    {
194
195
        // create the unique cache key
196
        $uniqueKey = $this->cacheKey($key);
197
198
        // query whether or not the key has already been used
199
        if ($this->isCached($uniqueKey) && $override === false) {
200
            throw new \Exception(
201
                sprintf(
202
                    'Try to override data with key "%s"',
203
                    $uniqueKey
204
                )
205
            );
206
        }
207
208
        // prepend the tags with the cache key
209
        array_walk($tags, function (&$tag) {
210
            $tag = $this->cacheKey($tag);
211
        });
212
213
        // initialize the cache item
214
        $cacheItem = $this->cache->getItem($uniqueKey);
215
        $cacheItem->set($value)->expiresAfter($time);
216
        
217
        if ($cacheItem instanceof TaggableCacheItemInterface) {
218
            $cacheItem->setTags($tags);
219
        } elseif ($cacheItem instanceof ItemInterface) {
0 ignored issues
show
Bug introduced by
The class Symfony\Contracts\Cache\ItemInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
220
            $cacheItem->tag($tags);
221
        }
222
223
        // set the attribute in the registry
224
        $this->cache->save($cacheItem);
225
226
        // also register the references if given
227
        foreach ($references as $from => $to) {
228
            $this->addReference($from, $to);
229
        }
230
    }
231
232
    /**
233
     * Returns a new cache item for the passed key
234
     *
235
     * @param string $key The cache key to return the item for
236
     *
237
     * @return mixed The value for the passed key
238
     */
239
    public function fromCache($key)
240
    {
241
        return $this->cache->getItem($this->resolveReference($this->cacheKey($key)))->get();
242
    }
243
244
    /**
245
     * Flush the cache and remove the references.
246
     *
247
     * @return void
248
     */
249
    public function flushCache()
250
    {
251
        $this->cache->clear();
252
    }
253
254
    /**
255
     * Invalidate the cache entries for the passed tags.
256
     *
257
     * @param array $tags The tags to invalidate the cache for
258
     *
259
     * @return void
260
     */
261
    public function invalidateTags(array $tags)
262
    {
263
264
        // prepend the tags with the cache key
265
        array_walk($tags, function (&$tag) {
266
            $tag = $this->cacheKey($tag);
267
        });
268
269
        // query whether or not references are available
270
        if ($this->isCached(CacheKeys::REFERENCES)) {
271
            // load the array with references from the cache
272
            $references = $this->fromCache(CacheKeys::REFERENCES);
273
274
            // remove all the references of items that has one of the passed tags
275
            foreach ($tags as $tag) {
276
                foreach ($references as $from => $to) {
277
                    // load the cache item for the referenced key
278
                    $cacheItem = $this->cache->getItem($to);
279
                    // query whether or not the cache item has the tag, if yes remove the reference
280
                    if (in_array($tag, $cacheItem->getPreviousTags())) {
281
                        unset($references[$from]);
282
                    }
283
                }
284
            }
285
286
            // query whether or not the references exists
287
            if (sizeof($references) > 0) {
288
                // set the array with references to the cache
289
                $this->toCache(CacheKeys::REFERENCES, $references);
290
            } else {
291
                $this->removeCache(CacheKeys::REFERENCES);
292
            }
293
        }
294
295
        // finally, invalidate the items with the passed tags
296
        $this->cache->invalidateTags($tags);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Psr\Cache\CacheItemPoolInterface as the method invalidateTags() does only exist in the following implementations of said interface: Cache\Adapter\Common\AbstractCachePool, Cache\Adapter\PHPArray\ArrayCachePool.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
297
    }
298
299
    /**
300
     * Remove the item with the passed key and all its references from the cache.
301
     *
302
     * @param string $key The key of the cache item to Remove
303
     *
304
     * @return void
305
     */
306
    public function removeCache($key)
307
    {
308
309
        // delete the item with the passed key
310
        $this->cache->deleteItem($this->resolveReference($uniqueKey = $this->cacheKey($key)));
311
312
        // query whether or not references are available
313
        if ($this->isCached(CacheKeys::REFERENCES)) {
314
            // load the array with references from the cache
315
            $references = $this->fromCache(CacheKeys::REFERENCES);
316
            // query whether or not the references exists
317
            if (isset($references[$uniqueKey])) {
318
                // remove the reference
319
                unset($references[$uniqueKey]);
320
                // set the array with references to the cache
321
                $this->toCache(CacheKeys::REFERENCES, $references);
322
            }
323
        }
324
    }
325
}
326