TYPO3 CMS  TYPO3_7-6
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 
132  public function __construct($context, array $options = [])
133  {
134  if (!extension_loaded('redis')) {
135  throw new \TYPO3\CMS\Core\Cache\Exception('The PHP extension "redis" must be installed and loaded in order to use the redis backend.', 1279462933);
136  }
137  parent::__construct($context, $options);
138  }
139 
146  public function initializeObject()
147  {
148  $this->redis = new \Redis();
149  try {
150  if ($this->persistentConnection) {
151  $this->connected = $this->redis->pconnect($this->hostname, $this->port);
152  } else {
153  $this->connected = $this->redis->connect($this->hostname, $this->port);
154  }
155  } catch (\Exception $e) {
156  \TYPO3\CMS\Core\Utility\GeneralUtility::sysLog('Could not connect to redis server.', 'core', \TYPO3\CMS\Core\Utility\GeneralUtility::SYSLOG_SEVERITY_ERROR);
157  }
158  if ($this->connected) {
159  if ($this->password !== '') {
160  $success = $this->redis->auth($this->password);
161  if (!$success) {
162  throw new \TYPO3\CMS\Core\Cache\Exception('The given password was not accepted by the redis server.', 1279765134);
163  }
164  }
165  if ($this->database > 0) {
166  $success = $this->redis->select($this->database);
167  if (!$success) {
168  throw new \TYPO3\CMS\Core\Cache\Exception('The given database "' . $this->database . '" could not be selected.', 1279765144);
169  }
170  }
171  }
172  }
173 
182  {
183  $this->persistentConnection = $persistentConnection;
184  }
185 
193  public function setHostname($hostname)
194  {
195  $this->hostname = $hostname;
196  }
197 
205  public function setPort($port)
206  {
207  $this->port = $port;
208  }
209 
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 
236  public function setPassword($password)
237  {
238  $this->password = $password;
239  }
240 
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 
268  {
269  if (!is_int($compressionLevel)) {
270  throw new \InvalidArgumentException('The specified compression of type "' . gettype($compressionLevel) . '" but an integer is expected.', 1289679154);
271  }
272  if ($compressionLevel >= -1 && $compressionLevel <= 9) {
273  $this->compressionLevel = $compressionLevel;
274  } else {
275  throw new \InvalidArgumentException('The specified compression level must be an integer between -1 and 9.', 1289679155);
276  }
277  }
278 
294  public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
295  {
296  if (!$this->canBeUsedInStringContext($entryIdentifier)) {
297  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006651);
298  }
299  if (!is_string($data)) {
300  throw new \TYPO3\CMS\Core\Cache\Exception\InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1279469941);
301  }
302  $lifetime = $lifetime === null ? $this->defaultLifetime : $lifetime;
303  if (!is_int($lifetime)) {
304  throw new \InvalidArgumentException('The specified lifetime is of type "' . gettype($lifetime) . '" but an integer or NULL is expected.', 1279488008);
305  }
306  if ($lifetime < 0) {
307  throw new \InvalidArgumentException('The specified lifetime "' . $lifetime . '" must be greater or equal than zero.', 1279487573);
308  }
309  if ($this->connected) {
310  $expiration = $lifetime === 0 ? self::FAKED_UNLIMITED_LIFETIME : $lifetime;
311  if ($this->compression) {
312  $data = gzcompress($data, $this->compressionLevel);
313  }
314  $this->redis->setex(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier, $expiration, $data);
315  $addTags = $tags;
316  $removeTags = [];
317  $existingTags = $this->redis->sMembers(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
318  if (!empty($existingTags)) {
319  $addTags = array_diff($tags, $existingTags);
320  $removeTags = array_diff($existingTags, $tags);
321  }
322  if (!empty($removeTags) || !empty($addTags)) {
323  $queue = $this->redis->multi(\Redis::PIPELINE);
324  foreach ($removeTags as $tag) {
325  $queue->sRemove(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier, $tag);
326  $queue->sRemove(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
327  }
328  foreach ($addTags as $tag) {
329  $queue->sAdd(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier, $tag);
330  $queue->sAdd(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
331  }
332  $queue->exec();
333  }
334  }
335  }
336 
347  public function get($entryIdentifier)
348  {
349  if (!$this->canBeUsedInStringContext($entryIdentifier)) {
350  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006652);
351  }
352  $storedEntry = false;
353  if ($this->connected) {
354  $storedEntry = $this->redis->get(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier);
355  }
356  if ($this->compression && (string)$storedEntry !== '') {
357  $storedEntry = gzuncompress($storedEntry);
358  }
359  return $storedEntry;
360  }
361 
372  public function has($entryIdentifier)
373  {
374  if (!$this->canBeUsedInStringContext($entryIdentifier)) {
375  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006653);
376  }
377  return $this->connected && $this->redis->exists(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier);
378  }
379 
391  public function remove($entryIdentifier)
392  {
393  if (!$this->canBeUsedInStringContext($entryIdentifier)) {
394  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" which can\'t be converted to string.', 1377006654);
395  }
396  $elementsDeleted = false;
397  if ($this->connected) {
398  if ($this->redis->exists(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier)) {
399  $assignedTags = $this->redis->sMembers(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
400  $queue = $this->redis->multi(\Redis::PIPELINE);
401  foreach ($assignedTags as $tag) {
402  $queue->sRemove(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
403  }
404  $queue->delete(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier, self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
405  $queue->exec();
406  $elementsDeleted = true;
407  }
408  }
409  return $elementsDeleted;
410  }
411 
424  public function findIdentifiersByTag($tag)
425  {
426  if (!$this->canBeUsedInStringContext($tag)) {
427  throw new \InvalidArgumentException('The specified tag is of type "' . gettype($tag) . '" which can\'t be converted to string.', 1377006655);
428  }
429  $foundIdentifiers = [];
430  if ($this->connected) {
431  $foundIdentifiers = $this->redis->sMembers(self::TAG_IDENTIFIERS_PREFIX . $tag);
432  }
433  return $foundIdentifiers;
434  }
435 
444  public function flush()
445  {
446  if ($this->connected) {
447  $this->redis->flushdb();
448  }
449  }
450 
462  public function flushByTag($tag)
463  {
464  if (!$this->canBeUsedInStringContext($tag)) {
465  throw new \InvalidArgumentException('The specified tag is of type "' . gettype($tag) . '" which can\'t be converted to string.', 1377006656);
466  }
467  if ($this->connected) {
468  $identifiers = $this->redis->sMembers(self::TAG_IDENTIFIERS_PREFIX . $tag);
469  if (!empty($identifiers)) {
470  $this->removeIdentifierEntriesAndRelations($identifiers, [$tag]);
471  }
472  }
473  }
474 
486  public function collectGarbage()
487  {
488  $identifierToTagsKeys = $this->redis->getKeys(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->delete($identifierToTagsKey);
496  foreach ($tagsToRemoveIdentifierFrom as $tag) {
497  $queue->sRemove(self::TAG_IDENTIFIERS_PREFIX . $tag, $identifier);
498  }
499  $queue->exec();
500  }
501  }
502  }
503 
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->delete(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 }
removeIdentifierEntriesAndRelations(array $identifiers, array $tags)
setPersistentConnection($persistentConnection)
__construct($context, array $options=[])