[BUGFIX] Caching framework: Reduce code duplication in db backend
[Packages/TYPO3.CMS.git] / t3lib / cache / backend / class.t3lib_cache_backend_dbbackend.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2008-2011 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 * A caching backend which stores cache entries in database tables
27 *
28 * @package TYPO3
29 * @subpackage t3lib_cache
30 * @api
31 * @scope prototype
32 */
33 class t3lib_cache_backend_DbBackend extends t3lib_cache_backend_AbstractBackend {
34
35 /**
36 * @var integer Timestamp of 2038-01-01)
37 */
38 const FAKED_UNLIMITED_EXPIRE = 2145909600;
39
40 /**
41 * @var string Name of the cache data table
42 */
43 protected $cacheTable;
44
45 /**
46 * @var string Name of the cache tags table
47 */
48 protected $tagsTable;
49
50 /**
51 * @var boolean Indicates wether data is compressed or not (requires php zlib)
52 */
53 protected $compression = FALSE;
54
55 /**
56 * @var integer -1 to 9, indicates zlib compression level: -1 = default level 6, 0 = no compression, 9 maximum compression
57 */
58 protected $compressionLevel = -1;
59
60 /**
61 * @var string Name of the identifier field, 'table_name.identifier'
62 */
63 protected $identifierField;
64
65 /**
66 * @var string Name of the expire field, 'table_name.expires'
67 */
68 protected $expiresField;
69
70 /**
71 * @var integer Maximum lifetime to stay with expire field below FAKED_UNLIMITED_LIFETIME
72 */
73 protected $maximumLifetime;
74
75 /**
76 * @var string SQL where for a not expired entry
77 */
78 protected $notExpiredStatement;
79
80 /**
81 * @var string Opposite of notExpiredStatement
82 */
83 protected $expiredStatement;
84
85 /**
86 * @var string Data and tags table name comma separated
87 */
88 protected $tableList;
89
90 /**
91 * @var string Join condition for data and tags table
92 */
93 protected $tableJoin;
94
95 /**
96 * Set cache frontend instance and calculate data and tags table name
97 *
98 * @param t3lib_cache_frontend_Frontend $cache The frontend for this backend
99 * @return void
100 * @author Christian Kuhn <lolli@schwarzbu.ch>
101 * @api
102 */
103 public function setCache(t3lib_cache_frontend_Frontend $cache) {
104 parent::setCache($cache);
105
106 $this->cacheTable = 'cachingframework_' . $this->cacheIdentifier;
107 $this->tagsTable = 'cachingframework_' . $this->cacheIdentifier . '_tags';
108 $this->initializeCommonReferences();
109 }
110
111 /**
112 * Initializes common references used in this backend.
113 *
114 * @return void
115 */
116 protected function initializeCommonReferences() {
117 $this->identifierField = $this->cacheTable . '.identifier';
118 $this->expiresField = $this->cacheTable . '.expires';
119 $this->maximumLifetime = self::FAKED_UNLIMITED_EXPIRE - $GLOBALS['EXEC_TIME'];
120 $this->tableList = $this->cacheTable . ', ' . $this->tagsTable;
121 $this->tableJoin = $this->identifierField . ' = ' . $this->tagsTable . '.identifier';
122 $this->expiredStatement = $this->expiresField . ' < ' . $GLOBALS['EXEC_TIME'];
123 $this->notExpiredStatement = $this->expiresField . ' >= ' . $GLOBALS['EXEC_TIME'];
124 }
125
126 /**
127 * Saves data in a cache file.
128 *
129 * @param string An identifier for this specific cache entry
130 * @param string The data to be stored
131 * @param array Tags to associate with this cache entry
132 * @param integer Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited liftime.
133 * @return void
134 * @throws t3lib_cache_Exception if no cache frontend has been set.
135 * @throws t3lib_cache_exception_InvalidData if the data to be stored is not a string.
136 * @author Ingo Renner <ingo@typo3.org>
137 */
138 public function set($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
139 $this->throwExceptionIfFrontendDoesNotExist();
140
141 if (!is_string($data)) {
142 throw new t3lib_cache_exception_InvalidData(
143 'The specified data is of type "' . gettype($data) . '" but a string is expected.',
144 1236518298
145 );
146 }
147
148 if (is_null($lifetime)) {
149 $lifetime = $this->defaultLifetime;
150 }
151 if ($lifetime === 0 || $lifetime > $this->maximumLifetime) {
152 $lifetime = $this->maximumLifetime;
153 }
154 $expires = $GLOBALS['EXEC_TIME'] + $lifetime;
155
156 $this->remove($entryIdentifier);
157
158 if ($this->compression) {
159 $data = gzcompress($data, $this->compressionLevel);
160 }
161
162 $GLOBALS['TYPO3_DB']->exec_INSERTquery(
163 $this->cacheTable,
164 array(
165 'identifier' => $entryIdentifier,
166 'expires' => $expires,
167 'content' => $data,
168 )
169 );
170
171 if (count($tags)) {
172 $fields = array();
173 $fields[] = 'identifier';
174 $fields[] = 'tag';
175
176 $tagRows = array();
177 foreach ($tags as $tag) {
178 $tagRow = array();
179 $tagRow[] = $entryIdentifier;
180 $tagRow[] = $tag;
181 $tagRows[] = $tagRow;
182 }
183
184 $GLOBALS['TYPO3_DB']->exec_INSERTmultipleRows(
185 $this->tagsTable,
186 $fields,
187 $tagRows
188 );
189 }
190 }
191
192 /**
193 * Loads data from a cache file.
194 *
195 * @param string An identifier which describes the cache entry to load
196 * @return mixed The cache entry's data as a string or FALSE if the cache entry could not be loaded
197 * @author Ingo Renner <ingo@typo3.org>
198 */
199 public function get($entryIdentifier) {
200 $this->throwExceptionIfFrontendDoesNotExist();
201
202 $cacheEntry = FALSE;
203
204 $cacheEntry = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow(
205 'content',
206 $this->cacheTable,
207 'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->cacheTable) .
208 ' AND ' . $this->notExpiredStatement
209 );
210
211 if (is_array($cacheEntry)) {
212 $cacheEntry = $cacheEntry['content'];
213 }
214
215 if ($this->compression && strlen($cacheEntry)) {
216 $cacheEntry = gzuncompress($cacheEntry);
217 }
218
219 return $cacheEntry;
220 }
221
222 /**
223 * Checks if a cache entry with the specified identifier exists.
224 *
225 * @param string Specifies the identifier to check for existence
226 * @return boolean TRUE if such an entry exists, FALSE if not
227 * @author Ingo Renner <ingo@typo3.org>
228 */
229 public function has($entryIdentifier) {
230 $this->throwExceptionIfFrontendDoesNotExist();
231
232 $hasEntry = FALSE;
233
234 $cacheEntries = $GLOBALS['TYPO3_DB']->exec_SELECTcountRows(
235 '*',
236 $this->cacheTable,
237 'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->cacheTable) .
238 ' AND ' . $this->notExpiredStatement
239 );
240 if ($cacheEntries >= 1) {
241 $hasEntry = TRUE;
242 }
243
244 return $hasEntry;
245 }
246
247 /**
248 * Removes all cache entries matching the specified identifier.
249 * Usually this only affects one entry.
250 *
251 * @param string Specifies the cache entry to remove
252 * @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
253 * @author Ingo Renner <ingo@typo3.org>
254 */
255 public function remove($entryIdentifier) {
256 $this->throwExceptionIfFrontendDoesNotExist();
257
258 $entryRemoved = FALSE;
259
260 $res = $GLOBALS['TYPO3_DB']->exec_DELETEquery(
261 $this->cacheTable,
262 'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->cacheTable)
263 );
264
265 $GLOBALS['TYPO3_DB']->exec_DELETEquery(
266 $this->tagsTable,
267 'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->tagsTable)
268 );
269
270 if ($GLOBALS['TYPO3_DB']->sql_affected_rows($res) == 1) {
271 $entryRemoved = TRUE;
272 }
273
274 return $entryRemoved;
275 }
276
277 /**
278 * Finds and returns all cache entries which are tagged by the specified tag.
279 *
280 * @param string The tag to search for
281 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
282 * @author Ingo Renner <ingo@typo3.org>
283 */
284 public function findIdentifiersByTag($tag) {
285 $this->throwExceptionIfFrontendDoesNotExist();
286
287 $cacheEntryIdentifiers = array();
288
289 $cacheEntryIdentifierRows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
290 $this->identifierField,
291 $this->tableList,
292 $this->tagsTable . '.tag = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($tag, $this->tagsTable) .
293 ' AND ' . $this->tableJoin .
294 ' AND ' . $this->notExpiredStatement,
295 $this->identifierField
296 );
297
298 foreach ($cacheEntryIdentifierRows as $cacheEntryIdentifierRow) {
299 $cacheEntryIdentifiers[$cacheEntryIdentifierRow['identifier']] = $cacheEntryIdentifierRow['identifier'];
300 }
301
302 return $cacheEntryIdentifiers;
303 }
304
305 /**
306 * Removes all cache entries of this cache.
307 *
308 * @return void
309 * @author Ingo Renner <ingo@typo3.org>
310 */
311 public function flush() {
312 $this->throwExceptionIfFrontendDoesNotExist();
313
314 $GLOBALS['TYPO3_DB']->exec_TRUNCATEquery($this->cacheTable);
315 $GLOBALS['TYPO3_DB']->exec_TRUNCATEquery($this->tagsTable);
316 $GLOBALS['TYPO3_DB']->admin_query('DROP TABLE IF EXISTS ' . $this->cacheTable);
317 $GLOBALS['TYPO3_DB']->admin_query('DROP TABLE IF EXISTS ' . $this->tagsTable);
318 $this->createCacheTable();
319 $this->createTagsTable();
320 }
321
322 /**
323 * Removes all cache entries of this cache which are tagged by the specified tag.
324 *
325 * @param string The tag the entries must have
326 * @return void
327 */
328 public function flushByTag($tag) {
329 $this->throwExceptionIfFrontendDoesNotExist();
330
331 $tagsTableWhereClause = $this->tagsTable . '.tag = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($tag, $this->tagsTable);
332
333 $this->deleteCacheTableRowsByTagsTableWhereClause($tagsTableWhereClause);
334
335 $GLOBALS['TYPO3_DB']->exec_DELETEquery(
336 $this->tagsTable,
337 $tagsTableWhereClause
338 );
339 }
340
341 /**
342 * Does garbage collection
343 *
344 * @return void
345 * @author Ingo Renner <ingo@typo3.org>
346 */
347 public function collectGarbage() {
348 $this->throwExceptionIfFrontendDoesNotExist();
349
350 // Get identifiers of expired cache entries
351 $tagsEntryIdentifierRowsResource = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
352 'identifier',
353 $this->cacheTable,
354 $this->expiredStatement
355 );
356
357 $tagsEntryIdentifiers = array();
358 while ($tagsEntryIdentifierRow = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($tagsEntryIdentifierRowsResource)) {
359 $tagsEntryIdentifiers[] = $GLOBALS['TYPO3_DB']->fullQuoteStr(
360 $tagsEntryIdentifierRow['identifier'],
361 $this->tagsTable
362 );
363 }
364 $GLOBALS['TYPO3_DB']->sql_free_result($tagsEntryIdentifierRowsResource);
365
366 // Delete tag rows connected to expired cache entries
367 if (count($tagsEntryIdentifiers)) {
368 $GLOBALS['TYPO3_DB']->exec_DELETEquery(
369 $this->tagsTable,
370 'identifier IN (' . implode(', ', $tagsEntryIdentifiers) . ')'
371 );
372 }
373
374 // Delete expired cache rows
375 $GLOBALS['TYPO3_DB']->exec_DELETEquery(
376 $this->cacheTable,
377 $this->expiredStatement
378 );
379 }
380
381 /**
382 * Sets the table where the cache entries are stored.
383 *
384 * @deprecated since TYPO3 4.6: The backend calculates the
385 * table name internally, this method does nothing anymore
386 * @param string $cacheTable Table name
387 * @return void
388 * @author Ingo Renner <ingo@typo3.org>
389 */
390 public function setCacheTable($cacheTable) {
391 t3lib_div::logDeprecatedFunction();
392 }
393
394 /**
395 * Returns the table where the cache entries are stored.
396 *
397 * @return string The cache table.
398 * @author Ingo Renner <ingo@typo3.org>
399 */
400 public function getCacheTable() {
401 $this->throwExceptionIfFrontendDoesNotExist();
402
403 return $this->cacheTable;
404 }
405
406 /**
407 * Sets the table where cache tags are stored.
408 *
409 * @deprecated since TYPO3 4.6: The backend calculates the
410 * table name internally, this method does nothing anymore
411 * @param string $tagsTable: Tags table name
412 * @return void
413 */
414 public function setTagsTable($tagsTable) {
415 t3lib_div::logDeprecatedFunction();
416 }
417
418 /**
419 * Gets the table where cache tags are stored.
420 *
421 * @return string Name of the table storing tags
422 */
423 public function getTagsTable() {
424 $this->throwExceptionIfFrontendDoesNotExist();
425
426 return $this->tagsTable;
427 }
428
429 /**
430 * Enable data compression
431 *
432 * @param boolean TRUE to enable compression
433 */
434 public function setCompression($compression) {
435 $this->compression = $compression;
436 }
437
438 /**
439 * Set data compression level.
440 * If compression is enabled and this is not set,
441 * gzcompress default level will be used
442 *
443 * @param integer -1 to 9: Compression level
444 */
445 public function setCompressionLevel($compressionLevel) {
446 if ($compressionLevel >= -1 && $compressionLevel <= 9) {
447 $this->compressionLevel = $compressionLevel;
448 }
449 }
450
451 /**
452 * Check if required frontend instance exists
453 *
454 * @throws t3lib_cache_Exception If there is no frontend instance in $this->cache
455 * @return void
456 */
457 protected function throwExceptionIfFrontendDoesNotExist() {
458 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
459 throw new t3lib_cache_Exception(
460 'No cache frontend has been set via setCache() yet.',
461 1236518288
462 );
463 }
464 }
465
466 /**
467 * Create data table of cache
468 *
469 * @return void
470 */
471 protected function createCacheTable() {
472 $sql = file_get_contents(PATH_t3lib . 'cache/backend/resources/dbbackend-layout-cache.sql');
473 $sql = str_replace('###CACHE_TABLE###', $this->cacheTable, $sql);
474 $GLOBALS['TYPO3_DB']->admin_query($sql);
475 }
476
477 /**
478 * Create tags table of cache
479 *
480 * @return void
481 */
482 protected function createTagsTable() {
483 $sql = file_get_contents(PATH_t3lib . 'cache/backend/resources/dbbackend-layout-tags.sql');
484 $sql = str_replace('###TAGS_TABLE###', $this->tagsTable, $sql);
485 $GLOBALS['TYPO3_DB']->admin_query($sql);
486 }
487
488 /**
489 * Deletes rows in cache table found by where clause on tags table
490 *
491 * @param string The where clause for the tags table
492 * @return void
493 */
494 protected function deleteCacheTableRowsByTagsTableWhereClause($tagsTableWhereClause) {
495 $cacheEntryIdentifierRowsRessource = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
496 'DISTINCT identifier',
497 $this->tagsTable,
498 $tagsTableWhereClause
499 );
500
501 $cacheEntryIdentifiers = array();
502 while ($cacheEntryIdentifierRow = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($cacheEntryIdentifierRowsRessource)) {
503 $cacheEntryIdentifiers[] = $GLOBALS['TYPO3_DB']->fullQuoteStr(
504 $cacheEntryIdentifierRow['identifier'],
505 $this->cacheTable
506 );
507 }
508 $GLOBALS['TYPO3_DB']->sql_free_result($cacheEntryIdentifierRowsRessource);
509
510 if (count($cacheEntryIdentifiers)) {
511 $GLOBALS['TYPO3_DB']->exec_DELETEquery(
512 $this->cacheTable,
513 'identifier IN (' . implode(', ', $cacheEntryIdentifiers) . ')'
514 );
515 }
516 }
517 }
518
519
520 if (defined('TYPO3_MODE') && isset($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_dbbackend.php'])) {
521 include_once($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_dbbackend.php']);
522 }
523
524 ?>