TYPO3 CMS  TYPO3_6-2
RedisBackend.php
Go to the documentation of this file.
1 <?php
3 
28 
42  const FAKED_UNLIMITED_LIFETIME = 31536000;
48  const IDENTIFIER_DATA_PREFIX = 'identData:';
54  const IDENTIFIER_TAGS_PREFIX = 'identTags:';
60  const TAG_IDENTIFIERS_PREFIX = 'tagIdents:';
66  protected $redis;
67 
73  protected $connected = FALSE;
74 
80  protected $hostname = '127.0.0.1';
81 
87  protected $port = 6379;
88 
94  protected $database = 0;
95 
101  protected $password = '';
102 
108  protected $compression = FALSE;
109 
115  protected $compressionLevel = -1;
116 
124  public function __construct($context, array $options = array()) {
125  if (!extension_loaded('redis')) {
126  throw new \TYPO3\CMS\Core\Cache\Exception('The PHP extension "redis" must be installed and loaded in order to use the redis backend.', 1279462933);
127  }
128  parent::__construct($context, $options);
129  }
130 
137  public function initializeObject() {
138  $this->redis = new \Redis();
139  try {
140  $this->connected = $this->redis->connect($this->hostname, $this->port);
141  } catch (\Exception $e) {
142  \TYPO3\CMS\Core\Utility\GeneralUtility::sysLog('Could not connect to redis server.', 'core', \TYPO3\CMS\Core\Utility\GeneralUtility::SYSLOG_SEVERITY_ERROR);
143  }
144  if ($this->connected) {
145  if (strlen($this->password)) {
146  $success = $this->redis->auth($this->password);
147  if (!$success) {
148  throw new \TYPO3\CMS\Core\Cache\Exception('The given password was not accepted by the redis server.', 1279765134);
149  }
150  }
151  if ($this->database > 0) {
152  $success = $this->redis->select($this->database);
153  if (!$success) {
154  throw new \TYPO3\CMS\Core\Cache\Exception('The given database "' . $this->database . '" could not be selected.', 1279765144);
155  }
156  }
157  }
158  }
159 
167  public function setHostname($hostname) {
168  $this->hostname = $hostname;
169  }
170 
178  public function setPort($port) {
179  $this->port = $port;
180  }
181 
190  public function setDatabase($database) {
191  if (!is_integer($database)) {
192  throw new \InvalidArgumentException('The specified database number is of type "' . gettype($database) . '" but an integer is expected.', 1279763057);
193  }
194  if ($database < 0) {
195  throw new \InvalidArgumentException('The specified database "' . $database . '" must be greater or equal than zero.', 1279763534);
196  }
197  $this->database = $database;
198  }
199 
207  public function setPassword($password) {
208  $this->password = $password;
209  }
210 
219  public function setCompression($compression) {
220  if (!is_bool($compression)) {
221  throw new \InvalidArgumentException('The specified compression of type "' . gettype($compression) . '" but a boolean is expected.', 1289679153);
222  }
223  $this->compression = $compression;
224  }
225 
237  if (!is_integer($compressionLevel)) {
238  throw new \InvalidArgumentException('The specified compression of type "' . gettype($compressionLevel) . '" but an integer is expected.', 1289679154);
239  }
240  if ($compressionLevel >= -1 && $compressionLevel <= 9) {
241  $this->compressionLevel = $compressionLevel;
242  } else {
243  throw new \InvalidArgumentException('The specified compression level must be an integer between -1 and 9.', 1289679155);
244  }
245  }
246 
262  public function set($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
263  if (!is_string($entryIdentifier)) {
264  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" but a string is expected.', 1279470252);
265  }
266  if (!is_string($data)) {
267  throw new \TYPO3\CMS\Core\Cache\Exception\InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1279469941);
268  }
269  $lifetime = $lifetime === NULL ? $this->defaultLifetime : $lifetime;
270  if (!is_integer($lifetime)) {
271  throw new \InvalidArgumentException('The specified lifetime is of type "' . gettype($lifetime) . '" but an integer or NULL is expected.', 1279488008);
272  }
273  if ($lifetime < 0) {
274  throw new \InvalidArgumentException('The specified lifetime "' . $lifetime . '" must be greater or equal than zero.', 1279487573);
275  }
276  if ($this->connected) {
277  $expiration = $lifetime === 0 ? self::FAKED_UNLIMITED_LIFETIME : $lifetime;
278  if ($this->compression) {
279  $data = gzcompress($data, $this->compressionLevel);
280  }
281  $this->redis->setex(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier, $expiration, $data);
282  $addTags = $tags;
283  $removeTags = array();
284  $existingTags = $this->redis->sMembers(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
285  if (!empty($existingTags)) {
286  $addTags = array_diff($tags, $existingTags);
287  $removeTags = array_diff($existingTags, $tags);
288  }
289  if (count($removeTags) > 0 || count($addTags) > 0) {
290  $queue = $this->redis->multi(\Redis::PIPELINE);
291  foreach ($removeTags as $tag) {
292  $queue->sRemove(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier, $tag);
293  $queue->sRemove(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
294  }
295  foreach ($addTags as $tag) {
296  $queue->sAdd(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier, $tag);
297  $queue->sAdd(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
298  }
299  $queue->exec();
300  }
301  }
302  }
303 
314  public function get($entryIdentifier) {
315  if (!is_string($entryIdentifier)) {
316  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" but a string is expected.', 1279470253);
317  }
318  $storedEntry = FALSE;
319  if ($this->connected) {
320  $storedEntry = $this->redis->get(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier);
321  }
322  if ($this->compression && strlen($storedEntry) > 0) {
323  $storedEntry = gzuncompress($storedEntry);
324  }
325  return $storedEntry;
326  }
327 
338  public function has($entryIdentifier) {
339  if (!is_string($entryIdentifier)) {
340  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" but a string is expected.', 1279470254);
341  }
342  return $this->connected && $this->redis->exists(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier);
343  }
344 
356  public function remove($entryIdentifier) {
357  if (!is_string($entryIdentifier)) {
358  throw new \InvalidArgumentException('The specified identifier is of type "' . gettype($entryIdentifier) . '" but a string is expected.', 1279470255);
359  }
360  $elementsDeleted = FALSE;
361  if ($this->connected) {
362  if ($this->redis->exists(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier)) {
363  $assignedTags = $this->redis->sMembers(self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
364  $queue = $this->redis->multi(\Redis::PIPELINE);
365  foreach ($assignedTags as $tag) {
366  $queue->sRemove(self::TAG_IDENTIFIERS_PREFIX . $tag, $entryIdentifier);
367  }
368  $queue->delete(self::IDENTIFIER_DATA_PREFIX . $entryIdentifier, self::IDENTIFIER_TAGS_PREFIX . $entryIdentifier);
369  $queue->exec();
370  $elementsDeleted = TRUE;
371  }
372  }
373  return $elementsDeleted;
374  }
375 
388  public function findIdentifiersByTag($tag) {
389  if (!is_string($tag)) {
390  throw new \InvalidArgumentException('The specified tag is of type "' . gettype($tag) . '" but a string is expected.', 1279569759);
391  }
392  $foundIdentifiers = array();
393  if ($this->connected) {
394  $foundIdentifiers = $this->redis->sMembers(self::TAG_IDENTIFIERS_PREFIX . $tag);
395  }
396  return $foundIdentifiers;
397  }
398 
407  public function flush() {
408  if ($this->connected) {
409  $this->redis->flushdb();
410  }
411  }
412 
424  public function flushByTag($tag) {
425  if (!is_string($tag)) {
426  throw new \InvalidArgumentException('The specified tag is of type "' . gettype($tag) . '" but a string is expected.', 1279578078);
427  }
428  if ($this->connected) {
429  $identifiers = $this->redis->sMembers(self::TAG_IDENTIFIERS_PREFIX . $tag);
430  if (count($identifiers) > 0) {
431  $this->removeIdentifierEntriesAndRelations($identifiers, array($tag));
432  }
433  }
434  }
435 
447  public function collectGarbage() {
448  $identifierToTagsKeys = $this->redis->getKeys(self::IDENTIFIER_TAGS_PREFIX . '*');
449  foreach ($identifierToTagsKeys as $identifierToTagsKey) {
450  list(, $identifier) = explode(':', $identifierToTagsKey);
451  // Check if the data entry still exists
452  if (!$this->redis->exists((self::IDENTIFIER_DATA_PREFIX . $identifier))) {
453  $tagsToRemoveIdentifierFrom = $this->redis->sMembers($identifierToTagsKey);
454  $queue = $this->redis->multi(\Redis::PIPELINE);
455  $queue->delete($identifierToTagsKey);
456  foreach ($tagsToRemoveIdentifierFrom as $tag) {
457  $queue->sRemove(self::TAG_IDENTIFIERS_PREFIX . $tag, $identifier);
458  }
459  $queue->exec();
460  }
461  }
462  }
463 
475  protected function removeIdentifierEntriesAndRelations(array $identifiers, array $tags) {
476  // Set a temporary entry which holds all identifiers that need to be removed from
477  // the tag to identifiers sets
478  $uniqueTempKey = 'temp:' . uniqid('', TRUE);
479  $prefixedKeysToDelete = array($uniqueTempKey);
480  $prefixedIdentifierToTagsKeysToDelete = array();
481  foreach ($identifiers as $identifier) {
482  $prefixedKeysToDelete[] = self::IDENTIFIER_DATA_PREFIX . $identifier;
483  $prefixedIdentifierToTagsKeysToDelete[] = self::IDENTIFIER_TAGS_PREFIX . $identifier;
484  }
485  foreach ($tags as $tag) {
486  $prefixedKeysToDelete[] = self::TAG_IDENTIFIERS_PREFIX . $tag;
487  }
488  $tagToIdentifiersSetsToRemoveIdentifiersFrom = $this->redis->sUnion($prefixedIdentifierToTagsKeysToDelete);
489  // Remove the tag to identifier set of the given tags, they will be removed anyway
490  $tagToIdentifiersSetsToRemoveIdentifiersFrom = array_diff($tagToIdentifiersSetsToRemoveIdentifiersFrom, $tags);
491  // Diff all identifiers that must be removed from tag to identifiers sets off from a
492  // tag to identifiers set and store result in same tag to identifiers set again
493  $queue = $this->redis->multi(\Redis::PIPELINE);
494  foreach ($identifiers as $identifier) {
495  $queue->sAdd($uniqueTempKey, $identifier);
496  }
497  foreach ($tagToIdentifiersSetsToRemoveIdentifiersFrom as $tagToIdentifiersSet) {
498  $queue->sDiffStore(self::TAG_IDENTIFIERS_PREFIX . $tagToIdentifiersSet, self::TAG_IDENTIFIERS_PREFIX . $tagToIdentifiersSet, $uniqueTempKey);
499  }
500  $queue->delete(array_merge($prefixedKeysToDelete, $prefixedIdentifierToTagsKeysToDelete));
501  $queue->exec();
502  }
503 
504 }
removeIdentifierEntriesAndRelations(array $identifiers, array $tags)
__construct($context, array $options=array())