Issues (910)

framework/caching/DbCache.php (1 issue)

1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\caching;
9
10
use Yii;
11
use yii\base\InvalidConfigException;
12
use yii\db\Connection;
13
use yii\db\PdoValue;
14
use yii\db\Query;
15
use yii\di\Instance;
16
17
/**
18
 * DbCache implements a cache application component by storing cached data in a database.
19
 *
20
 * By default, DbCache stores session data in a DB table named 'cache'. This table
21
 * must be pre-created. The table name can be changed by setting [[cacheTable]].
22
 *
23
 * Please refer to [[Cache]] for common cache operations that are supported by DbCache.
24
 *
25
 * The following example shows how you can configure the application to use DbCache:
26
 *
27
 * ```php
28
 * 'cache' => [
29
 *     'class' => 'yii\caching\DbCache',
30
 *     // 'db' => 'mydb',
31
 *     // 'cacheTable' => 'my_cache',
32
 * ]
33
 * ```
34
 *
35
 * For more details and usage information on Cache, see the [guide article on caching](guide:caching-overview).
36
 *
37
 * @author Qiang Xue <[email protected]>
38
 * @since 2.0
39
 */
40
class DbCache extends Cache
41
{
42
    /**
43
     * @var Connection|array|string the DB connection object or the application component ID of the DB connection.
44
     * After the DbCache object is created, if you want to change this property, you should only assign it
45
     * with a DB connection object.
46
     * Starting from version 2.0.2, this can also be a configuration array for creating the object.
47
     */
48
    public $db = 'db';
49
    /**
50
     * @var string name of the DB table to store cache content.
51
     * The table should be pre-created as follows:
52
     *
53
     * ```php
54
     * CREATE TABLE cache (
55
     *     id char(128) NOT NULL PRIMARY KEY,
56
     *     expire int(11),
57
     *     data BLOB
58
     * );
59
     * ```
60
     *
61
     * For MSSQL:
62
     * ```php
63
     * CREATE TABLE cache (
64
     *     id VARCHAR(128) NOT NULL PRIMARY KEY,
65
     *     expire INT(11),
66
     *     data VARBINARY(MAX)
67
     * );
68
     * ```
69
     *
70
     * where 'BLOB' refers to the BLOB-type of your preferred DBMS. Below are the BLOB type
71
     * that can be used for some popular DBMS:
72
     *
73
     * - MySQL: LONGBLOB
74
     * - PostgreSQL: BYTEA
75
     *
76
     * When using DbCache in a production server, we recommend you create a DB index for the 'expire'
77
     * column in the cache table to improve the performance.
78
     */
79
    public $cacheTable = '{{%cache}}';
80
    /**
81
     * @var int the probability (parts per million) that garbage collection (GC) should be performed
82
     * when storing a piece of data in the cache. Defaults to 100, meaning 0.01% chance.
83
     * This number should be between 0 and 1000000. A value 0 meaning no GC will be performed at all.
84
     */
85
    public $gcProbability = 100;
86
87
    protected $isVarbinaryDataField;
88
89
90
    /**
91
     * Initializes the DbCache component.
92
     * This method will initialize the [[db]] property to make sure it refers to a valid DB connection.
93
     * @throws InvalidConfigException if [[db]] is invalid.
94
     */
95 41
    public function init()
96
    {
97 41
        parent::init();
98 41
        $this->db = Instance::ensure($this->db, Connection::className());
0 ignored issues
show
Deprecated Code introduced by
The function yii\base\BaseObject::className() has been deprecated: since 2.0.14. On PHP >=5.5, use `::class` instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

98
        $this->db = Instance::ensure($this->db, /** @scrutinizer ignore-deprecated */ Connection::className());

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
99
    }
100
101
    /**
102
     * Checks whether a specified key exists in the cache.
103
     * This can be faster than getting the value from the cache if the data is big.
104
     * Note that this method does not check whether the dependency associated
105
     * with the cached data, if there is any, has changed. So a call to [[get]]
106
     * may return false while exists returns true.
107
     * @param mixed $key a key identifying the cached value. This can be a simple string or
108
     * a complex data structure consisting of factors representing the key.
109
     * @return bool true if a value exists in cache, false if the value is not in the cache or expired.
110
     */
111 2
    public function exists($key)
112
    {
113 2
        $key = $this->buildKey($key);
114
115 2
        $query = new Query();
116 2
        $query->select(['COUNT(*)'])
117 2
            ->from($this->cacheTable)
118 2
            ->where('[[id]] = :id AND ([[expire]] = 0 OR [[expire]] >' . time() . ')', [':id' => $key]);
119 2
        if ($this->db->enableQueryCache) {
120
            // temporarily disable and re-enable query caching
121 2
            $this->db->enableQueryCache = false;
122 2
            $result = $query->createCommand($this->db)->queryScalar();
123 2
            $this->db->enableQueryCache = true;
124
        } else {
125
            $result = $query->createCommand($this->db)->queryScalar();
126
        }
127
128 2
        return $result > 0;
129
    }
130
131
    /**
132
     * Retrieves a value from cache with a specified key.
133
     * This is the implementation of the method declared in the parent class.
134
     * @param string $key a unique key identifying the cached value
135
     * @return string|false the value stored in cache, false if the value is not in the cache or expired.
136
     */
137 34
    protected function getValue($key)
138
    {
139 34
        $query = new Query();
140 34
        $query->select([$this->getDataFieldName()])
141 34
            ->from($this->cacheTable)
142 34
            ->where('[[id]] = :id AND ([[expire]] = 0 OR [[expire]] >' . time() . ')', [':id' => $key]);
143 34
        if ($this->db->enableQueryCache) {
144
            // temporarily disable and re-enable query caching
145 34
            $this->db->enableQueryCache = false;
146 34
            $result = $query->createCommand($this->db)->queryScalar();
147 34
            $this->db->enableQueryCache = true;
148
149 34
            return $result;
150
        }
151
152
        return $query->createCommand($this->db)->queryScalar();
153
    }
154
155
    /**
156
     * Retrieves multiple values from cache with the specified keys.
157
     * @param array $keys a list of keys identifying the cached values
158
     * @return array a list of cached values indexed by the keys
159
     */
160 4
    protected function getValues($keys)
161
    {
162 4
        if (empty($keys)) {
163
            return [];
164
        }
165 4
        $query = new Query();
166 4
        $query->select(['id', $this->getDataFieldName()])
167 4
            ->from($this->cacheTable)
168 4
            ->where(['id' => $keys])
169 4
            ->andWhere('([[expire]] = 0 OR [[expire]] > ' . time() . ')');
170
171 4
        if ($this->db->enableQueryCache) {
172 4
            $this->db->enableQueryCache = false;
173 4
            $rows = $query->createCommand($this->db)->queryAll();
174 4
            $this->db->enableQueryCache = true;
175
        } else {
176
            $rows = $query->createCommand($this->db)->queryAll();
177
        }
178
179 4
        $results = [];
180 4
        foreach ($keys as $key) {
181 4
            $results[$key] = false;
182
        }
183 4
        foreach ($rows as $row) {
184 4
            if (is_resource($row['data']) && get_resource_type($row['data']) === 'stream') {
185 2
                $results[$row['id']] = stream_get_contents($row['data']);
186
            } else {
187 2
                $results[$row['id']] = $row['data'];
188
            }
189
        }
190
191 4
        return $results;
192
    }
193
194
    /**
195
     * Stores a value identified by a key in cache.
196
     * This is the implementation of the method declared in the parent class.
197
     *
198
     * @param string $key the key identifying the value to be cached
199
     * @param string $value the value to be cached. Other types (if you have disabled [[serializer]]) cannot be saved.
200
     * @param int $duration the number of seconds in which the cached value will expire. 0 means never expire.
201
     * @return bool true if the value is successfully stored into cache, false otherwise
202
     */
203 32
    protected function setValue($key, $value, $duration)
204
    {
205
        try {
206 32
            $this->db->noCache(function (Connection $db) use ($key, $value, $duration) {
207 32
                $db->createCommand()->upsert($this->cacheTable, [
208 32
                    'id' => $key,
209 32
                    'expire' => $duration > 0 ? $duration + time() : 0,
210 32
                    'data' => $this->getDataFieldValue($value),
211 32
                ])->execute();
212 32
            });
213
214 32
            $this->gc();
215
216 32
            return true;
217
        } catch (\Exception $e) {
218
            Yii::warning("Unable to update or insert cache data: {$e->getMessage()}", __METHOD__);
219
220
            return false;
221
        }
222
    }
223
224
    /**
225
     * Stores a value identified by a key into cache if the cache does not contain this key.
226
     * This is the implementation of the method declared in the parent class.
227
     *
228
     * @param string $key the key identifying the value to be cached
229
     * @param string $value the value to be cached. Other types (if you have disabled [[serializer]]) cannot be saved.
230
     * @param int $duration the number of seconds in which the cached value will expire. 0 means never expire.
231
     * @return bool true if the value is successfully stored into cache, false otherwise
232
     */
233 6
    protected function addValue($key, $value, $duration)
234
    {
235 6
        $this->gc();
236
237
        try {
238 6
            $this->db->noCache(function (Connection $db) use ($key, $value, $duration) {
239 6
                $db->createCommand()
240 6
                    ->insert($this->cacheTable, [
241 6
                        'id' => $key,
242 6
                        'expire' => $duration > 0 ? $duration + time() : 0,
243 6
                        'data' => $this->getDataFieldValue($value),
244 6
                    ])->execute();
245 6
            });
246
247 6
            return true;
248 4
        } catch (\Exception $e) {
249 4
            Yii::warning("Unable to insert cache data: {$e->getMessage()}", __METHOD__);
250
251 4
            return false;
252
        }
253
    }
254
255
    /**
256
     * Deletes a value with the specified key from cache
257
     * This is the implementation of the method declared in the parent class.
258
     * @param string $key the key of the value to be deleted
259
     * @return bool if no error happens during deletion
260
     */
261 2
    protected function deleteValue($key)
262
    {
263 2
        $this->db->noCache(function (Connection $db) use ($key) {
264 2
            $db->createCommand()
265 2
                ->delete($this->cacheTable, ['id' => $key])
266 2
                ->execute();
267 2
        });
268
269 2
        return true;
270
    }
271
272
    /**
273
     * Removes the expired data values.
274
     * @param bool $force whether to enforce the garbage collection regardless of [[gcProbability]].
275
     * Defaults to false, meaning the actual deletion happens with the probability as specified by [[gcProbability]].
276
     */
277 34
    public function gc($force = false)
278
    {
279
280 34
        if ($force || random_int(0, 1000000) < $this->gcProbability) {
281
            $this->db->createCommand()
282
                ->delete($this->cacheTable, '[[expire]] > 0 AND [[expire]] < ' . time())
283
                ->execute();
284
        }
285
    }
286
287
    /**
288
     * Deletes all values from cache.
289
     * This is the implementation of the method declared in the parent class.
290
     * @return bool whether the flush operation was successful.
291
     */
292 22
    protected function flushValues()
293
    {
294 22
        $this->db->createCommand()
295 22
            ->delete($this->cacheTable)
296 22
            ->execute();
297
298 22
        return true;
299
    }
300
301
    /**
302
     * @return bool whether field is MSSQL varbinary
303
     * @since 2.0.42
304
     */
305 38
    protected function isVarbinaryDataField()
306
    {
307 38
        if ($this->isVarbinaryDataField === null) {
308 38
            $this->isVarbinaryDataField = in_array($this->db->getDriverName(), ['sqlsrv', 'dblib']) &&
309 38
                $this->db->getTableSchema($this->cacheTable)->columns['data']->dbType === 'varbinary';
310
        }
311 38
        return $this->isVarbinaryDataField;
312
    }
313
314
    /**
315
     * @return string `data` field name converted for usage in MSSQL (if needed)
316
     * @since 2.0.42
317
     */
318 36
    protected function getDataFieldName()
319
    {
320 36
        return $this->isVarbinaryDataField() ? 'CONVERT(VARCHAR(MAX), [[data]]) data' : 'data';
321
    }
322
323
    /**
324
     * @return PdoValue PdoValue or direct $value for usage in MSSQL
325
     * @since 2.0.42
326
     */
327 34
    protected function getDataFieldValue($value)
328
    {
329 34
        return $this->isVarbinaryDataField() ? $value : new PdoValue($value, \PDO::PARAM_LOB);
330
    }
331
}
332