Passed
Push — tmp_tooltest ( 3ffa99...652fa3 )
by Jonathan
01:38
created

SQLCache::getMultiple()   A

Complexity

Conditions 6
Paths 13

Size

Total Lines 27
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 17
c 1
b 0
f 0
dl 0
loc 27
ccs 17
cts 17
cp 1
rs 9.0777
cc 6
nc 13
nop 2
crap 6
1
<?php
2
3
namespace Vectorface\Cache;
4
5
use PDO;
6
use PDOStatement;
7
use PDOException;
8
use Vectorface\Cache\Common\PSR16Util;
9
10
/**
11
 * This cache is slow, according to basic benchmarks:
12
 *
13
 * Parameters:
14
 *   MySQL 5.0, running locally
15
 *   9-byte key
16
 *   151-byte value
17
 *   10000-iteration test
18
 *
19
 * Result:
20
 *   16.7824881077 seconds
21
 *
22
 * Conclusion:
23
 *   Capable of approximately 595.85 requests/second
24
 */
25
26
/**
27
 * A cache implementation that uses SQL for storage.
28
 *
29
 * An example table might look like:
30
 * CREATE TABLE cache (
31
 *     entry VARCHAR(64) PRIMARY KEY NOT NULL,
32
 *     value LONGBLOB,
33
 *     expires BIGINT UNSIGNED DEFAULT NULL,
34
 *     KEY expires (expires)
35
 * ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
36
 */
