[FEATURE] Caching framework db backend: better db layout
[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 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
140 throw new t3lib_cache_Exception(
141 'No cache frontend has been set via setCache() yet.',
142 1236518288
143 );
144 }
145
146 if (!is_string($data)) {
147 throw new t3lib_cache_exception_InvalidData(
148 'The specified data is of type "' . gettype($data) . '" but a string is expected.',
149 1236518298
150 );
151 }
152
153 if (is_null($lifetime)) {
154 $lifetime = $this->defaultLifetime;
155 }
156 if ($lifetime === 0 || $lifetime > $this->maximumLifetime) {
157 $lifetime = $this->maximumLifetime;
158 }
159 $expires = $GLOBALS['EXEC_TIME'] + $lifetime;
160
161 $this->remove($entryIdentifier);
162
163 if ($this->compression) {
164 $data = gzcompress($data, $this->compressionLevel);
165 }
166
167 $GLOBALS['TYPO3_DB']->exec_INSERTquery(
168 $this->cacheTable,
169 array(
170 'identifier' => $entryIdentifier,
171 'expires' => $expires,
172 'content' => $data,
173 )
174 );
175
176 if (count($tags)) {
177 $fields = array();
178 $fields[] = 'identifier';
179 $fields[] = 'tag';
180
181 $tagRows = array();
182 foreach ($tags as $tag) {
183 $tagRow = array();
184 $tagRow[] = $entryIdentifier;
185 $tagRow[] = $tag;
186 $tagRows[] = $tagRow;
187 }
188
189 $GLOBALS['TYPO3_DB']->exec_INSERTmultipleRows(
190 $this->tagsTable,
191 $fields,
192 $tagRows
193 );
194 }
195 }
196
197 /**
198 * Loads data from a cache file.
199 *
200 * @param string An identifier which describes the cache entry to load
201 * @return mixed The cache entry's data as a string or FALSE if the cache entry could not be loaded
202 * @author Ingo Renner <ingo@typo3.org>
203 */
204 public function get($entryIdentifier) {
205 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
206 throw new t3lib_cache_Exception(
207 'No cache frontend has been set via setCache() yet.',
208 1308435810
209 );
210 }
211
212 $cacheEntry = FALSE;
213
214 $cacheEntry = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow(
215 'content',
216 $this->cacheTable,
217 'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->cacheTable) .
218 ' AND ' . $this->notExpiredStatement
219 );
220
221 if (is_array($cacheEntry)) {
222 $cacheEntry = $cacheEntry['content'];
223 }
224
225 if ($this->compression && strlen($cacheEntry)) {
226 $cacheEntry = gzuncompress($cacheEntry);
227 }
228
229 return $cacheEntry;
230 }
231
232 /**
233 * Checks if a cache entry with the specified identifier exists.
234 *
235 * @param string Specifies the identifier to check for existence
236 * @return boolean TRUE if such an entry exists, FALSE if not
237 * @author Ingo Renner <ingo@typo3.org>
238 */
239 public function has($entryIdentifier) {
240 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
241 throw new t3lib_cache_Exception(
242 'No cache frontend has been set via setCache() yet.',
243 1308435811
244 );
245 }
246
247 $hasEntry = FALSE;
248
249 $cacheEntries = $GLOBALS['TYPO3_DB']->exec_SELECTcountRows(
250 '*',
251 $this->cacheTable,
252 'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->cacheTable) .
253 ' AND ' . $this->notExpiredStatement
254 );
255 if ($cacheEntries >= 1) {
256 $hasEntry = TRUE;
257 }
258
259 return $hasEntry;
260 }
261
262 /**
263 * Removes all cache entries matching the specified identifier.
264 * Usually this only affects one entry.
265 *
266 * @param string Specifies the cache entry to remove
267 * @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
268 * @author Ingo Renner <ingo@typo3.org>
269 */
270 public function remove($entryIdentifier) {
271 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
272 throw new t3lib_cache_Exception(
273 'No cache frontend has been set via setCache() yet.',
274 1308435812
275 );
276 }
277
278 $entryRemoved = FALSE;
279
280 $res = $GLOBALS['TYPO3_DB']->exec_DELETEquery(
281 $this->cacheTable,
282 'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->cacheTable)
283 );
284
285 $GLOBALS['TYPO3_DB']->exec_DELETEquery(
286 $this->tagsTable,
287 'identifier = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($entryIdentifier, $this->tagsTable)
288 );
289
290 if ($GLOBALS['TYPO3_DB']->sql_affected_rows($res) == 1) {
291 $entryRemoved = TRUE;
292 }
293
294 return $entryRemoved;
295 }
296
297 /**
298 * Finds and returns all cache entries which are tagged by the specified tag.
299 *
300 * @param string The tag to search for
301 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
302 * @author Ingo Renner <ingo@typo3.org>
303 */
304 public function findIdentifiersByTag($tag) {
305 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
306 throw new t3lib_cache_Exception(
307 'No cache frontend has been set via setCache() yet.',
308 1308435813
309 );
310 }
311
312 $cacheEntryIdentifiers = array();
313
314 $cacheEntryIdentifierRows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
315 $this->identifierField,
316 $this->tableList,
317 $this->getQueryForTag($tag) .
318 ' AND ' . $this->tableJoin .
319 ' AND ' . $this->notExpiredStatement,
320 $this->identifierField
321 );
322
323 foreach ($cacheEntryIdentifierRows as $cacheEntryIdentifierRow) {
324 $cacheEntryIdentifiers[$cacheEntryIdentifierRow['identifier']] = $cacheEntryIdentifierRow['identifier'];
325 }
326
327 return $cacheEntryIdentifiers;
328 }
329
330 /**
331 * Removes all cache entries of this cache.
332 *
333 * @return void
334 * @author Ingo Renner <ingo@typo3.org>
335 */
336 public function flush() {
337 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
338 throw new t3lib_cache_Exception(
339 'No cache frontend has been set via setCache() yet.',
340 1308435814
341 );
342 }
343
344 $GLOBALS['TYPO3_DB']->exec_TRUNCATEquery($this->cacheTable);
345 $GLOBALS['TYPO3_DB']->exec_TRUNCATEquery($this->tagsTable);
346 $GLOBALS['TYPO3_DB']->admin_query('DROP TABLE IF EXISTS ' . $this->cacheTable);
347 $GLOBALS['TYPO3_DB']->admin_query('DROP TABLE IF EXISTS ' . $this->tagsTable);
348 $this->createCacheTable();
349 $this->createTagsTable();
350 }
351
352 /**
353 * Removes all cache entries of this cache which are tagged by the specified tag.
354 *
355 * @param string The tag the entries must have
356 * @return void
357 */
358 public function flushByTag($tag) {
359 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
360 throw new t3lib_cache_Exception(
361 'No cache frontend has been set via setCache() yet.',
362 1308435815
363 );
364 }
365
366 $tagsTableWhereClause = $this->getQueryForTag($tag);
367
368 $this->deleteCacheTableRowsByTagsTableWhereClause($tagsTableWhereClause);
369
370 $GLOBALS['TYPO3_DB']->exec_DELETEquery(
371 $this->tagsTable,
372 $tagsTableWhereClause
373 );
374 }
375
376 /**
377 * Does garbage collection
378 *
379 * @return void
380 * @author Ingo Renner <ingo@typo3.org>
381 */
382 public function collectGarbage() {
383 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
384 throw new t3lib_cache_Exception(
385 'No cache frontend has been set via setCache() yet.',
386 1308435816
387 );
388 }
389
390 // Get identifiers of expired cache entries
391 $tagsEntryIdentifierRowsResource = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
392 'identifier',
393 $this->cacheTable,
394 $this->expiredStatement
395 );
396
397 $tagsEntryIdentifiers = array();
398 while ($tagsEntryIdentifierRow = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($tagsEntryIdentifierRowsResource)) {
399 $tagsEntryIdentifiers[] = $GLOBALS['TYPO3_DB']->fullQuoteStr(
400 $tagsEntryIdentifierRow['identifier'],
401 $this->tagsTable
402 );
403 }
404 $GLOBALS['TYPO3_DB']->sql_free_result($tagsEntryIdentifierRowsResource);
405
406 // Delete tag rows connected to expired cache entries
407 if (count($tagsEntryIdentifiers)) {
408 $GLOBALS['TYPO3_DB']->exec_DELETEquery(
409 $this->tagsTable,
410 'identifier IN (' . implode(', ', $tagsEntryIdentifiers) . ')'
411 );
412 }
413
414 // Delete expired cache rows
415 $GLOBALS['TYPO3_DB']->exec_DELETEquery(
416 $this->cacheTable,
417 $this->expiredStatement
418 );
419 }
420
421 /**
422 * Sets the table where the cache entries are stored.
423 *
424 * @deprecated since TYPO3 4.6: The backend calculates the
425 * table name internally, this method does nothing anymore
426 * @param string $cacheTable Table name
427 * @return void
428 * @author Ingo Renner <ingo@typo3.org>
429 */
430 public function setCacheTable($cacheTable) {
431 t3lib_div::logDeprecatedFunction();
432 }
433
434 /**
435 * Returns the table where the cache entries are stored.
436 *
437 * @return string The cache table.
438 * @author Ingo Renner <ingo@typo3.org>
439 */
440 public function getCacheTable() {
441 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
442 throw new t3lib_cache_Exception(
443 'No cache frontend has been set via setCache() yet.',
444 1308435817
445 );
446 }
447
448 return $this->cacheTable;
449 }
450
451 /**
452 * Sets the table where cache tags are stored.
453 *
454 * @deprecated since TYPO3 4.6: The backend calculates the
455 * table name internally, this method does nothing anymore
456 * @param string $tagsTable: Tags table name
457 * @return void
458 */
459 public function setTagsTable($tagsTable) {
460 t3lib_div::logDeprecatedFunction();
461 }
462
463 /**
464 * Gets the table where cache tags are stored.
465 *
466 * @return string Name of the table storing tags
467 */
468 public function getTagsTable() {
469 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
470 throw new t3lib_cache_Exception(
471 'No cache frontend has been set via setCache() yet.',
472 1308435818
473 );
474 }
475
476 return $this->tagsTable;
477 }
478
479 /**
480 * Enable data compression
481 *
482 * @param boolean TRUE to enable compression
483 */
484 public function setCompression($compression) {
485 $this->compression = $compression;
486 }
487
488 /**
489 * Set data compression level.
490 * If compression is enabled and this is not set,
491 * gzcompress default level will be used
492 *
493 * @param integer -1 to 9: Compression level
494 */
495 public function setCompressionLevel($compressionLevel) {
496 if ($compressionLevel >= -1 && $compressionLevel <= 9) {
497 $this->compressionLevel = $compressionLevel;
498 }
499 }
500
501 /**
502 * Create data table of cache
503 *
504 * @return void
505 */
506 protected function createCacheTable() {
507 $sql = file_get_contents(PATH_t3lib . 'cache/backend/resources/dbbackend-layout-cache.sql');
508 $sql = str_replace('###CACHE_TABLE###', $this->cacheTable, $sql);
509 $GLOBALS['TYPO3_DB']->admin_query($sql);
510 }
511
512 /**
513 * Create tags table of cache
514 *
515 * @return void
516 */
517 protected function createTagsTable() {
518 $sql = file_get_contents(PATH_t3lib . 'cache/backend/resources/dbbackend-layout-tags.sql');
519 $sql = str_replace('###TAGS_TABLE###', $this->tagsTable, $sql);
520 $GLOBALS['TYPO3_DB']->admin_query($sql);
521 }
522
523 /**
524 * Gets the query to be used for selecting entries by a tag. The asterisk ("*")
525 * is allowed as a wildcard at the beginning and the end of a tag.
526 *
527 * @param string The tag to search for, the "*" wildcard is supported
528 * @return string the query to be used for selecting entries
529 * @author Oliver Hader <oliver@typo3.org>
530 */
531 protected function getQueryForTag($tag) {
532 if (strpos($tag, '*') === FALSE) {
533 $query = $this->tagsTable . '.tag = ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($tag, $this->tagsTable);
534 } else {
535 $patternForLike = $GLOBALS['TYPO3_DB']->escapeStrForLike(
536 $GLOBALS['TYPO3_DB']->quoteStr($tag, $this->tagsTable),
537 $this->tagsTable
538 );
539 $query = $this->tagsTable . '.tag LIKE \'' . $patternForLike . '\'';
540 }
541
542 return $query;
543 }
544
545 /**
546 * Deletes rows in cache table found by where clause on tags table
547 *
548 * @param string The where clause for the tags table
549 * @return void
550 */
551 protected function deleteCacheTableRowsByTagsTableWhereClause($tagsTableWhereClause) {
552 $cacheEntryIdentifierRowsRessource = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
553 'DISTINCT identifier',
554 $this->tagsTable,
555 $tagsTableWhereClause
556 );
557
558 $cacheEntryIdentifiers = array();
559 while ($cacheEntryIdentifierRow = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($cacheEntryIdentifierRowsRessource)) {
560 $cacheEntryIdentifiers[] = $GLOBALS['TYPO3_DB']->fullQuoteStr(
561 $cacheEntryIdentifierRow['identifier'],
562 $this->cacheTable
563 );
564 }
565 $GLOBALS['TYPO3_DB']->sql_free_result($cacheEntryIdentifierRowsRessource);
566
567 if (count($cacheEntryIdentifiers)) {
568 $GLOBALS['TYPO3_DB']->exec_DELETEquery(
569 $this->cacheTable,
570 'identifier IN (' . implode(', ', $cacheEntryIdentifiers) . ')'
571 );
572 }
573 }
574 }
575
576
577 if (defined('TYPO3_MODE') && isset($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_dbbackend.php'])) {
578 include_once($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_dbbackend.php']);
579 }
580
581 ?>