TYPO3  7.6
RedisBackend.php
Go to the documentation of this file.
1 <?php
2 namespace TYPO3\CMS\Core\Cache\Backend;
3 
4 /*
5  * This file is part of the TYPO3 CMS project.
6  *
7  * It is free software; you can redistribute it and/or modify it under
8  * the terms of the GNU General Public License, either version 2
9  * of the License, or any later version.
10  *
11  * For the full copyright and license information, please read the
12  * LICENSE.txt file that was distributed with this source code.
13  *
14  * The TYPO3 project - inspiring people to share!
15  */
16 
18 
29 {
43  const FAKED_UNLIMITED_LIFETIME = 31536000;
49  const IDENTIFIER_DATA_PREFIX = 'identData:';
55  const IDENTIFIER_TAGS_PREFIX = 'identTags:';
61  const TAG_IDENTIFIERS_PREFIX = 'tagIdents:';
67  protected $redis;
68 
74  protected $connected = false;
75 
81  protected $hostname = '127.0.0.1';
82 
88  protected $port = 6379;
89 
95  protected $database = 0;
96 
102  protected $password = '';
103 
109  protected $compression = false;
110 
116  protected $compressionLevel = -1;
117 
125  public function __construct($context, array $options = array())
126  {
127  if (!extension_loaded('redis')) {
128  throw new \TYPO3\CMS\Core\Cache\Exception('The PHP extension "redis" must be installed and loaded in order to use the redis backend.', 1279462933);
129  }
130  parent::__construct($context, $options);
131  }
132 
139  public function initializeObject()
140  {
141  $this->redis = new \Redis();
142  try {
143  $this->connected = $this->redis->connect($this->hostname, $this->port);
144  } catch (\Exception $e) {
145  \TYPO3\CMS\Core\Utility\GeneralUtility::sysLog('Could not connect to redis server.', 'core', \TYPO3\CMS\Core\Utility\GeneralUtility::SYSLOG_SEVERITY_ERROR);
146  }
147  if ($this->connected) {
148  if ($this->password !== '') {
149  $success = $this->redis->auth($this->password);
150  if (!$success) {
151  throw new \TYPO3\CMS\Core\Cache\Exception('The given password was not accepted by the redis server.', 1279765134);
152  }
153  }
154  if ($this->database > 0) {
155  $success = $this->redis->select($this->database);
156  if (!$success) {
157  throw new \TYPO3\CMS\Core\Cache\Exception('The given database "' . $this->database . '" could not be selected.', 1279765144);
158  }
159  }
160  }
161  }
162 
170  public function setHostname($hostname)
171  {
172  $this->hostname = $hostname;
173  }
174 
182  public function setPort($port)
183  {
184  $this->port = $port;
185  }
186 
195  public function setDatabase($database)
196  {
197  if (!is_integer($database)) {
198  throw new \InvalidArgumentException('The specified database number is of type "' . gettype($database) . '" but an integer is expected.', 1279763057);
199  }
200  if ($database < 0) {
201  throw new \InvalidArgumentException('The specified database "' . $database . '" must be greater or equal than zero.', 1279763534);
202  }
203  $this->database = $database;
204  }
205 
213  public function setPassword($password)
214  {
215  $this->password = $password;
216  }
217 
226  public function setCompression($compression)
227  {
228  if (!is_bool($compression)) {
229  throw new \InvalidArgumentException('The specified compression of type "' . gettype($compression) . '" but a boolean is expected.', 1289679153);
230  }
231  $this->compression = $compression;
232  }
233 
245  {
246  if (!is_integer($compressionLevel)) {
247  throw new \InvalidArgumentException('The specified compression of type "' . gettype($compressionLevel) . '" but an integer is expected.', 1289679154);
248  }
249  if ($compressionLevel >= -1 && $compressionLevel <= 9) {
250  $this->compressionLevel = $compressionLevel;
251  } else {
252  throw new \InvalidArgumentException('The specified compression level must be an integer between -1 and 9.', 1289679155);
253  }
254  }
255 
271  public function set($entryIdentifier, $data, array $tags = array(), $lifetime = null)
272  {
273  if (!$this->canBeUsedInStringContext($entryIdentifier)) {
274  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006651);
275  }
276  if (!is_string($data)) {
277  throw new \TYPO3\CMS\Core\Cache\Exception\InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1279469941);
278  }
279  $lifetime = $lifetime === null ? $this->defaultLifetime : $lifetime;
280  if (!is_integer($lifetime)) {
281  throw new \InvalidArgumentException('The specified lifetime is of type "' . gettype($lifetime) . '" but an integer or NULL is expected.', 1279488008);
282  }
283  if ($lifetime < 0) {
284  throw new \InvalidArgumentException('The specified lifetime "' . $lifetime . '" must be greater or equal than zero.', 1279487573);
285  }
286  if ($this->connected) {
287  $expiration = $lifetime === 0 ? self::FAKED_UNLIMITED_LIFETIME : $lifetime;
288  if ($this->compression) {
289  $data = gzcompress($data, $this->compressionLevel);
290  }
291  $this->redis->setex(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier, $expiration, $data);
292  $addTags = $tags;
293  $removeTags = array();
294  $existingTags = $this->redis->sMembers(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
295  if (!empty($existingTags)) {
296  $addTags = array_diff($tags, $existingTags);
297  $removeTags = array_diff($existingTags, $tags);
298  }
299  if (!empty($removeTags) || !empty($addTags)) {
300  $queue = $this->redis->multi(\Redis::PIPELINE);
301  foreach ($removeTags as $tag) {
302  $queue->sRemove(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier, $tag);
303  $queue->sRemove(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
304  }
305  foreach ($addTags as $tag) {
306  $queue->sAdd(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier, $tag);
307  $queue->sAdd(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
308  }
309  $queue->exec();
310  }
311  }
312  }
313 
324  public function get($entryIdentifier)
325  {
326  if (!$this->canBeUsedInStringContext($entryIdentifier)) {
327  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006652);
328  }
329  $storedEntry = false;
330  if ($this->connected) {
331  $storedEntry = $this->redis->get(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier);
332  }
333  if ($this->compression && (string)$storedEntry !== '') {
334  $storedEntry = gzuncompress($storedEntry);
335  }
336  return $storedEntry;
337  }
338 
349  public function has($entryIdentifier)
350  {
351  if (!$this->canBeUsedInStringContext($entryIdentifier)) {
352  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006653);
353  }
354  return $this->connected && $this->redis->exists(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier);
355  }
356 
368  public function remove($entryIdentifier)
369  {
370  if (!$this->canBeUsedInStringContext($entryIdentifier)) {
371  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006654);
372  }
373  $elementsDeleted = false;
374  if ($this->connected) {
375  if ($this->redis->exists(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier)) {
376  $assignedTags = $this->redis->sMembers(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
377  $queue = $this->redis->multi(\Redis::PIPELINE);
378  foreach ($assignedTags as $tag) {
379  $queue->sRemove(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
380  }
381  $queue->delete(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier, self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
382  $queue->exec();
383  $elementsDeleted = true;
384  }
385  }
386  return $elementsDeleted;
387  }
388 
401  public function findIdentifiersByTag($tag)
402  {
403  if (!$this->canBeUsedInStringContext($tag)) {
404  throw new \InvalidArgumentException('The specified tag is of type "' . gettype($tag) . '" which can\'t be converted to string.', 1377006655);
405  }
406  $foundIdentifiers = array();
407  if ($this->connected) {
408  $foundIdentifiers = $this->redis->sMembers(self::TAG_IDENTIFIERS_PREFIX . $tag);
409  }
410  return $foundIdentifiers;
411  }
412 
421  public function flush()
422  {
423  if ($this->connected) {
424  $this->redis->flushdb();
425  }
426  }
427 
439  public function flushByTag($tag)
440  {
441  if (!$this->canBeUsedInStringContext($tag)) {
442  throw new \InvalidArgumentException('The specified tag is of type "' . gettype($tag) . '" which can\'t be converted to string.', 1377006656);
443  }
444  if ($this->connected) {
445  $identifiers = $this->redis->sMembers(self::TAG_IDENTIFIERS_PREFIX . $tag);
446  if (!empty($identifiers)) {
447  $this->removeIdentifierEntriesAndRelations($identifiers, array($tag));
448  }
449  }
450  }
451 
463  public function collectGarbage()
464  {
465  $identifierToTagsKeys = $this->redis->getKeys(self::IDENTIFIER_TAGS_PREFIX . '*');
466  foreach ($identifierToTagsKeys as $identifierToTagsKey) {
467  list(, $identifier) = explode(':', $identifierToTagsKey);
468  // Check if the data entry still exists
469  if (!$this->redis->exists((self::IDENTIFIER_DATA_PREFIX . $identifier))) {
470  $tagsToRemoveIdentifierFrom = $this->redis->sMembers($identifierToTagsKey);
471  $queue = $this->redis->multi(\Redis::PIPELINE);
472  $queue->delete($identifierToTagsKey);
473  foreach ($tagsToRemoveIdentifierFrom as $tag) {
474  $queue->sRemove(self::TAG_IDENTIFIERS_PREFIX . $tag, $identifier);
475  }
476  $queue->exec();
477  }
478  }
479  }
480 
492  protected function removeIdentifierEntriesAndRelations(array $identifiers, array $tags)
493  {
494  // Set a temporary entry which holds all identifiers that need to be removed from
495  // the tag to identifiers sets
496  $uniqueTempKey = 'temp:' . StringUtility::getUniqueId();
497  $prefixedKeysToDelete = array($uniqueTempKey);
498  $prefixedIdentifierToTagsKeysToDelete = array();
499  foreach ($identifiers as $identifier) {
500  $prefixedKeysToDelete[] = self::IDENTIFIER_DATA_PREFIX . $identifier;
501  $prefixedIdentifierToTagsKeysToDelete[] = self::IDENTIFIER_TAGS_PREFIX . $identifier;
502  }
503  foreach ($tags as $tag) {
504  $prefixedKeysToDelete[] = self::TAG_IDENTIFIERS_PREFIX . $tag;
505  }
506  $tagToIdentifiersSetsToRemoveIdentifiersFrom = $this->redis->sUnion($prefixedIdentifierToTagsKeysToDelete);
507  // Remove the tag to identifier set of the given tags, they will be removed anyway
508  $tagToIdentifiersSetsToRemoveIdentifiersFrom = array_diff($tagToIdentifiersSetsToRemoveIdentifiersFrom, $tags);
509  // Diff all identifiers that must be removed from tag to identifiers sets off from a
510  // tag to identifiers set and store result in same tag to identifiers set again
511  $queue = $this->redis->multi(\Redis::PIPELINE);
512  foreach ($identifiers as $identifier) {
513  $queue->sAdd($uniqueTempKey, $identifier);
514  }
515  foreach ($tagToIdentifiersSetsToRemoveIdentifiersFrom as $tagToIdentifiersSet) {
516  $queue->sDiffStore(self::TAG_IDENTIFIERS_PREFIX . $tagToIdentifiersSet, self::TAG_IDENTIFIERS_PREFIX . $tagToIdentifiersSet, $uniqueTempKey);
517  }
518  $queue->delete(array_merge($prefixedKeysToDelete, $prefixedIdentifierToTagsKeysToDelete));
519  $queue->exec();
520  }
521 
528  protected function canBeUsedInStringContext($variable)
529  {
530  return is_scalar($variable) || (is_object($variable) && method_exists($variable, '__toString'));
531  }
532 }