Issues (70)

src/Psr6/Pool.php (1 issue)

Severity
1
<?php
2
3
namespace MatthiasMullie\Scrapbook\Psr6;
4
5
use MatthiasMullie\Scrapbook\KeyValueStore;
6
use Psr\Cache\CacheItemInterface;
7
use Psr\Cache\CacheItemPoolInterface;
8
9
/**
10
 * Representation of the cache storage, which lets you read items from, and add
11
 * values to the cache.
12
 *
13
 * @author Matthias Mullie <[email protected]>
14
 * @copyright Copyright (c) 2014, Matthias Mullie. All rights reserved
15
 * @license LICENSE MIT
16
 */
17
class Pool implements CacheItemPoolInterface
18
{
19
    /**
20
     * List of invalid (or reserved) key characters.
21
     *
22
     * @var string
23
     */
24
    /* public */ const KEY_INVALID_CHARACTERS = '{}()/\@:';
25
26
    /**
27
     * @var KeyValueStore
28
     */
29
    protected $store;
30
31
    /**
32
     * @var Repository
33
     */
34
    protected $repository;
35
36
    /**
37
     * @var Item[]
38
     */
39
    protected $deferred = array();
40
41
    public function __construct(KeyValueStore $store)
42
    {
43
        $this->store = $store;
44
        $this->repository = new Repository($store);
45
    }
46
47
    public function __destruct()
48
    {
49
        // make sure all deferred items are actually saved
50
        $this->commit();
51
    }
52
53
    /**
54
     * {@inheritdoc}
55
     */
56
    public function getItem($key)
57
    {
58
        $this->assertValidKey($key);
59
        if (array_key_exists($key, $this->deferred)) {
60
            /*
61
             * In theory, we could request & change a deferred value. In the
62
             * case of objects, we'll clone them to make sure that when the
63
             * value for 1 item is manipulated, it doesn't affect the value of
64
             * the one about to be stored to cache (because those objects would
65
             * be passed by-ref without the cloning)
66
             */
67
            $value = $this->deferred[$key];
68
            $item = is_object($value) ? clone $value : $value;
69
70
            /*
71
             * Deferred items should identify as being hit, unless if expired:
72
             * @see https://groups.google.com/forum/?fromgroups#!topic/php-fig/pxy_VYgm2sU
73
             */
74
            $item->overrideIsHit(!$item->isExpired());
75
76
            return $item;
77
        }
78
79
        // return a stub object - the real call to the cache store will only be
80
        // done once we actually want to access data from this object
81
        return new Item($key, $this->repository);
82
    }
83
84
    /**
85
     * {@inheritdoc}
86
     *
87
     * @return Item[]
88
     */
89
    public function getItems(array $keys = array())
90
    {
91
        $items = array();
92
        foreach ($keys as $key) {
93
            $this->assertValidKey($key);
94
95
            $items[$key] = $this->getItem($key);
96
        }
97
98
        return $items;
99
    }
100
101
    /**
102
     * {@inheritdoc}
103
     */
104
    public function hasItem($key)
105
    {
106
        $this->assertValidKey($key);
107
108
        $item = $this->getItem($key);
109
110
        return $item->isHit();
111
    }
112
113
    /**
114
     * {@inheritdoc}
115
     */
116
    public function clear()
117
    {
118
        $this->deferred = array();
119
120
        return $this->store->flush();
121
    }
122
123
    /**
124
     * {@inheritdoc}
125
     */
126
    public function deleteItem($key)
127
    {
128
        $this->assertValidKey($key);
129
130
        $this->store->delete($key);
131
        unset($this->deferred[$key]);
132
133
        // as long as the item is gone from the cache (even if it never existed
134
        // and delete failed because of that), we should return `true`
135
        return true;
136
    }
137
138
    /**
139
     * {@inheritdoc}
140
     */
141
    public function deleteItems(array $keys)
142
    {
143
        foreach ($keys as $key) {
144
            $this->assertValidKey($key);
145
146
            unset($this->deferred[$key]);
147
        }
148
149
        $this->store->deleteMulti($keys);
150
151
        // as long as the item is gone from the cache (even if it never existed
152
        // and delete failed because of that), we should return `true`
153
        return true;
154
    }
155
156
    /**
157
     * {@inheritdoc}
158
     */
159
    public function save(CacheItemInterface $item)
160
    {
161
        if (!$item instanceof Item) {
162
            throw new InvalidArgumentException('MatthiasMullie\Scrapbook\Psr6\Pool can only save
163
                MatthiasMullie\Scrapbook\Psr6\Item objects');
164
        }
165
166
        if (!$item->hasChanged()) {
167
            /*
168
             * If the item didn't change, we don't have to re-save it. We do,
169
             * however, need to check if the item actually holds a value: if it
170
             * does, it should be considered "saved" (even though nothing has
171
             * changed, the value for this key is in cache) and if it doesn't,
172
             * well then nothing is in cache.
173
             */
174
            return null !== $item->get();
175
        }
176
177
        $expire = $item->getExpiration();
178
179
        return $this->store->set($item->getKey(), $item->get(), $expire);
180
    }
181
182
    /**
183
     * {@inheritdoc}
184
     */
185
    public function saveDeferred(CacheItemInterface $item)
186
    {
187
        if (!$item instanceof Item) {
188
            throw new InvalidArgumentException('MatthiasMullie\Scrapbook\Psr6\Pool can only save
189
                MatthiasMullie\Scrapbook\Psr6\Item objects');
190
        }
191
192
        $this->deferred[$item->getKey()] = $item;
193
        // let's pretend that this actually comes from cache (we'll store it
194
        // there soon), unless if it's already expired (in which case it will
195
        // never reach cache...)
196
        $item->overrideIsHit(!$item->isExpired());
197
198
        return true;
199
    }
200
201
    /**
202
     * {@inheritdoc}
203
     */
204
    public function commit()
205
    {
206
        $deferred = array();
207
        foreach ($this->deferred as $key => $item) {
208
            if ($item->isExpired()) {
209
                // already expired: don't even save it
210
                continue;
211
            }
212
213
            // setMulti doesn't allow to set expiration times on a per-item basis,
214
            // so we'll have to group our requests per expiration date
215
            $expire = $item->getExpiration();
216
            $deferred[$expire][$item->getKey()] = $item->get();
217
        }
218
219
        // setMulti doesn't allow to set expiration times on a per-item basis,
220
        // so we'll have to group our requests per expiration date
221
        $success = true;
222
        foreach ($deferred as $expire => $items) {
223
            $status = $this->store->setMulti($items, $expire);
224
            $success &= !in_array(false, $status);
225
            unset($deferred[$expire]);
226
        }
227
228
        return (bool) $success;
229
    }
230
231
    /**
232
     * Throws an exception if $key is invalid.
233
     *
234
     * @param string $key
235
     *
236
     * @throws InvalidArgumentException
237
     */
238
    protected function assertValidKey($key)
239
    {
240
        if (!is_string($key)) {
0 ignored issues
show
The condition is_string($key) is always true.
Loading history...
241
            throw new InvalidArgumentException('Invalid key: '.var_export($key, true).'. Key should be a string.');
242
        }
243
244
        // valid key according to PSR-6 rules
245
        $invalid = preg_quote(static::KEY_INVALID_CHARACTERS, '/');
246
        if (preg_match('/['.$invalid.']/', $key)) {
247
            throw new InvalidArgumentException('Invalid key: '.$key.'. Contains (a) character(s) reserved for future extension: '.static::KEY_INVALID_CHARACTERS);
248
        }
249
    }
250
}
251