‪TYPO3CMS  ‪main
RedisBackend.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  * This file is part of the TYPO3 CMS project.
5  *
6  * It is free software; you can redistribute it and/or modify it under
7  * the terms of the GNU General Public License, either version 2
8  * of the License, or any later version.
9  *
10  * For the full copyright and license information, please read the
11  * LICENSE.txt file that was distributed with this source code.
12  *
13  * The TYPO3 project - inspiring people to share!
14  */
15 
17 
21 
31 {
45  public const ‪FAKED_UNLIMITED_LIFETIME = 31536000;
51  public const ‪IDENTIFIER_DATA_PREFIX = 'identData:';
57  public const ‪IDENTIFIER_TAGS_PREFIX = 'identTags:';
63  public const ‪TAG_IDENTIFIERS_PREFIX = 'tagIdents:';
69  protected ‪$redis;
70 
76  protected ‪$connected = false;
77 
83  protected ‪$persistentConnection = false;
84 
90  protected ‪$hostname = '127.0.0.1';
91 
97  protected ‪$port = 6379;
98 
104  protected ‪$database = 0;
105 
111  protected ‪$password = '';
112 
118  protected ‪$compression = false;
119 
125  protected ‪$compressionLevel = -1;
126 
132  protected ‪$connectionTimeout = 0;
133 
141  public function ‪__construct(‪$context, array $options = [])
142  {
143  if (!extension_loaded('redis')) {
144  throw new ‪Exception('The PHP extension "redis" must be installed and loaded in order to use the redis backend.', 1279462933);
145  }
146  parent::__construct(‪$context, $options);
147  }
148 
154  public function ‪initializeObject()
155  {
156  $this->redis = new \Redis();
157  try {
158  if ($this->persistentConnection) {
159  $this->connected = $this->redis->pconnect($this->hostname, $this->port, $this->connectionTimeout, (string)$this->database);
160  } else {
161  $this->connected = $this->redis->connect($this->hostname, $this->port, $this->connectionTimeout);
162  }
163  } catch (\Exception $e) {
164  $this->logger->alert('Could not connect to redis server.', ['exception' => $e]);
165  }
166  if ($this->connected) {
167  if ($this->password !== '') {
168  $success = $this->redis->auth($this->password);
169  if (!$success) {
170  throw new Exception('The given password was not accepted by the redis server.', 1279765134);
171  }
172  }
173  if ($this->database >= 0) {
174  $success = $this->redis->select($this->database);
175  if (!$success) {
176  throw new Exception('The given database "' . $this->database . '" could not be selected.', 1279765144);
177  }
178  }
179  }
180  }
181 
188  {
189  $this->persistentConnection = ‪$persistentConnection;
190  }
191 
197  public function ‪setHostname(‪$hostname)
198  {
199  $this->hostname = ‪$hostname;
200  }
201 
207  public function ‪setPort(‪$port)
208  {
209  $this->port = ‪$port;
210  }
211 
218  public function ‪setDatabase(‪$database)
219  {
220  if (!is_int(‪$database)) {
221  throw new \InvalidArgumentException('The specified database number is of type "' . gettype(‪$database) . '" but an integer is expected.', 1279763057);
222  }
223  if (‪$database < 0) {
224  throw new \InvalidArgumentException('The specified database "' . ‪$database . '" must be greater or equal than zero.', 1279763534);
225  }
226  $this->database = ‪$database;
227  }
228 
234  public function ‪setPassword(‪$password)
235  {
236  $this->password = ‪$password;
237  }
238 
245  public function ‪setCompression(‪$compression)
246  {
247  if (!is_bool(‪$compression)) {
248  throw new \InvalidArgumentException('The specified compression of type "' . gettype(‪$compression) . '" but a boolean is expected.', 1289679153);
249  }
250  $this->compression = ‪$compression;
251  }
252 
262  {
263  if (!is_int(‪$compressionLevel)) {
264  throw new \InvalidArgumentException('The specified compression of type "' . gettype(‪$compressionLevel) . '" but an integer is expected.', 1289679154);
265  }
266  if (‪$compressionLevel >= -1 && ‪$compressionLevel <= 9) {
267  $this->compressionLevel = ‪$compressionLevel;
268  } else {
269  throw new \InvalidArgumentException('The specified compression level must be an integer between -1 and 9.', 1289679155);
270  }
271  }
272 
282  {
283  if (!is_int(‪$connectionTimeout)) {
284  throw new \InvalidArgumentException('The specified connection timeout is of type "' . gettype(‪$connectionTimeout) . '" but an integer is expected.', 1487849315);
285  }
286 
287  if (‪$connectionTimeout < 0) {
288  throw new \InvalidArgumentException('The specified connection timeout "' . ‪$connectionTimeout . '" must be greater or equal than zero.', 1487849326);
289  }
290 
291  $this->connectionTimeout = ‪$connectionTimeout;
292  }
293 
307  public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
308  {
309  if (!$this->‪canBeUsedInStringContext($entryIdentifier)) {
310  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006651);
311  }
312  if (!is_string($data)) {
313  throw new InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1279469941);
314  }
315  $lifetime = $lifetime ?? ‪$this->defaultLifetime;
316  if (!is_int($lifetime)) {
317  throw new \InvalidArgumentException('The specified lifetime is of type "' . gettype($lifetime) . '" but an integer or NULL is expected.', 1279488008);
318  }
319  if ($lifetime < 0) {
320  throw new \InvalidArgumentException('The specified lifetime "' . $lifetime . '" must be greater or equal than zero.', 1279487573);
321  }
322  if ($this->connected) {
323  $expiration = $lifetime === 0 ? ‪self::FAKED_UNLIMITED_LIFETIME : $lifetime;
324  if ($this->compression) {
325  $data = gzcompress($data, $this->compressionLevel);
326  }
327  $this->redis->setex(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier, $expiration, $data);
328  $addTags = $tags;
329  $removeTags = [];
330  $existingTags = $this->redis->sMembers(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
331  if (!empty($existingTags)) {
332  $addTags = array_diff($tags, $existingTags);
333  $removeTags = array_diff($existingTags, $tags);
334  }
335  if (!empty($removeTags) || !empty($addTags)) {
336  $queue = $this->redis->multi(\Redis::PIPELINE);
337  foreach ($removeTags as $tag) {
338  $queue->sRem(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier, $tag);
339  $queue->sRem(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
340  }
341  foreach ($addTags as $tag) {
342  $queue->sAdd(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier, $tag);
343  $queue->sAdd(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
344  }
345  $queue->exec();
346  }
347  }
348  }
349 
359  public function get($entryIdentifier)
360  {
361  if (!$this->‪canBeUsedInStringContext($entryIdentifier)) {
362  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006652);
363  }
364  $storedEntry = false;
365  if ($this->connected) {
366  $storedEntry = $this->redis->get(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier);
367  }
368  if ($this->compression && (string)$storedEntry !== '') {
369  $storedEntry = gzuncompress((string)$storedEntry);
370  }
371  return $storedEntry;
372  }
373 
383  public function ‪has($entryIdentifier)
384  {
385  if (!$this->‪canBeUsedInStringContext($entryIdentifier)) {
386  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006653);
387  }
388  return $this->connected && $this->redis->exists(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier);
389  }
390 
401  public function remove($entryIdentifier)
402  {
403  if (!$this->‪canBeUsedInStringContext($entryIdentifier)) {
404  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006654);
405  }
406  $elementsDeleted = false;
407  if ($this->connected) {
408  if ($this->redis->exists(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier)) {
409  $assignedTags = $this->redis->sMembers(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
410  $queue = $this->redis->multi(\Redis::PIPELINE);
411  foreach ($assignedTags as $tag) {
412  $queue->sRem(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
413  }
414  $queue->del(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier, self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
415  $queue->exec();
416  $elementsDeleted = true;
417  }
418  }
419  return $elementsDeleted;
420  }
421 
433  public function ‪findIdentifiersByTag($tag)
434  {
435  if (!$this->‪canBeUsedInStringContext($tag)) {
436  throw new \InvalidArgumentException('The specified tag is of type "' . gettype($tag) . '" which can\'t be converted to string.', 1377006655);
437  }
438  $foundIdentifiers = [];
439  if ($this->connected) {
440  $foundIdentifiers = $this->redis->sMembers(self::TAG_IDENTIFIERS_PREFIX . $tag);
441  }
442  return $foundIdentifiers;
443  }
444 
450  public function ‪flush()
451  {
452  if ($this->connected) {
453  $this->redis->flushDB();
454  }
455  }
456 
466  public function ‪flushByTag($tag)
467  {
468  if (!$this->‪canBeUsedInStringContext($tag)) {
469  throw new \InvalidArgumentException('The specified tag is of type "' . gettype($tag) . '" which can\'t be converted to string.', 1377006656);
470  }
471  if ($this->connected) {
472  $identifiers = $this->redis->sMembers(self::TAG_IDENTIFIERS_PREFIX . $tag);
473  if (!empty($identifiers)) {
474  $this->‪removeIdentifierEntriesAndRelations($identifiers, [$tag]);
475  }
476  }
477  }
478 
487  public function ‪collectGarbage()
488  {
489  $identifierToTagsKeys = $this->redis->keys(self::IDENTIFIER_TAGS_PREFIX . '*');
490  foreach ($identifierToTagsKeys as $identifierToTagsKey) {
491  [, ‪$identifier] = explode(':', $identifierToTagsKey);
492  // Check if the data entry still exists
493  if (!$this->redis->exists(self::IDENTIFIER_DATA_PREFIX . ‪$identifier)) {
494  $tagsToRemoveIdentifierFrom = $this->redis->sMembers($identifierToTagsKey);
495  $queue = $this->redis->multi(\Redis::PIPELINE);
496  $queue->del($identifierToTagsKey);
497  foreach ($tagsToRemoveIdentifierFrom as $tag) {
498  $queue->sRem(self::TAG_IDENTIFIERS_PREFIX . $tag, ‪$identifier);
499  }
500  $queue->exec();
501  }
502  }
503  }
504 
515  protected function ‪removeIdentifierEntriesAndRelations(array $identifiers, array $tags)
516  {
517  // Set a temporary entry which holds all identifiers that need to be removed from
518  // the tag to identifiers sets
519  $uniqueTempKey = 'temp:' . ‪StringUtility::getUniqueId();
520  $prefixedKeysToDelete = [$uniqueTempKey];
521  $prefixedIdentifierToTagsKeysToDelete = [];
522  foreach ($identifiers as ‪$identifier) {
523  $prefixedKeysToDelete[] = self::IDENTIFIER_DATA_PREFIX . ‪$identifier;
524  $prefixedIdentifierToTagsKeysToDelete[] = self::IDENTIFIER_TAGS_PREFIX . ‪$identifier;
525  }
526  foreach ($tags as $tag) {
527  $prefixedKeysToDelete[] = self::TAG_IDENTIFIERS_PREFIX . $tag;
528  }
529  $tagToIdentifiersSetsToRemoveIdentifiersFrom = $this->redis->sUnion($prefixedIdentifierToTagsKeysToDelete);
530  // Remove the tag to identifier set of the given tags, they will be removed anyway
531  $tagToIdentifiersSetsToRemoveIdentifiersFrom = array_diff($tagToIdentifiersSetsToRemoveIdentifiersFrom, $tags);
532  // Diff all identifiers that must be removed from tag to identifiers sets off from a
533  // tag to identifiers set and store result in same tag to identifiers set again
534  $queue = $this->redis->multi(\Redis::PIPELINE);
535  foreach ($identifiers as ‪$identifier) {
536  $queue->sAdd($uniqueTempKey, ‪$identifier);
537  }
538  foreach ($tagToIdentifiersSetsToRemoveIdentifiersFrom as $tagToIdentifiersSet) {
539  $queue->sDiffStore(self::TAG_IDENTIFIERS_PREFIX . $tagToIdentifiersSet, self::TAG_IDENTIFIERS_PREFIX . $tagToIdentifiersSet, $uniqueTempKey);
540  }
541  $queue->del(array_merge($prefixedKeysToDelete, $prefixedIdentifierToTagsKeysToDelete));
542  $queue->exec();
543  }
544 
551  protected function ‪canBeUsedInStringContext($variable)
552  {
553  return is_scalar($variable) || (is_object($variable) && method_exists($variable, '__toString'));
554  }
555 }
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\setDatabase
‪setDatabase($database)
Definition: RedisBackend.php:208
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\removeIdentifierEntriesAndRelations
‪removeIdentifierEntriesAndRelations(array $identifiers, array $tags)
Definition: RedisBackend.php:505
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\TAG_IDENTIFIERS_PREFIX
‪const TAG_IDENTIFIERS_PREFIX
Definition: RedisBackend.php:63
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\setPersistentConnection
‪setPersistentConnection($persistentConnection)
Definition: RedisBackend.php:177
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\collectGarbage
‪collectGarbage()
Definition: RedisBackend.php:477
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\$connectionTimeout
‪int $connectionTimeout
Definition: RedisBackend.php:122
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\$compression
‪bool $compression
Definition: RedisBackend.php:110
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\setPassword
‪setPassword($password)
Definition: RedisBackend.php:224
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\$hostname
‪string $hostname
Definition: RedisBackend.php:86
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\flush
‪flush()
Definition: RedisBackend.php:440
‪TYPO3\CMS\Core\Cache\Backend\TaggableBackendInterface
Definition: TaggableBackendInterface.php:22
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\FAKED_UNLIMITED_LIFETIME
‪const FAKED_UNLIMITED_LIFETIME
Definition: RedisBackend.php:45
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend\$defaultLifetime
‪int $defaultLifetime
Definition: AbstractBackend.php:57
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\IDENTIFIER_DATA_PREFIX
‪const IDENTIFIER_DATA_PREFIX
Definition: RedisBackend.php:51
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\$compressionLevel
‪int $compressionLevel
Definition: RedisBackend.php:116
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\findIdentifiersByTag
‪array findIdentifiersByTag($tag)
Definition: RedisBackend.php:423
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\$port
‪int $port
Definition: RedisBackend.php:92
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend
Definition: RedisBackend.php:31
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\IDENTIFIER_TAGS_PREFIX
‪const IDENTIFIER_TAGS_PREFIX
Definition: RedisBackend.php:57
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\flushByTag
‪flushByTag($tag)
Definition: RedisBackend.php:456
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\$database
‪int $database
Definition: RedisBackend.php:98
‪TYPO3\CMS\Core\Cache\Exception
Definition: DuplicateIdentifierException.php:16
‪TYPO3\CMS\Core\Cache\Exception\InvalidDataException
Definition: InvalidDataException.php:23
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\has
‪bool has($entryIdentifier)
Definition: RedisBackend.php:373
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\__construct
‪__construct($context, array $options=[])
Definition: RedisBackend.php:131
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\$persistentConnection
‪bool $persistentConnection
Definition: RedisBackend.php:80
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\setHostname
‪setHostname($hostname)
Definition: RedisBackend.php:187
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\setCompressionLevel
‪setCompressionLevel($compressionLevel)
Definition: RedisBackend.php:251
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend
Definition: AbstractBackend.php:28
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\$redis
‪Redis $redis
Definition: RedisBackend.php:68
‪TYPO3\CMS\Core\Cache\Backend
Definition: AbstractBackend.php:16
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend\$context
‪string $context
Definition: AbstractBackend.php:51
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\$connected
‪bool $connected
Definition: RedisBackend.php:74
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\setCompression
‪setCompression($compression)
Definition: RedisBackend.php:235
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\canBeUsedInStringContext
‪bool canBeUsedInStringContext($variable)
Definition: RedisBackend.php:541
‪TYPO3\CMS\Core\Utility\StringUtility
Definition: StringUtility.php:24
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\setPort
‪setPort($port)
Definition: RedisBackend.php:197
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\initializeObject
‪initializeObject()
Definition: RedisBackend.php:144
‪TYPO3\CMS\Webhooks\Message\$identifier
‪identifier readonly string $identifier
Definition: FileAddedMessage.php:37
‪TYPO3\CMS\Core\Utility\StringUtility\getUniqueId
‪static getUniqueId(string $prefix='')
Definition: StringUtility.php:57
‪TYPO3\CMS\Core\Cache\Exception
Definition: Exception.php:21
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\setConnectionTimeout
‪setConnectionTimeout($connectionTimeout)
Definition: RedisBackend.php:271
‪TYPO3\CMS\Core\Cache\Backend\RedisBackend\$password
‪string $password
Definition: RedisBackend.php:104