Passed
Push — master ( 159f4a...679316 )
by
unknown
02:11
created

SQLCache::deleteMultiple()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 11
c 0
b 0
f 0
dl 0
loc 18
ccs 11
cts 11
cp 1
rs 9.9
cc 4
nc 4
nop 1
crap 4
1
<?php
2
/** @noinspection SqlResolve */
3
/** @noinspection SqlWithoutWhere */
4
/** @noinspection SqlNoDataSourceInspection */
5
/** @noinspection PhpComposerExtensionStubsInspection */
6
7
namespace Vectorface\Cache;
8
9
use PDO;
10
use PDOStatement;
11
use PDOException;
12
use Vectorface\Cache\Common\PSR16Util;
13
14
/**
15
 * This cache is slow, according to basic benchmarks:
16
 *
17
 * Parameters:
18
 *   MySQL 5.0, running locally
19
 *   9-byte key
20
 *   151-byte value
21
 *   10000-iteration test
22
 *
23
 * Result:
24
 *   16.7824881077 seconds
25
 *
26
 * Conclusion:
27
 *   Capable of approximately 595.85 requests/second
28
 */
29
30
/**
31
 * A cache implementation that uses SQL for storage.
32
 *
33
 * An example table might look like:
34
 * CREATE TABLE cache (
35
 *     entry VARCHAR(64) PRIMARY KEY NOT NULL,
36
 *     value LONGBLOB,
37
 *     expires BIGINT UNSIGNED DEFAULT NULL,
38
 *     KEY expires (expires)
39
 * ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
40
 */
