TYPO3 CMS  TYPO3_8-7
RedisBackend.php
Go to the documentation of this file.
1 <?php
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 $persistentConnection = false;
82 
88  protected $hostname = '127.0.0.1';
89 
95  protected $port = 6379;
96 
102  protected $database = 0;
103 
109  protected $password = '';
110 
116  protected $compression = false;
117 
123  protected $compressionLevel = -1;
124 
130  protected $connectionTimeout = 0;
131 
139  public function __construct($context, array $options = [])
140  {
141  if (!extension_loaded('redis')) {
142  throw new \TYPO3\CMS\Core\Cache\Exception('The PHP extension "redis" must be installed and loaded in order to use the redis backend.', 1279462933);
143  }
144  parent::__construct($context, $options);
145  }
146 
152  public function initializeObject()
153  {
154  $this->redis = new \Redis();
155  try {
156  if ($this->persistentConnection) {
157  $this->connected = $this->redis->pconnect($this->hostname, $this->port, $this->connectionTimeout, (string)$this->database);
158  } else {
159  $this->connected = $this->redis->connect($this->hostname, $this->port, $this->connectionTimeout);
160  }
161  } catch (\Exception $e) {
162  \TYPO3\CMS\Core\Utility\GeneralUtility::sysLog('Could not connect to redis server.', 'core', \TYPO3\CMS\Core\Utility\GeneralUtility::SYSLOG_SEVERITY_ERROR);
163  }
164  if ($this->connected) {
165  if ($this->password !== '') {
166  $success = $this->redis->auth($this->password);
167  if (!$success) {
168  throw new \TYPO3\CMS\Core\Cache\Exception('The given password was not accepted by the redis server.', 1279765134);
169  }
170  }
171  if ($this->database >= 0) {
172  $success = $this->redis->select($this->database);
173  if (!$success) {
174  throw new \TYPO3\CMS\Core\Cache\Exception('The given database "' . $this->database . '" could not be selected.', 1279765144);
175  }
176  }
177  }
178  }
179 
187  {
188  $this->persistentConnection = $persistentConnection;
189  }
190 
197  public function setHostname($hostname)
198  {
199  $this->hostname = $hostname;
200  }
201 
208  public function setPort($port)
209  {
210  $this->port = $port;
211  }
212 
220  public function setDatabase($database)
221  {
222  if (!is_int($database)) {
223  throw new \InvalidArgumentException('The specified database number is of type "' . gettype($database) . '" but an integer is expected.', 1279763057);
224  }
225  if ($database < 0) {
226  throw new \InvalidArgumentException('The specified database "' . $database . '" must be greater or equal than zero.', 1279763534);
227  }
228  $this->database = $database;
229  }
230 
237  public function setPassword($password)
238  {
239  $this->password = $password;
240  }
241 
249  public function setCompression($compression)
250  {
251  if (!is_bool($compression)) {
252  throw new \InvalidArgumentException('The specified compression of type "' . gettype($compression) . '" but a boolean is expected.', 1289679153);
253  }
254  $this->compression = $compression;
255  }
256 
267  {
268  if (!is_int($compressionLevel)) {
269  throw new \InvalidArgumentException('The specified compression of type "' . gettype($compressionLevel) . '" but an integer is expected.', 1289679154);
270  }
271  if ($compressionLevel >= -1 && $compressionLevel <= 9) {
272  $this->compressionLevel = $compressionLevel;
273  } else {
274  throw new \InvalidArgumentException('The specified compression level must be an integer between -1 and 9.', 1289679155);
275  }
276  }
277 
288  {
289  if (!is_int($connectionTimeout)) {
290  throw new \InvalidArgumentException('The specified connection timeout is of type "' . gettype($connectionTimeout) . '" but an integer is expected.', 1487849315);
291  }
292 
293  if ($connectionTimeout < 0) {
294  throw new \InvalidArgumentException('The specified connection timeout "' . $connectionTimeout . '" must be greater or equal than zero.', 1487849326);
295  }
296 
297  $this->connectionTimeout = $connectionTimeout;
298  }
299 
314  public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
315  {
316  if (!$this->canBeUsedInStringContext($entryIdentifier)) {
317  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006651);
318  }
319  if (!is_string($data)) {
320  throw new \TYPO3\CMS\Core\Cache\Exception\InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1279469941);
321  }
322  $lifetime = $lifetime === null ? $this->defaultLifetime : $lifetime;
323  if (!is_int($lifetime)) {
324  throw new \InvalidArgumentException('The specified lifetime is of type "' . gettype($lifetime) . '" but an integer or NULL is expected.', 1279488008);
325  }
326  if ($lifetime < 0) {
327  throw new \InvalidArgumentException('The specified lifetime "' . $lifetime . '" must be greater or equal than zero.', 1279487573);
328  }
329  if ($this->connected) {
330  $expiration = $lifetime === 0 ? self::FAKED_UNLIMITED_LIFETIME : $lifetime;
331  if ($this->compression) {
332  $data = gzcompress($data, $this->compressionLevel);
333  }
334  $this->redis->setex(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier, $expiration, $data);
335  $addTags = $tags;
336  $removeTags = [];
337  $existingTags = $this->redis->sMembers(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
338  if (!empty($existingTags)) {
339  $addTags = array_diff($tags, $existingTags);
340  $removeTags = array_diff($existingTags, $tags);
341  }
342  if (!empty($removeTags) || !empty($addTags)) {
343  $queue = $this->redis->multi(\Redis::PIPELINE);
344  foreach ($removeTags as $tag) {
345  $queue->sRem(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier, $tag);
346  $queue->sRem(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
347  }
348  foreach ($addTags as $tag) {
349  $queue->sAdd(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier, $tag);
350  $queue->sAdd(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
351  }
352  $queue->exec();
353  }
354  }
355  }
356 
367  public function get($entryIdentifier)
368  {
369  if (!$this->canBeUsedInStringContext($entryIdentifier)) {
370  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006652);
371  }
372  $storedEntry = false;
373  if ($this->connected) {
374  $storedEntry = $this->redis->get(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier);
375  }
376  if ($this->compression && (string)$storedEntry !== '') {
377  $storedEntry = gzuncompress($storedEntry);
378  }
379  return $storedEntry;
380  }
381 
392  public function has($entryIdentifier)
393  {
394  if (!$this->canBeUsedInStringContext($entryIdentifier)) {
395  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006653);
396  }
397  return $this->connected && $this->redis->exists(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier);
398  }
399 
411  public function remove($entryIdentifier)
412  {
413  if (!$this->canBeUsedInStringContext($entryIdentifier)) {
414  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006654);
415  }
416  $elementsDeleted = false;
417  if ($this->connected) {
418  if ($this->redis->exists(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier)) {
419  $assignedTags = $this->redis->sMembers(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
420  $queue = $this->redis->multi(\Redis::PIPELINE);
421  foreach ($assignedTags as $tag) {
422  $queue->sRem(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
423  }
424  $queue->del(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier, self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
425  $queue->exec();
426  $elementsDeleted = true;
427  }
428  }
429  return $elementsDeleted;
430  }
431 
444  public function findIdentifiersByTag($tag)
445  {
446  if (!$this->canBeUsedInStringContext($tag)) {
447  throw new \InvalidArgumentException('The specified tag is of type "' . gettype($tag) . '" which can\'t be converted to string.', 1377006655);
448  }
449  $foundIdentifiers = [];
450  if ($this->connected) {
451  $foundIdentifiers = $this->redis->sMembers(self::TAG_IDENTIFIERS_PREFIX . $tag);
452  }
453  return $foundIdentifiers;
454  }
455 
463  public function flush()
464  {
465  if ($this->connected) {
466  $this->redis->flushDB();
467  }
468  }
469 
480  public function flushByTag($tag)
481  {
482  if (!$this->canBeUsedInStringContext($tag)) {
483  throw new \InvalidArgumentException('The specified tag is of type "' . gettype($tag) . '" which can\'t be converted to string.', 1377006656);
484  }
485  if ($this->connected) {
486  $identifiers = $this->redis->sMembers(self::TAG_IDENTIFIERS_PREFIX . $tag);
487  if (!empty($identifiers)) {
488  $this->removeIdentifierEntriesAndRelations($identifiers, [$tag]);
489  }
490  }
491  }
492 
503  public function collectGarbage()
504  {
505  $identifierToTagsKeys = $this->redis->keys(self::IDENTIFIER_TAGS_PREFIX . '*');
506  foreach ($identifierToTagsKeys as $identifierToTagsKey) {
507  list(, $identifier) = explode(':', $identifierToTagsKey);
508  // Check if the data entry still exists
509  if (!$this->redis->exists((self::IDENTIFIER_DATA_PREFIX . $identifier))) {
510  $tagsToRemoveIdentifierFrom = $this->redis->sMembers($identifierToTagsKey);
511  $queue = $this->redis->multi(\Redis::PIPELINE);
512  $queue->del($identifierToTagsKey);
513  foreach ($tagsToRemoveIdentifierFrom as $tag) {
514  $queue->sRem(self::TAG_IDENTIFIERS_PREFIX . $tag, $identifier);
515  }
516  $queue->exec();
517  }
518  }
519  }
520 
531  protected function removeIdentifierEntriesAndRelations(array $identifiers, array $tags)
532  {
533  // Set a temporary entry which holds all identifiers that need to be removed from
534  // the tag to identifiers sets
535  $uniqueTempKey = 'temp:' . StringUtility::getUniqueId();
536  $prefixedKeysToDelete = [$uniqueTempKey];
537  $prefixedIdentifierToTagsKeysToDelete = [];
538  foreach ($identifiers as $identifier) {
539  $prefixedKeysToDelete[] = self::IDENTIFIER_DATA_PREFIX . $identifier;
540  $prefixedIdentifierToTagsKeysToDelete[] = self::IDENTIFIER_TAGS_PREFIX . $identifier;
541  }
542  foreach ($tags as $tag) {
543  $prefixedKeysToDelete[] = self::TAG_IDENTIFIERS_PREFIX . $tag;
544  }
545  $tagToIdentifiersSetsToRemoveIdentifiersFrom = $this->redis->sUnion($prefixedIdentifierToTagsKeysToDelete);
546  // Remove the tag to identifier set of the given tags, they will be removed anyway
547  $tagToIdentifiersSetsToRemoveIdentifiersFrom = array_diff($tagToIdentifiersSetsToRemoveIdentifiersFrom, $tags);
548  // Diff all identifiers that must be removed from tag to identifiers sets off from a
549  // tag to identifiers set and store result in same tag to identifiers set again
550  $queue = $this->redis->multi(\Redis::PIPELINE);
551  foreach ($identifiers as $identifier) {
552  $queue->sAdd($uniqueTempKey, $identifier);
553  }
554  foreach ($tagToIdentifiersSetsToRemoveIdentifiersFrom as $tagToIdentifiersSet) {
555  $queue->sDiffStore(self::TAG_IDENTIFIERS_PREFIX . $tagToIdentifiersSet, self::TAG_IDENTIFIERS_PREFIX . $tagToIdentifiersSet, $uniqueTempKey);
556  }
557  $queue->del(array_merge($prefixedKeysToDelete, $prefixedIdentifierToTagsKeysToDelete));
558  $queue->exec();
559  }
560 
567  protected function canBeUsedInStringContext($variable)
568  {
569  return is_scalar($variable) || (is_object($variable) && method_exists($variable, '__toString'));
570  }
571 }
removeIdentifierEntriesAndRelations(array $identifiers, array $tags)
setPersistentConnection($persistentConnection)
__construct($context, array $options=[])