2 /***************************************************************
5 * (c) 2009 Ingo Renner <ingo@typo3.org>
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.
14 * The GNU General Public License can be found at
15 * http://www.gnu.org/copyleft/gpl.html.
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.
22 * This copyright notice MUST APPEAR in all copies of the script!
23 ***************************************************************/
27 * A caching backend which stores cache entries by using Memcached.
29 * This backend uses the following types of Memcache keys:
31 * xxx is tag name, value is array of associated identifiers identifier. This
32 * is "forward" tag index. It is mainly used for obtaining content by tag
33 * (get identifier by tag -> get content by identifier)
35 * xxx is identifier, value is array of associated tags. This is "reverse" tag
36 * index. It provides quick access for all tags associated with this identifier
37 * and used when removing the identifier
39 * Value is a List of all tags (array)
40 * Each key is prepended with a prefix. By default prefix consists from two parts
41 * separated by underscore character and ends in yet another underscore character:
43 * - Current site path obtained from the PATH_site constant
44 * This prefix makes sure that keys from the different installations do not
47 * Note: When using the Memcached backend to store values of more than ~1 MB,
48 * the data will be split into chunks to make them fit into the memcached limits.
50 * This file is a backport from FLOW3 by Ingo Renner.
53 * @subpackage t3lib_cache
56 class t3lib_cache_backend_MemcachedBackend
extends t3lib_cache_backend_AbstractBackend
{
59 * Max bucket size, (1024*1024)-42 bytes
62 const MAX_BUCKET_SIZE
= 1048534;
65 * Instance of the PHP Memcache class
72 * Array of Memcache server configurations
76 protected $servers = array();
79 * Indicates whether the memcache uses compression or not (requires zlib),
80 * either 0 or MEMCACHE_COMPRESSED
87 * A prefix to seperate stored data from other data possibly stored in the
88 * memcache. This prefix must be unique for each site in the tree. Default
89 * implementation uses MD5 of the current site path to make identifier prefix
94 protected $identifierPrefix;
97 * Indicates whther the server is connected
101 protected $serverConnected = false;
104 * Constructs this backend
106 * @param mixed $options Configuration options - depends on the actual backend
107 * @author Robert Lemke <robert@typo3.org>
109 public function __construct($options = array()) {
110 if (!extension_loaded('memcache')) {
111 throw new t3lib_cache_Exception(
112 'The PHP extension "memcached" must be installed and loaded in ' .
113 'order to use the Memcached backend.',
118 parent
::__construct($options);
120 $this->memcache
= new Memcache();
121 $this->identifierPrefix
= $this->getIdentifierPrefix();
122 $defaultPort = ini_get('memcache.default_port');
124 if (!count($this->servers
)) {
125 throw new t3lib_cache_Exception(
126 'No servers were given to Memcache',
131 foreach ($this->servers
as $serverConfiguration) {
132 if (substr($serverConfiguration, 0, 7) == 'unix://') {
133 $host = $serverConfiguration;
136 if (substr($serverConfiguration, 0, 6) === 'tcp://') {
137 $serverConfiguration = substr($serverConfiguration, 6);
139 if (strstr($serverConfiguration, ':') !== FALSE) {
140 list($host, $port) = explode(':', $serverConfiguration, 2);
142 $host = $serverConfiguration;
143 $port = $defaultPort;
147 if ($this->serverConnected
) {
148 $this->memcache
->addserver($host, $port);
150 // pconnect throws PHP warnings when it cannot connect!
151 $this->serverConnected
= @$this->memcache
->pconnect($host, $port);
155 if (!$this->serverConnected
) {
156 t3lib_div
::sysLog('Unable to connect to any Memcached server', 'core', 3);
161 * Setter for servers to be used. Expects an array, the values are expected
162 * to be formatted like "<host>[:<port>]" or "unix://<path>"
164 * @param array An array of servers to add.
166 * @author Christian Jul Jensen <julle@typo3.org>
168 protected function setServers(array $servers) {
169 $this->servers
= $servers;
173 * Setter for compression flags bit
175 * @param boolean $useCompression
177 * @author Christian Jul Jensen <julle@typo3.org>
179 protected function setCompression($useCompression) {
180 if ($useCompression === TRUE) {
181 $this->flags ^
= MEMCACHE_COMPRESSED
;
183 $this->flags
&= ~MEMCACHE_COMPRESSED
;
188 * Saves data in the cache.
190 * @param string An identifier for this specific cache entry
191 * @param string The data to be stored
192 * @param array Tags to associate with this cache entry
193 * @param integer Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited liftime.
195 * @throws t3lib_cache_Exception if no cache frontend has been set.
196 * @throws InvalidArgumentException if the identifier is not valid or the final memcached key is longer than 250 characters
197 * @throws t3lib_cache_exception_InvalidData if $data is not a string
198 * @author Christian Jul Jensen <julle@typo3.org>
199 * @author Karsten Dambekalns <karsten@typo3.org>
201 public function set($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
202 if (strlen($this->identifierPrefix
. $entryIdentifier) > 250) {
203 throw new InvalidArgumentException(
204 'Could not set value. Key more than 250 characters (' . $this->identifierPrefix
. $entryIdentifier . ').',
209 if (!$this->cache
instanceof t3lib_cache_frontend_Frontend
) {
210 throw new t3lib_cache_Exception(
211 'No cache frontend has been set yet via setCache().',
216 if (!is_string($data)) {
217 throw new t3lib_cache_Exception_InvalidData(
218 'The specified data is of type "' . gettype($data) .
219 '" but a string is expected.',
224 $tags[] = '%MEMCACHEBE%' . $this->cache
->getIdentifier();
225 $expiration = $lifetime !== NULL ?
$lifetime : $this->defaultLifetime
;
228 if(strlen($data) > self
::MAX_BUCKET_SIZE
) {
229 $data = str_split($data, 1024 * 1000);
233 foreach ($data as $chunk) {
234 $success &= $this->memcache
->set(
235 $this->identifierPrefix
. $entryIdentifier . '_chunk_' . $chunkNumber,
242 $success &= $this->memcache
->set(
243 $this->identifierPrefix
. $entryIdentifier,
244 'TYPO3*chunked:' . $chunkNumber,
249 $success = $this->memcache
->set(
250 $this->identifierPrefix
. $entryIdentifier,
257 if ($success === TRUE) {
258 $this->removeIdentifierFromAllTags($entryIdentifier);
259 $this->addTagsToTagIndex($tags);
260 $this->addIdentifierToTags($entryIdentifier, $tags);
262 } catch(Exception
$exception) {
263 throw new t3lib_cache_Exception(
264 'Could not set value. ' .
265 $exception->getMessage(),
272 * Loads data from the cache.
274 * @param string An identifier which describes the cache entry to load
275 * @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
276 * @author Christian Jul Jensen <julle@typo3.org>
277 * @author Karsten Dambekalns <karsten@typo3.org>
279 public function get($entryIdentifier) {
280 $value = $this->memcache
->get($this->identifierPrefix
. $entryIdentifier);
282 if (substr($value, 0, 14) === 'TYPO3*chunked:') {
283 list( , $chunkCount) = explode(':', $value);
286 for ($chunkNumber = 1 ; $chunkNumber < $chunkCount; $chunkNumber++
) {
287 $value .= $this->memcache
->get($this->identifierPrefix
. $entryIdentifier . '_chunk_' . $chunkNumber);
295 * Checks if a cache entry with the specified identifier exists.
297 * @param string An identifier specifying the cache entry
298 * @return boolean TRUE if such an entry exists, FALSE if not
299 * @author Christian Jul Jensen <julle@typo3.org>
300 * @author Karsten Dambekalns <karsten@typo3.org>
302 public function has($entryIdentifier) {
303 return $this->serverConnected
&& $this->memcache
->get($this->identifierPrefix
. $entryIdentifier) !== false;
307 * Removes all cache entries matching the specified identifier.
308 * Usually this only affects one entry but if - for what reason ever -
309 * old entries for the identifier still exist, they are removed as well.
311 * @param string Specifies the cache entry to remove
312 * @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
313 * @author Christian Jul Jensen <julle@typo3.org>
314 * @author Karsten Dambekalns <karsten@typo3.org>
316 public function remove($entryIdentifier) {
317 $this->removeIdentifierFromAllTags($entryIdentifier);
318 return $this->memcache
->delete($this->identifierPrefix
. $entryIdentifier);
322 * Finds and returns all cache entry identifiers which are tagged by the
325 * @param string The tag to search for
326 * @return array An array of entries with all matching entries. An empty array if no entries matched
327 * @author Karsten Dambekalns <karsten@typo3.org>
329 public function findIdentifiersByTag($tag) {
330 $identifiers = $this->memcache
->get($this->identifierPrefix
. 'tag_' . $tag);
332 if ($identifiers !== FALSE) {
333 return (array) $identifiers;
341 * Finds and returns all cache entry identifiers which are tagged by the
344 * @param array Array of tags to search for
345 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
346 * @author Ingo Renner <ingo@typo3.org>
348 public function findIdentifiersByTags(array $tags) {
349 $taggedEntries = array();
350 $foundEntries = array();
352 foreach ($tags as $tag) {
353 $taggedEntries[$tag] = $this->findIdentifiersByTag($tag);
356 $intersectedTaggedEntries = call_user_func_array('array_intersect', $taggedEntries);
358 foreach ($intersectedTaggedEntries as $entryIdentifier) {
359 $foundEntries[$entryIdentifier] = $entryIdentifier;
362 return $foundEntries;
366 * Removes all cache entries of this cache.
369 * @author Karsten Dambekalns <karsten@typo3.org>
371 public function flush() {
372 if (!$this->cache
instanceof t3lib_cache_frontend_Frontend
) {
373 throw new t3lib_cache_Exception('No cache frontend has been set via setCache() yet.', 1204111376);
376 $this->flushByTag('%MEMCACHEBE%' . $this->cache
->getIdentifier());
380 * Removes all cache entries of this cache which are tagged by the specified tag.
382 * @param string $tag The tag the entries must have
384 * @author Karsten Dambekalns <karsten@typo3.org>
386 public function flushByTag($tag) {
387 $identifiers = $this->findIdentifiersByTag($tag);
389 foreach($identifiers as $identifier) {
390 $this->remove($identifier);
396 * Removes all cache entries of this cache which are tagged by the specified tag.
398 * @param array The tags the entries must have
400 * @author Ingo Renner <ingo@typo3.org>
402 public function flushByTags(array $tags) {
403 foreach ($tags as $tag) {
404 $this->flushByTag($tag);
409 * Returns an array with all known tags
412 * @author Karsten Dambekalns <karsten@typo3.org>
415 protected function getTagIndex() {
416 $tagIndex = $this->memcache
->get($this->identifierPrefix
. 'tagIndex');
417 return ($tagIndex == false ?
array() : (array)$tagIndex);
421 * Saves the tags known to the backend
423 * @param array Array of tags
424 * @author Karsten Dambekalns <karsten@typo3.org>
427 protected function setTagIndex(array $tags) {
428 $this->memcache
->set($this->identifierPrefix
. 'tagIndex', array_unique($tags), 0, 0);
432 * Adds the given tags to the tag index
434 * @param array Array of tags
436 * @author Karsten Dambekalns <karsten@typo3.org>
439 protected function addTagsToTagIndex(array $tags) {
441 $this->setTagIndex(array_merge($tags, $this->getTagIndex()));
446 * Removes the given tags from the tag index
450 * @author Karsten Dambekalns <karsten@typo3.org>
453 protected function removeTagsFromTagIndex(array $tags) {
455 $this->setTagIndex(array_diff($this->getTagIndex(), $tags));
460 * Associates the identifier with the given tags
462 * @param string $entryIdentifier
463 * @param array Array of tags
464 * @author Karsten Dambekalns <karsten@typo3.org>
465 * @author Dmitry Dulepov <dmitry@typo3.org>
468 protected function addIdentifierToTags($entryIdentifier, array $tags) {
469 if ($this->serverConnected
) {
470 foreach($tags as $tag) {
471 // Update tag-to-identifier index
472 $identifiers = $this->findIdentifiersByTag($tag);
473 if (array_search($entryIdentifier, $identifiers) === false) {
474 $identifiers[] = $entryIdentifier;
475 $this->memcache
->set($this->identifierPrefix
. 'tag_' . $tag,
479 // Update identifier-to-tag index
480 $existingTags = $this->findTagsByIdentifier($entryIdentifier);
481 if (array_search($entryIdentifier, $existingTags) === false) {
482 $this->memcache
->set($this->identifierPrefix
. 'ident_' . $entryIdentifier,
483 array_merge($existingTags, $tags));
490 * Removes association of the identifier with the given tags
492 * @param string $entryIdentifier
493 * @param array Array of tags
494 * @author Karsten Dambekalns <karsten@typo3.org>
495 * @author Dmitry Dulepov <dmitry@typo3.org>
498 protected function removeIdentifierFromAllTags($entryIdentifier) {
499 if ($this->serverConnected
) {
500 // Get tags for this identifier
501 $tags = $this->findTagsByIdentifier($entryIdentifier);
502 // Deassociate tags with this identifier
503 foreach ($tags as $tag) {
504 $identifiers = $this->findIdentifiersByTag($tag);
505 // Formally array_search() below should never return false
506 // due to the behavior of findTagsForIdentifier(). But if
507 // reverse index is corrupted, we still can get 'false' from
508 // array_search(). This is not a problem because we are
509 // removing this identifier from anywhere.
510 if (($key = array_search($entryIdentifier, $identifiers)) !== false) {
511 unset($identifiers[$key]);
513 if(count($identifiers)) {
514 $this->memcache
->set(
515 $this->identifierPrefix
. 'tag_' . $tag,
519 $this->removeTagsFromTagIndex(array($tag));
520 $this->memcache
->delete($this->identifierPrefix
. 'tag_' . $tag);
525 // Clear reverse tag index for this identifier
526 $this->memcache
->delete($this->identifierPrefix
. 'ident_' . $entryIdentifier);
531 * Finds all tags for the given identifier. This function uses reverse tag
532 * index to search for tags.
534 * @param string Identifier to find tags by
535 * @return array Array with tags
536 * @author Dmitry Dulepov <dmitry@typo3.org>
539 protected function findTagsByIdentifier($identifier) {
540 $tags = $this->memcache
->get($this->identifierPrefix
. 'ident_' . $identifier);
541 return ($tags === FALSE ?
array() : (array)$tags);
545 * Returns idenfier prefix. Extensions can override this function to provide
546 * another identifier prefix if it is necessary for special purposes.
547 * Default identifier prefix is based on PATH_site only. In most cases
548 * it is enough because different installations use different paths and page
549 * IDs in the same installation never repeat.
551 * @return string Identifier prefix, ending with underscore
552 * @author Dmitry Dulepov
554 protected function getIdentifierPrefix() {
555 return 'TYPO3_' . md5(PATH_site
) . '_';
559 * Does nothing, as memcached does GC itself
563 public function collectGarbage() {
568 if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE
]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_memcachedbackend.php']) {
569 include_once($TYPO3_CONF_VARS[TYPO3_MODE
]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_memcachedbackend.php']);