Fixed bug #11903: Use separate tables for tags in the caching framework
[Packages/TYPO3.CMS.git] / t3lib / cache / backend / class.t3lib_cache_backend_dbbackend.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2008-2009 Ingo Renner <ingo@typo3.org>
6 * All rights reserved
7 *
8 * This script is part of the TYPO3 project. The TYPO3 project is
9 * free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * The GNU General Public License can be found at
15 * http://www.gnu.org/copyleft/gpl.html.
16 *
17 * This script is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
21 *
22 * This copyright notice MUST APPEAR in all copies of the script!
23 ***************************************************************/
24
25
26 /**
27 * A caching backend which stores cache entries in database tables
28 *
29 * @package TYPO3
30 * @subpackage t3lib_cache
31 * @version $Id$
32 */
33 class t3lib_cache_backend_DbBackend extends t3lib_cache_backend_AbstractBackend {
34
35 protected $cacheTable;
36 protected $tagsTable;
37
38 protected $identifierField;
39 protected $creationField;
40 protected $lifetimeField;
41 protected $notExpiredStatement;
42 protected $tableList;
43 protected $tableJoin;
44
45 /**
46 * Constructs this backend
47 *
48 * @param mixed Configuration options - depends on the actual backend
49 */
50 public function __construct(array $options = array()) {
51 parent::__construct($options);
52
53 if (!$this->cacheTable) {
54 throw new t3lib_cache_Exception(
55 'No table to write data to has been set using the setting "cacheTable".',
56 1253534136
57 );
58 }
59
60 if (!$this->tagsTable) {
61 throw new t3lib_cache_Exception(
62 'No table to write tags to has been set using the setting "tagsTable".',
63 1253534137
64 );
65 }
66
67 $this->initializeCommonReferences();
68 }
69
70 /**
71 * Initializes common references used in this backend.
72 *
73 * @return void
74 */
75 protected function initializeCommonReferences() {
76 $this->identifierField = $this->cacheTable . '.identifier';
77 $this->creationField = $this->cacheTable . '.crdate';
78 $this->lifetimeField = $this->cacheTable . '.lifetime';
79 $this->tableList = $this->cacheTable . ', ' . $this->tagsTable;
80 $this->tableJoin = $this->identifierField . ' = ' . $this->tagsTable . '.identifier';
81 $this->notExpiredStatement = '(' . $this->creationField . ' + ' . $this->lifetimeField .
82 ' >= ' . $GLOBALS['EXEC_TIME'] . ' OR ' . $this->lifetimeField . ' = 0)';
83 }
84
85 /**
86 * Saves data in a cache file.
87 *
88 * @param string An identifier for this specific cache entry
89 * @param string The data to be stored
90 * @param array Tags to associate with this cache entry
91 * @param integer Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited liftime.
92 * @return void
93 * @throws t3lib_cache_Exception if no cache frontend has been set.
94 * @throws t3lib_cache_exception_InvalidData if the data to be stored is not a string.
95 * @author Ingo Renner <ingo@typo3.org>
96 */
97 public function set($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
98 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
99 throw new t3lib_cache_Exception(
100 'No cache frontend has been set via setCache() yet.',
101 1236518288
102 );
103 }
104
105 if (!is_string($data)) {
106 throw new t3lib_cache_exception_InvalidData(
107 'The specified data is of type "' . gettype($data) . '" but a string is expected.',
108 1236518298
109 );
110 }
111
112 if (is_null($lifetime)) {
113 $lifetime = $this->defaultLifetime;
114 }
115
116 $this->remove($entryIdentifier);
117
118 $GLOBALS['TYPO3_DB']->exec_INSERTquery(
119 $this->cacheTable,
120 array(
121 'identifier' => $entryIdentifier,
122 'crdate' => $GLOBALS['EXEC_TIME'],
123 'content' => $data,
124 'lifetime' => $lifetime
125 )
126 );
127
128 foreach ($tags as $tag) {
129 $GLOBALS['TYPO3_DB']->exec_INSERTquery(
130 $this->tagsTable,
131 array(
132 'identifier' => $entryIdentifier,
133 'tag' => $tag,
134 )
135 );
136 }
137 }
138
139 /**
140 * Loads data from a cache file.
141 *
142 * @param string An identifier which describes the cache entry to load
143 * @return mixed The cache entry's data as a string or FALSE if the cache entry could not be loaded
144 * @author Ingo Renner <ingo@typo3.org>
145 */
146 public function get($entryIdentifier) {
147 $cacheEntry = false;
148
149 $cacheEntries = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
150 'content',
151 $this->cacheTable,
152 'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->cacheTable) . ' '
153 . 'AND (crdate + lifetime >= ' . $GLOBALS['EXEC_TIME'] . ' OR lifetime = 0)'
154 );
155
156 if (count($cacheEntries) == 1) {
157 $cacheEntry = $cacheEntries[0]['content'];
158 }
159
160 return $cacheEntry;
161 }
162
163 /**
164 * Checks if a cache entry with the specified identifier exists.
165 *
166 * @param unknown_type
167 * @return boolean TRUE if such an entry exists, FALSE if not
168 * @author Ingo Renner <ingo@typo3.org>
169 */
170 public function has($entryIdentifier) {
171 $hasEntry = false;
172
173 $cacheEntries = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
174 'content',
175 $this->cacheTable,
176 'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->cacheTable) . ' '
177 . 'AND crdate + lifetime >= ' . $GLOBALS['EXEC_TIME']
178 );
179
180 if (count($cacheEntries) == 1) {
181 $hasEntry = true;
182 }
183
184 return $hasEntry;
185 }
186
187 /**
188 * Removes all cache entries matching the specified identifier.
189 * Usually this only affects one entry.
190 *
191 * @param string Specifies the cache entry to remove
192 * @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
193 * @author Ingo Renner <ingo@typo3.org>
194 */
195 public function remove($entryIdentifier) {
196 $entryRemoved = false;
197
198 $res = $GLOBALS['TYPO3_DB']->exec_DELETEquery(
199 $this->cacheTable,
200 'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->cacheTable)
201 );
202
203 $GLOBALS['TYPO3_DB']->exec_DELETEquery(
204 $this->tagsTable,
205 'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->tagsTable)
206 );
207
208 if($GLOBALS['TYPO3_DB']->sql_affected_rows($res) == 1) {
209 $entryRemoved = true;
210 }
211
212 return $entryRemoved;
213 }
214
215 /**
216 * Finds and returns all cache entries which are tagged by the specified tag.
217 *
218 * @param string The tag to search for
219 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
220 * @author Ingo Renner <ingo@typo3.org>
221 */
222 public function findIdentifiersByTag($tag) {
223 $cacheEntryIdentifiers = array();
224
225 $cacheEntryIdentifierRows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
226 $this->identifierField,
227 $this->tableList,
228 $this->getQueryForTag($tag) .
229 ' AND ' . $this->tableJoin .
230 ' AND ' . $this->notExpiredStatement,
231 $this->identifierField
232 );
233
234 foreach ($cacheEntryIdentifierRows as $cacheEntryIdentifierRow) {
235 $cacheEntryIdentifiers[$cacheEntryIdentifierRow['identifier']] = $cacheEntryIdentifierRow['identifier'];
236 }
237
238 return $cacheEntryIdentifiers;
239 }
240
241 /**
242 * Finds and returns all cache entry identifiers which are tagged by the
243 * specified tags.
244 *
245 * @param array Array of tags to search for
246 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
247 * @author Ingo Renner <ingo@typo3.org>
248 */
249 public function findIdentifiersByTags(array $tags) {
250 $cacheEntryIdentifiers = array();
251 $whereClause = array();
252
253 foreach ($tags as $tag) {
254 $whereClause[] = $this->getQueryForTag($tag);
255 }
256
257 $whereClause[] = $this->tableJoin;
258 $whereClause[] = $this->notExpiredStatement;
259
260 $cacheEntryIdentifierRows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
261 $this->identifierField,
262 $this->tableList,
263 implode(' AND ', $whereClause),
264 $this->identifierField
265 );
266
267 foreach ($cacheEntryIdentifierRows as $cacheEntryIdentifierRow) {
268 $cacheEntryIdentifiers[$cacheEntryIdentifierRow['identifier']] = $cacheEntryIdentifierRow['identifier'];
269 }
270
271 return $cacheEntryIdentifiers;
272 }
273
274 /**
275 * Removes all cache entries of this cache.
276 *
277 * @return void
278 * @author Ingo Renner <ingo@typo3.org>
279 */
280 public function flush() {
281 $GLOBALS['TYPO3_DB']->sql_query('TRUNCATE ' . $this->cacheTable);
282 $GLOBALS['TYPO3_DB']->sql_query('TRUNCATE ' . $this->tagsTable);
283 }
284
285 /**
286 * Removes all cache entries of this cache which are tagged by the specified tag.
287 *
288 * @param string The tag the entries must have
289 * @return void
290 */
291 public function flushByTag($tag) {
292 $GLOBALS['TYPO3_DB']->exec_DELETEmultipleTablesQuery(
293 $this->tableList,
294 $this->tableList,
295 $this->tableJoin .
296 ' AND ' . $this->getQueryForTag($tag)
297 );
298 }
299
300 /**
301 * Removes all cache entries of this cache which are tagged by the specified tags.
302 *
303 * @param array The tags the entries must have
304 * @return void
305 */
306 public function flushByTags(array $tags) {
307 if (count($tags)) {
308 $listQueryConditions = array();
309 foreach ($tags as $tag) {
310 $listQueryConditions[$tag] = $this->getQueryForTag($tag);
311 }
312
313 $GLOBALS['TYPO3_DB']->exec_DELETEmultipleTablesQuery(
314 $this->tableList,
315 $this->tableList,
316 $this->tableJoin .
317 ' AND (' . implode(' OR ', $listQueryConditions) . ')'
318 );
319 }
320 }
321
322 /**
323 * Does garbage collection
324 *
325 * @return void
326 * @author Ingo Renner <ingo@typo3.org>
327 */
328 public function collectGarbage() {
329 $GLOBALS['TYPO3_DB']->exec_DELETEmultipleTablesQuery(
330 $this->tableList,
331 $this->tableList,
332 $this->tableJoin .
333 ' AND ' . $this->cacheTable . '.crdate + ' . $this->cacheTable . '.lifetime < ' . $GLOBALS['EXEC_TIME'] .
334 ' AND ' . $this->cacheTable . '.lifetime > 0'
335 );
336
337 $GLOBALS['TYPO3_DB']->exec_DELETEquery(
338 $this->cacheTable,
339 'crdate + lifetime < ' . $GLOBALS['EXEC_TIME'] . ' AND lifetime > 0'
340 );
341 }
342
343 /**
344 * Sets the table where the cache entries are stored. The specified table
345 * must exist already.
346 *
347 * @param string The table.
348 * @return void
349 * @throws t3lib_cache_Exception if the table does not exist.
350 * @author Ingo Renner <ingo@typo3.org>
351 */
352 public function setCacheTable($cacheTable) {
353 /*
354
355 TODO reenable this check or remove it before 4.3 final
356
357 This check causes mysql warnings when not being logged in and calling
358 typo3/backend.php or the install tool.
359 Reason: the caches in typo3/init.php get initialized before a DB connection
360 has been established.
361 Related Question: Why aren't there warnings in the FE as the caches get
362 initialized in tslib_fe's constructor which is also before a DB conection
363 exsits?
364 Assumption Ingo Renner: Is a custom error_reporting level causing that?
365
366 There's also an unit test for that check (also deactivated for now).
367
368 $result = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
369 'id',
370 $cacheTable,
371 '',
372 '',
373 '',
374 1
375 );
376
377 if (!is_array($result)) {
378 throw new t3lib_cache_Exception(
379 'The table "' . $cacheTable . '" does not exist.',
380 1236516444
381 );
382 }
383 */
384 $this->cacheTable = $cacheTable;
385 $this->initializeCommonReferences();
386 }
387
388 /**
389 * Returns the table where the cache entries are stored.
390 *
391 * @return string The cache table.
392 * @author Ingo Renner <ingo@typo3.org>
393 */
394 public function getCacheTable() {
395 return $this->cacheTable;
396 }
397
398 /**
399 * Sets the table where cache tags are stored.
400 *
401 * @param string $tagsTabls: Name of the table
402 * @return void
403 */
404 public function setTagsTable($tagsTable) {
405 $this->tagsTable = $tagsTable;
406 $this->initializeCommonReferences();
407 }
408
409 /**
410 * Gets the table where cache tags are stored.
411 *
412 * @return string Name of the table storing tags
413 */
414 public function getTagsTable() {
415 return $this->tagsTable;
416 }
417
418 /**
419 * Gets the query to be used for selecting entries by a tag. The asterisk ("*")
420 * is allowed as a wildcard at the beginning and the end of a tag.
421 *
422 * @param string The tag to search for, the "*" wildcard is supported
423 * @return string the query to be used for selecting entries
424 * @author Oliver Hader <oliver@typo3.org>
425 */
426 protected function getQueryForTag($tag) {
427 if (strpos($tag, '*') === false) {
428 $query = $this->tagsTable . '.tag = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($tag, $this->tagsTable);
429 } else {
430 $patternForLike = $GLOBALS['TYPO3_DB']->escapeStrForLike(
431 $GLOBALS['TYPO3_DB']->quoteStr($tag, $this->tagsTable),
432 $this->tagsTable
433 );
434 $query = $this->tagsTable . '.tag LIKE \'' . $patternForLike . '\'';
435 }
436
437 return $query;
438 }
439 }
440
441
442 if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_dbbackend.php']) {
443 include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_dbbackend.php']);
444 }
445
446 ?>