‪TYPO3CMS  9.5
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 
20 
30 {
44  const ‪FAKED_UNLIMITED_LIFETIME = 31536000;
50  const ‪IDENTIFIER_DATA_PREFIX = 'identData:';
56  const ‪IDENTIFIER_TAGS_PREFIX = 'identTags:';
62  const ‪TAG_IDENTIFIERS_PREFIX = 'tagIdents:';
68  protected ‪$redis;
69 
75  protected ‪$connected = false;
76 
82  protected ‪$persistentConnection = false;
83 
89  protected ‪$hostname = '127.0.0.1';
90 
96  protected ‪$port = 6379;
97 
103  protected ‪$database = 0;
104 
110  protected ‪$password = '';
111 
117  protected ‪$compression = false;
118 
124  protected ‪$compressionLevel = -1;
125 
131  protected ‪$connectionTimeout = 0;
132 
140  public function ‪__construct(‪$context, array $options = [])
141  {
142  if (!extension_loaded('redis')) {
143  throw new ‪Exception('The PHP extension "redis" must be installed and loaded in order to use the redis backend.', 1279462933);
144  }
145  parent::__construct(‪$context, $options);
146  }
147 
153  public function ‪initializeObject()
154  {
155  $this->redis = new \Redis();
156  try {
157  if ($this->persistentConnection) {
158  $this->connected = $this->redis->pconnect($this->hostname, $this->port, $this->connectionTimeout, (string)$this->database);
159  } else {
160  $this->connected = $this->redis->connect($this->hostname, $this->port, $this->connectionTimeout);
161  }
162  } catch (\Exception $e) {
163  $this->logger->alert('Could not connect to redis server.', ['exception' => $e]);
164  }
165  if ($this->connected) {
166  if ($this->password !== '') {
167  $success = $this->redis->auth($this->password);
168  if (!$success) {
169  throw new Exception('The given password was not accepted by the redis server.', 1279765134);
170  }
171  }
172  if ($this->database >= 0) {
173  $success = $this->redis->select($this->database);
174  if (!$success) {
175  throw new Exception('The given database "' . $this->database . '" could not be selected.', 1279765144);
176  }
177  }
178  }
179  }
180 
187  {
188  $this->persistentConnection = ‪$persistentConnection;
189  }
190 
196  public function ‪setHostname(‪$hostname)
197  {
198  $this->hostname = ‪$hostname;
199  }
200 
206  public function ‪setPort(‪$port)
207  {
208  $this->port = ‪$port;
209  }
210 
217  public function ‪setDatabase(‪$database)
218  {
219  if (!is_int(‪$database)) {
220  throw new \InvalidArgumentException('The specified database number is of type "' . gettype(‪$database) . '" but an integer is expected.', 1279763057);
221  }
222  if (‪$database < 0) {
223  throw new \InvalidArgumentException('The specified database "' . ‪$database . '" must be greater or equal than zero.', 1279763534);
224  }
225  $this->database = ‪$database;
226  }
227 
233  public function ‪setPassword(‪$password)
234  {
235  $this->password = ‪$password;
236  }
237 
244  public function ‪setCompression(‪$compression)
245  {
246  if (!is_bool(‪$compression)) {
247  throw new \InvalidArgumentException('The specified compression of type "' . gettype(‪$compression) . '" but a boolean is expected.', 1289679153);
248  }
249  $this->compression = ‪$compression;
250  }
251 
261  {
262  if (!is_int(‪$compressionLevel)) {
263  throw new \InvalidArgumentException('The specified compression of type "' . gettype(‪$compressionLevel) . '" but an integer is expected.', 1289679154);
264  }
265  if (‪$compressionLevel >= -1 && ‪$compressionLevel <= 9) {
266  $this->compressionLevel = ‪$compressionLevel;
267  } else {
268  throw new \InvalidArgumentException('The specified compression level must be an integer between -1 and 9.', 1289679155);
269  }
270  }
271 
281  {
282  if (!is_int(‪$connectionTimeout)) {
283  throw new \InvalidArgumentException('The specified connection timeout is of type "' . gettype(‪$connectionTimeout) . '" but an integer is expected.', 1487849315);
284  }
285 
286  if (‪$connectionTimeout < 0) {
287  throw new \InvalidArgumentException('The specified connection timeout "' . ‪$connectionTimeout . '" must be greater or equal than zero.', 1487849326);
288  }
289 
290  $this->connectionTimeout = ‪$connectionTimeout;
291  }
292 
306  public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
307  {
308  if (!$this->‪canBeUsedInStringContext($entryIdentifier)) {
309  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006651);
310  }
311  if (!is_string($data)) {
312  throw new InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1279469941);
313  }
314  $lifetime = $lifetime ?? ‪$this->defaultLifetime;
315  if (!is_int($lifetime)) {
316  throw new \InvalidArgumentException('The specified lifetime is of type "' . gettype($lifetime) . '" but an integer or NULL is expected.', 1279488008);
317  }
318  if ($lifetime < 0) {
319  throw new \InvalidArgumentException('The specified lifetime "' . $lifetime . '" must be greater or equal than zero.', 1279487573);
320  }
321  if ($this->connected) {
322  $expiration = $lifetime === 0 ? ‪self::FAKED_UNLIMITED_LIFETIME : $lifetime;
323  if ($this->compression) {
324  $data = gzcompress($data, $this->compressionLevel);
325  }
326  $this->redis->setex(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier, $expiration, $data);
327  $addTags = $tags;
328  $removeTags = [];
329  $existingTags = $this->redis->sMembers(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
330  if (!empty($existingTags)) {
331  $addTags = array_diff($tags, $existingTags);
332  $removeTags = array_diff($existingTags, $tags);
333  }
334  if (!empty($removeTags) || !empty($addTags)) {
335  $queue = $this->redis->multi(\Redis::PIPELINE);
336  foreach ($removeTags as $tag) {
337  $queue->sRem(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier, $tag);
338  $queue->sRem(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
339  }
340  foreach ($addTags as $tag) {
341  $queue->sAdd(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier, $tag);
342  $queue->sAdd(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
343  }
344  $queue->exec();
345  }
346  }
347  }
348 
358  public function get($entryIdentifier)
359  {
360  if (!$this->‪canBeUsedInStringContext($entryIdentifier)) {
361  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006652);
362  }
363  $storedEntry = false;
364  if ($this->connected) {
365  $storedEntry = $this->redis->get(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier);
366  }
367  if ($this->compression && (string)$storedEntry !== '') {
368  $storedEntry = gzuncompress($storedEntry);
369  }
370  return $storedEntry;
371  }
372 
382  public function ‪has($entryIdentifier)
383  {
384  if (!$this->‪canBeUsedInStringContext($entryIdentifier)) {
385  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006653);
386  }
387  return $this->connected && $this->redis->exists(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier);
388  }
389 
400  public function remove($entryIdentifier)
401  {
402  if (!$this->‪canBeUsedInStringContext($entryIdentifier)) {
403  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006654);
404  }
405  $elementsDeleted = false;
406  if ($this->connected) {
407  if ($this->redis->exists(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier)) {
408  $assignedTags = $this->redis->sMembers(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
409  $queue = $this->redis->multi(\Redis::PIPELINE);
410  foreach ($assignedTags as $tag) {
411  $queue->sRem(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
412  }
413  $queue->del(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier, self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
414  $queue->exec();
415  $elementsDeleted = true;
416  }
417  }
418  return $elementsDeleted;
419  }
420 
432  public function ‪findIdentifiersByTag($tag)
433  {
434  if (!$this->‪canBeUsedInStringContext($tag)) {
435  throw new \InvalidArgumentException('The specified tag is of type "' . gettype($tag) . '" which can\'t be converted to string.', 1377006655);
436  }
437  $foundIdentifiers = [];
438  if ($this->connected) {
439  $foundIdentifiers = $this->redis->sMembers(self::TAG_IDENTIFIERS_PREFIX . $tag);
440  }
441  return $foundIdentifiers;
442  }
443 
449  public function ‪flush()
450  {
451  if ($this->connected) {
452  $this->redis->flushDB();
453  }
454  }
455 
465  public function ‪flushByTag($tag)
466  {
467  if (!$this->‪canBeUsedInStringContext($tag)) {
468  throw new \InvalidArgumentException('The specified tag is of type "' . gettype($tag) . '" which can\'t be converted to string.', 1377006656);
469  }
470  if ($this->connected) {
471  $identifiers = $this->redis->sMembers(self::TAG_IDENTIFIERS_PREFIX . $tag);
472  if (!empty($identifiers)) {
473  $this->‪removeIdentifierEntriesAndRelations($identifiers, [$tag]);
474  }
475  }
476  }
477 
486  public function ‪collectGarbage()
487  {
488  $identifierToTagsKeys = $this->redis->keys(self::IDENTIFIER_TAGS_PREFIX . '*');
489  foreach ($identifierToTagsKeys as $identifierToTagsKey) {
490  list(, $identifier) = explode(':', $identifierToTagsKey);
491  // Check if the data entry still exists
492  if (!$this->redis->exists(self::IDENTIFIER_DATA_PREFIX . $identifier)) {
493  $tagsToRemoveIdentifierFrom = $this->redis->sMembers($identifierToTagsKey);
494  $queue = $this->redis->multi(\Redis::PIPELINE);
495  $queue->del($identifierToTagsKey);
496  foreach ($tagsToRemoveIdentifierFrom as $tag) {
497  $queue->sRem(self::TAG_IDENTIFIERS_PREFIX . $tag, $identifier);
498  }
499  $queue->exec();
500  }
501  }
502  }
503 
514  protected function ‪removeIdentifierEntriesAndRelations(array $identifiers, array $tags)
515  {
516  // Set a temporary entry which holds all identifiers that need to be removed from
517  // the tag to identifiers sets
518  $uniqueTempKey = 'temp:' . ‪StringUtility::getUniqueId();
519  $prefixedKeysToDelete = [$uniqueTempKey];
520  $prefixedIdentifierToTagsKeysToDelete = [];
521  foreach ($identifiers as $identifier) {
522  $prefixedKeysToDelete[] = self::IDENTIFIER_DATA_PREFIX . $identifier;
523  $prefixedIdentifierToTagsKeysToDelete[] = self::IDENTIFIER_TAGS_PREFIX . $identifier;
524  }
525  foreach ($tags as $tag) {
526  $prefixedKeysToDelete[] = self::TAG_IDENTIFIERS_PREFIX . $tag;
527  }
528  $tagToIdentifiersSetsToRemoveIdentifiersFrom = $this->redis->sUnion($prefixedIdentifierToTagsKeysToDelete);
529  // Remove the tag to identifier set of the given tags, they will be removed anyway
530  $tagToIdentifiersSetsToRemoveIdentifiersFrom = array_diff($tagToIdentifiersSetsToRemoveIdentifiersFrom, $tags);
531  // Diff all identifiers that must be removed from tag to identifiers sets off from a
532  // tag to identifiers set and store result in same tag to identifiers set again
533  $queue = $this->redis->multi(\Redis::PIPELINE);
534  foreach ($identifiers as $identifier) {
535  $queue->sAdd($uniqueTempKey, $identifier);
536  }
537  foreach ($tagToIdentifiersSetsToRemoveIdentifiersFrom as $tagToIdentifiersSet) {
538  $queue->sDiffStore(self::TAG_IDENTIFIERS_PREFIX . $tagToIdentifiersSet, self::TAG_IDENTIFIERS_PREFIX . $tagToIdentifiersSet, $uniqueTempKey);
539  }
540  $queue->del(array_merge($prefixedKeysToDelete, $prefixedIdentifierToTagsKeysToDelete));
541  $queue->exec();
542  }
543 
550  protected function ‪canBeUsedInStringContext($variable)
551  {
552  return is_scalar($variable) || (is_object($variable) && method_exists($variable, '__toString'));
553  }
554 }
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\setDatabase
‪setDatabase($database)
Definition: RedisBackend.php:207
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\removeIdentifierEntriesAndRelations
‪removeIdentifierEntriesAndRelations(array $identifiers, array $tags)
Definition: RedisBackend.php:504
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\TAG_IDENTIFIERS_PREFIX
‪const TAG_IDENTIFIERS_PREFIX
Definition: RedisBackend.php:62
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\setPersistentConnection
‪setPersistentConnection($persistentConnection)
Definition: RedisBackend.php:176
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\collectGarbage
‪collectGarbage()
Definition: RedisBackend.php:476
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\$connectionTimeout
‪int $connectionTimeout
Definition: RedisBackend.php:121
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\$compression
‪bool $compression
Definition: RedisBackend.php:109
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\setPassword
‪setPassword($password)
Definition: RedisBackend.php:223
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\$hostname
‪string $hostname
Definition: RedisBackend.php:85
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\flush
‪flush()
Definition: RedisBackend.php:439
‪TYPO3\CMS\Core\Cache\Backend\TaggableBackendInterface
Definition: TaggableBackendInterface.php:21
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\FAKED_UNLIMITED_LIFETIME
‪const FAKED_UNLIMITED_LIFETIME
Definition: RedisBackend.php:44
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend\$defaultLifetime
‪int $defaultLifetime
Definition: AbstractBackend.php:54
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\IDENTIFIER_DATA_PREFIX
‪const IDENTIFIER_DATA_PREFIX
Definition: RedisBackend.php:50
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\$compressionLevel
‪int $compressionLevel
Definition: RedisBackend.php:115
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\findIdentifiersByTag
‪array findIdentifiersByTag($tag)
Definition: RedisBackend.php:422
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\$port
‪int $port
Definition: RedisBackend.php:91
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend
Definition: RedisBackend.php:30
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\IDENTIFIER_TAGS_PREFIX
‪const IDENTIFIER_TAGS_PREFIX
Definition: RedisBackend.php:56
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\flushByTag
‪flushByTag($tag)
Definition: RedisBackend.php:455
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\$database
‪int $database
Definition: RedisBackend.php:97
‪TYPO3\CMS\Core\Cache\Exception
Definition: DuplicateIdentifierException.php:2
‪TYPO3\CMS\Core\Cache\Exception\InvalidDataException
Definition: InvalidDataException.php:21
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\has
‪bool has($entryIdentifier)
Definition: RedisBackend.php:372
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\__construct
‪__construct($context, array $options=[])
Definition: RedisBackend.php:130
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\$persistentConnection
‪bool $persistentConnection
Definition: RedisBackend.php:79
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\setHostname
‪setHostname($hostname)
Definition: RedisBackend.php:186
‪TYPO3\CMS\Core\Utility\StringUtility\getUniqueId
‪static string getUniqueId($prefix='')
Definition: StringUtility.php:91
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\setCompressionLevel
‪setCompressionLevel($compressionLevel)
Definition: RedisBackend.php:250
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend
Definition: AbstractBackend.php:26
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\$redis
‪Redis $redis
Definition: RedisBackend.php:67
‪TYPO3\CMS\Core\Cache\Backend
Definition: AbstractBackend.php:2
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend\$context
‪string $context
Definition: AbstractBackend.php:48
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\$connected
‪bool $connected
Definition: RedisBackend.php:73
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\setCompression
‪setCompression($compression)
Definition: RedisBackend.php:234
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\canBeUsedInStringContext
‪bool canBeUsedInStringContext($variable)
Definition: RedisBackend.php:540
‪TYPO3\CMS\Core\Utility\StringUtility
Definition: StringUtility.php:21
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\setPort
‪setPort($port)
Definition: RedisBackend.php:196
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\initializeObject
‪initializeObject()
Definition: RedisBackend.php:143
‪TYPO3\CMS\Core\Cache\Exception
Definition: Exception.php:21
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\setConnectionTimeout
‪setConnectionTimeout($connectionTimeout)
Definition: RedisBackend.php:270
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\$password
‪string $password
Definition: RedisBackend.php:103