41
class SQLCache implements Cache
42
{
43
    use PSR16Util;
44
45
    /**
46
     * Hash keys beyond this size
47
     */
48
    const MAX_KEY_LEN = 64;
49
50
    /**
51
     * Statement for flushing all entries from the cache.
52
     */
53
    const FLUSH_SQL = 'DELETE FROM cache';
54
55
    /**
56
     * Statement for deleting expired entries from the cache.
57
     */
58
    const CLEAN_SQL = 'DELETE FROM cache WHERE expires<=UNIX_TIMESTAMP()';
59
60
    /**
61
     * Statement for inserting or updating entries in the cache.
62
     */
63
    const SET_SQL = 'INSERT INTO cache (entry,value,expires) VALUES(?,?,?)';
64
65
    /**
66
     * Statement for updating if an entry already exists.
67
     */
68
    const UPDATE_SQL = 'UPDATE cache SET value=?, expires=? WHERE entry=?';
69
70
    /**
71
     * Statement for checking if an entry exists
72
     */
73
    const HAS_SQL = 'SELECT COUNT(*) AS num FROM cache WHERE entry=? AND expires>=UNIX_TIMESTAMP()';
74
75
    /**
76
     * Statement for retrieving an entry from the cache
77
     */
78
    const GET_SQL = 'SELECT value FROM cache WHERE entry=? AND expires>=UNIX_TIMESTAMP()';
79
80
    /**
81
     * Statement for retrieving entries from the cache (no statement caching)
82
     */
83
    const MGET_SQL = 'SELECT entry,value FROM cache WHERE entry IN(%s) AND expires>=UNIX_TIMESTAMP()';
84
85
    /**
86
     * Statement for deleting an entry from the cache
87
     */
88
    const DELETE_SQL = 'DELETE FROM cache WHERE entry=?';
89
90
    /**
91
     * Statement for deleting entries from the cache (no statement caching)
92
     */
93
    const MDELETE_SQL = 'DELETE FROM cache WHERE entry IN(%s)';
94
95
    /**
96
     * The database connection to be used for cache operations.
97
     *
98
     * @var PDO
99
     */
100
    private $conn;
101
102
    /**
103
     * An associative array of PDO statements used in get/set.
104
     *
105
     * @var PDOStatement
106
     */
107
    private $statements = [];
108
109
    /**
110
     * Create an instance of the SQL cache.
111
     *
112
     * @param PDO $conn The database connection to use for cache operations.
113
     */
114 20
    public function __construct(PDO $conn)
115
    {
116 20
        $this->conn = $conn;
117 20
    }
118
119
    /**
120
     * @inheritDoc
121
     */
122 16
    public function get($key, $default = null)
123
    {
124 16
        $key = $this->key($key);
125 15
        $key = $this->hashKey($key);
126
127
        try {
128 15
            $stmt = $this->getStatement(__METHOD__, self::GET_SQL);
129 15
            $stmt->execute([$key]);
130 1
        } catch (PDOException $e) {
131 1
            return $default;
132
        }
133 15
        $result = $stmt->fetchColumn();
134 15
        return empty($result) ? $default : unserialize($result);
135
    }
136
137
    /**
138
     * @inheritDoc
139
     */
140 4
    public function getMultiple($keys, $default = null)
141
    {
142 4
        if (empty($keys)) {
143 1
            return [];
144
        }
145
146 4
        $keys = $this->keys($keys);
147 3
        $sqlKeys = array_map([$this, 'hashKey'], $keys);
148
149
        try {
150 3
            $stmt = $this->conn->prepare(sprintf(
151 3
                self::MGET_SQL,
152 3
                implode(',', array_fill(0, count($keys), '?'))
153
            ));
154 2
            $stmt->execute($sqlKeys);
155 2
            $result = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
156 1
        } catch (PDOException $e) {
157 1
            $result = [];
158
        }
159
160 3
        $return = array_map('unserialize', $result);
161 3
        foreach ($keys as $key) {
162 3
            if (!isset($return[$key])) {
163 3
                $return[$key] = $default;
164
            }
165
        }
166 3
        return $return;
167
    }
168
169
170
    /**
171
     * @inheritDoc
172
     */
173 19
    public function set($key, $value, $ttl = null)
174
    {
175 19
        $key = $this->key($key);
176 19
        $key = $this->hashKey($key);
177 19
        $ttl = $this->ttl($ttl);
178 18
        $ttl = $ttl ? ($ttl + time()) : PHP_INT_MAX;
179 18
        $value = serialize($value);
180
181
        try {
182 18
            $stmt = $this->getStatement(__METHOD__ . ".insert", self::SET_SQL);
183 18
            return $stmt->execute([$key, $value, $ttl]);
184 4
        } catch (PDOException $e) {
185
            // Insert can fail if the entry exists; It's normal.
186
        }
187
188
        try {
189 4
            $stmt = $this->getStatement(__METHOD__ . ".update", self::UPDATE_SQL);
190 4
            $success = $stmt->execute([$value, $ttl, $key]);
191 4
            return $success && $stmt->rowCount() === 1;
192 1
        } catch (PDOException $e) {
193 1
            return false;
194
        }
195
    }
196
197
    /**
198
     * @inheritDoc
199
     */
200 3
    public function setMultiple($values, $ttl = null)
201
    {
202 3
        $success = true;
203 3
        foreach ($this->values($values) as $key => $value) {
204 2
            $success = $this->set($key, $value, $ttl) && $success;
205
        }
206 2
        return $success;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $success returns the type boolean which is incompatible with the return type mandated by Vectorface\Cache\Cache::setMultiple() of Traversable|array.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
207
    }
208
209
    /**
210
     * @inheritDoc
211
     */
212 5
    public function delete($key)
213
    {
214 5
        $key = $this->hashKey($this->key($key));
215
216
        try {
217 5
            $stmt = $this->getStatement(__METHOD__, self::DELETE_SQL);
218 5
            return $stmt->execute([$key]);
219 1
        } catch (PDOException $e) {
220 1
            return false;
221
        }
222
    }
223
224
    /**
225
     * @inheritDoc
226
     */
227 3
    public function deleteMultiple($keys)
228
    {
229 3
        if (empty($keys)) {
230 1
            return true;
231
        }
232
233 3
        $keysArray = array_map([$this, 'hashKey'], is_array($keys) ? $keys : iterator_to_array($keys));
234
235
        try {
236 3
            $stmt = $this->conn->prepare(sprintf(
237 3
                self::MDELETE_SQL,
238 3
                implode(',', array_fill(0, count($keysArray), '?'))
239
            ));
240 2
            $stmt->execute($keysArray);
241 1
        } catch (PDOException $e) {
242 1
            return false;
243
        }
244 2
        return true;
245
    }
246
247
    /**
248
     * @inheritdoc
249
     */
250 2
    public function clean()
251
    {
252
        try {
253 2
            $this->conn->exec(self::CLEAN_SQL);
254 1
        } catch (PDOException $e) {
255 1
            return false;
256
        }
257 2
        return true;
258
    }
259
260
    /**
261
     * @inheritdoc
262
     */
263 7
    public function flush()
264
    {
265
        try {
266 7
            $this->conn->exec(self::FLUSH_SQL);
267 1
        } catch (PDOException $e) {
268 1
            return false;
269
        }
270 7
        return true;
271
    }
272
273
    /**
274
     * @inheritDoc
275
     */
276 3
    public function clear()
277
    {
278 3
        return $this->flush();
279
    }
280
281
    /**
282
     * @inheritDoc
283
     */
284 2
    public function has($key)
285
    {
286 2
        $key = $this->hashKey($key);
287
288
        try {
289 2
            $stmt = $this->getStatement(__METHOD__, self::HAS_SQL);
290 1
            $stmt->execute([$key]);
291 1
        } catch (PDOException $e) {
292 1
            return false;
293
        }
294 1
        return $stmt->fetchColumn() ? true : false;
295
    }
296
297
    /**
298
     * Get a prepared statement for the given method's SQL.
299
     *
300
     * The result is stored internally to limit repeated preparing of SQL.
301
     *
302
     * @param string $method The method name to for which this statement applies.
303
     * @param string $sql The SQL statement associated with the given method.
304
     * @return PDOStatement Returns the prepared statement for the given method.
305
     */
306 18
    private function getStatement($method, $sql)
307
    {
308 18
        if (empty($this->statements[$method])) {
309 18
            $this->statements[$method] = $this->conn->prepare($sql);
310
        }
311 18
        return $this->statements[$method];
312
    }
313
314
    /**
315
     * Get a unique hash key when the key is too long
316
     *
317
     * @param string $key
318
     * @return string The key, or the hash of the key parameter if it goes beyond maximum length
319
     * @private Public for testing
320
     */
321 19
    public static function hashKey($key)
322
    {
323 19
        return (strlen($key) > self::MAX_KEY_LEN) ? hash('sha256', $key) : $key;
324
    }
325
}
326