37
class SQLCache implements Cache
38
{
39
    use PSR16Util;
40
41
    /**
42
     * Hash keys beyond this size
43
     */
44
    const MAX_KEY_LEN = 64;
45
46
    /**
47
     * Statement for flushing all entries from the cache.
48
     */
49
    const FLUSH_SQL = 'DELETE FROM cache';
50
51
    /**
52
     * Statement for deleting expired entries from the cache.
53
     */
54
    const CLEAN_SQL = 'DELETE FROM cache WHERE expires<=UNIX_TIMESTAMP()';
55
56
    /**
57
     * Statement for inserting or updating entries in the cache.
58
     */
59
    const SET_SQL = 'INSERT INTO cache (entry,value,expires) VALUES(?,?,?)';
60
61
    /**
62
     * Statement for updating if an entry already exists.
63
     */
64
    const UPDATE_SQL = 'UPDATE cache SET value=?, expires=? WHERE entry=?';
65
66
    /**
67
     * Statement for checking if an entry exists
68
     */
69
    const HAS_SQL = 'SELECT COUNT(*) AS num FROM cache WHERE entry=? AND expires>=UNIX_TIMESTAMP()';
70
71
    /**
72
     * Statement for retrieving an entry from the cache
73
     */
74
    const GET_SQL = 'SELECT value FROM cache WHERE entry=? AND expires>=UNIX_TIMESTAMP()';
75
76
    /**
77
     * Statement for retrieving entries from the cache (no statement caching)
78
     */
79
    const MGET_SQL = 'SELECT entry,value FROM cache WHERE entry IN(%s) AND expires>=UNIX_TIMESTAMP()';
80
81
    /**
82
     * Statement for deleting an entry from the cache
83
     */
84
    const DELETE_SQL = 'DELETE FROM cache WHERE entry=?';
85
86
    /**
87
     * Statement for deleting entries from the cache (no statement caching)
88
     */
89
    const MDELETE_SQL = 'DELETE FROM cache WHERE entry IN(%s)';
90
91
    /**
92
     * The database connection to be used for cache operations.
93
     *
94
     * @var PDO
95
     */
96
    private $conn;
97
98
    /**
99
     * An associative array of PDO statements used in get/set.
100
     *
101
     * @var PDOStatement
102
     */
103
    private $statements = [];
104
105
    /**
106
     * Create an instance of the SQL cache.
107
     *
108
     * @param PDO $conn The database connection to use for cache operations.
109
     */
110 20
    public function __construct(PDO $conn)
111
    {
112 20
        $this->conn = $conn;
113 20
    }
114
115
    /**
116
     * @inheritDoc Vectorface\Cache\Cache
117
     */
118 16
    public function get($key, $default = null)
119
    {
120 16
        $key = $this->key($key);
121 15
        $key = $this->hashKey($key);
122
123
        try {
124 15
            $stmt = $this->getStatement(__METHOD__, self::GET_SQL);
125 15
            $stmt->execute([$key]);
126 1
        } catch (PDOException $e) {
127 1
            return $default;
128
        }
129 15
        $result = $stmt->fetchColumn();
130 15
        return empty($result) ? $default : unserialize($result);
131
    }
132
133
    /**
134
     * @inheritDoc
135
     */
136 4
    public function getMultiple($keys, $default = null)
137
    {
138 4
        if (empty($keys)) {
139 1
            return [];
140
        }
141
142 4
        $keys = $this->keys($keys);
143 3
        $sqlKeys = array_map([$this, 'hashKey'], is_array($keys) ? $keys : iterator_to_array($keys));
0 ignored issues
show
introduced by
The condition is_array($keys) is always true.
Loading history...
144
145
        try {
146 3
            $stmt = $this->conn->prepare(sprintf(
147 3
                self::MGET_SQL,
148 3
                implode(',', array_fill(0, count($keys), '?'))
149
            ));
150 2
            $stmt->execute($sqlKeys);
151 2
            $result = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
152 1
        } catch (PDOException $e) {
153 1
            $result = [];
154
        }
155
156 3
        $return = array_map('unserialize', $result);
157 3
        foreach ($keys as $key) {
158 3
            if (!isset($return[$key])) {
159 3
                $return[$key] = $default;
160
            }
161
        }
162 3
        return $return;
163
    }
164
165
166
    /**
167
     * @inheritDoc Vectorface\Cache\Cache
168
     */
169 19
    public function set($key, $value, $ttl = null)
170
    {
171 19
        $key = $this->key($key);
172 19
        $key = $this->hashKey($key);
173 19
        $ttl = $this->ttl($ttl);
174 18
        $ttl = $ttl ? ($ttl + time()) : PHP_INT_MAX;
175 18
        $value = serialize($value);
176
177
        try {
178 18
            $stmt = $this->getStatement(__METHOD__ . ".insert", self::SET_SQL);
179 18
            return $stmt->execute([$key, $value, $ttl]);
180 4
        } catch (PDOException $e) {
181
            // Insert can fail if the entry exists; It's normal.
182
        }
183
184
        try {
185 4
            $stmt = $this->getStatement(__METHOD__ . ".update", self::UPDATE_SQL);
186 4
            $success = $stmt->execute([$value, $ttl, $key]);
187 4
            return $success && $stmt->rowCount() === 1;
188 1
        } catch (PDOException $e) {
189 1
            return false;
190
        }
191
    }
192
193
    /**
194
     * @inheritDoc Psr\SimpleCache\CacheInterface
195
     */
196 3
    public function setMultiple($values, $ttl = null)
197
    {
198 3
        $success = true;
199 3
        foreach ($this->values($values) as $key => $value) {
200 2
            $success = $this->set($key, $value, $ttl) && $success;
201
        }
202 2
        return $success;
203
    }
204
205
    /**
206
     * @inheritDoc Vectorface\Cache\Cache
207
     */
208 5
    public function delete($key)
209
    {
210 5
        $key = $this->hashKey($this->key($key));
211
212
        try {
213 5
            $stmt = $this->getStatement(__METHOD__, self::DELETE_SQL);
214 5
            return $stmt->execute([$key]);
215 1
        } catch (PDOException $e) {
216 1
            return false;
217
        }
218
    }
219
220
    /**
221
     * @inheritDoc
222
     */
223 3
    public function deleteMultiple($keys)
224
    {
225 3
        if (empty($keys)) {
226 1
            return true;
227
        }
228
229 3
        $keysArray = array_map([$this, 'hashKey'], is_array($keys) ? $keys : iterator_to_array($keys));
230
231
        try {
232 3
            $stmt = $this->conn->prepare(sprintf(
233 3
                self::MDELETE_SQL,
234 3
                implode(',', array_fill(0, count($keysArray), '?'))
235
            ));
236 2
            $stmt->execute($keysArray);
237 1
        } catch (PDOException $e) {
238 1
            return false;
239
        }
240 2
        return true;
241
    }
242
243
    /**
244
     * @inheritdoc vectorface\cache\cache
245
     */
246 2
    public function clean()
247
    {
248
        try {
249 2
            $this->conn->exec(self::CLEAN_SQL);
250 1
        } catch (PDOException $e) {
251 1
            return false;
252
        }
253 2
        return true;
254
    }
255
256
    /**
257
     * @inheritdoc vectorface\cache\cache
258
     */
259 7
    public function flush()
260
    {
261
        try {
262 7
            $this->conn->exec(self::FLUSH_SQL);
263 1
        } catch (PDOException $e) {
264 1
            return false;
265
        }
266 7
        return true;
267
    }
268
269
    /**
270
     * @inheritDoc Psr\SimpleCache\CacheInterface
271
     */
272 3
    public function clear()
273
    {
274 3
        return $this->flush();
275
    }
276
277
    /**
278
     * @inheritDoc Psr\SimpleCache\CacheInterface
279
     */
280 2
    public function has($key)
281
    {
282 2
        $key = $this->hashKey($key);
283
284
        try {
285 2
            $stmt = $this->getStatement(__METHOD__, self::HAS_SQL);
286 1
            $stmt->execute([$key]);
287 1
        } catch (PDOException $e) {
288 1
            return false;
289
        }
290 1
        return $stmt->fetchColumn() ? true : false;
291
    }
292
293
    /**
294
     * Get a prepared statement for the given method's SQL.
295
     *
296
     * The result is stored internally to limit repeated preparing of SQL.
297
     *
298
     * @param string $method The method name to for which this statement applies.
299
     * @param string $sql The SQL statement associated with the given method.
300
     * @return PDOStatement Returns the prepared statement for the given method.
301
     */
302 18
    private function getStatement($method, $sql)
303
    {
304 18
        if (empty($this->statements[$method])) {
305 18
            $this->statements[$method] = $this->conn->prepare($sql);
306
        }
307 18
        return $this->statements[$method];
308
    }
309
310
    /**
311
     * Get a unique hash key when the key is too long
312
     *
313
     * @param string $key
314
     * @return string The key, or the hash of the key parameter if it goes beyond maximum length
315
     * @private Public for testing
316
     */
317 19
    public static function hashKey($key)
318
    {
319 19
        return (strlen($key) > self::MAX_KEY_LEN) ? hash('sha256', $key) : $key;
320
    }
321
}
322