TYPO3 CMS  TYPO3_8-7
MemcachedBackend.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 
48 {
54  const MAX_BUCKET_SIZE = 1048534;
55 
61  protected $memcache;
62 
68  protected $usedPeclModule = '';
69 
75  protected $servers = [];
76 
83  protected $flags;
84 
90  protected $identifierPrefix;
91 
99  public function __construct($context, array $options = [])
100  {
101  if (!extension_loaded('memcache') && !extension_loaded('memcached')) {
102  throw new Exception('The PHP extension "memcache" or "memcached" must be installed and loaded in ' . 'order to use the Memcached backend.', 1213987706);
103  }
104 
105  parent::__construct($context, $options);
106 
107  if ($this->usedPeclModule === '') {
108  if (extension_loaded('memcache')) {
109  $this->usedPeclModule = 'memcache';
110  } elseif (extension_loaded('memcached')) {
111  $this->usedPeclModule = 'memcached';
112  }
113  }
114  }
115 
123  protected function setServers(array $servers)
124  {
125  $this->servers = $servers;
126  }
127 
134  protected function setCompression($useCompression)
135  {
136  $compressionFlag = $this->usedPeclModule === 'memcache' ? MEMCACHE_COMPRESSED : \Memcached::OPT_COMPRESSION;
137  if ($useCompression === true) {
138  $this->flags ^= $compressionFlag;
139  } else {
140  $this->flags &= ~$compressionFlag;
141  }
142  }
143 
150  protected function getCompression()
151  {
152  return $this->flags !== 0;
153  }
154 
160  public function initializeObject()
161  {
162  if (empty($this->servers)) {
163  throw new Exception('No servers were given to Memcache', 1213115903);
164  }
165  $memcachedPlugin = '\\' . ucfirst($this->usedPeclModule);
166  $this->memcache = new $memcachedPlugin;
167  $defaultPort = $this->usedPeclModule === 'memcache' ? ini_get('memcache.default_port') : 11211;
168  foreach ($this->servers as $server) {
169  if (substr($server, 0, 7) === 'unix://') {
170  $host = $server;
171  $port = 0;
172  } else {
173  if (substr($server, 0, 6) === 'tcp://') {
174  $server = substr($server, 6);
175  }
176  if (strpos($server, ':') !== false) {
177  list($host, $port) = explode(':', $server, 2);
178  } else {
179  $host = $server;
180  $port = $defaultPort;
181  }
182  }
183  $this->memcache->addserver($host, $port);
184  }
185  if ($this->usedPeclModule === 'memcached') {
186  $this->memcache->setOption(\Memcached::OPT_COMPRESSION, $this->getCompression());
187  }
188  }
189 
196  public function setPeclModule($peclModule)
197  {
198  if ($peclModule !== 'memcache' && $peclModule !== 'memcached') {
199  throw new Exception('PECL module must be either "memcache" or "memcached".', 1442239768);
200  }
201 
202  $this->usedPeclModule = $peclModule;
203  }
204 
211  {
212  parent::setCache($cache);
213  $identifierHash = substr(md5(PATH_site . $this->context . $this->cacheIdentifier), 0, 12);
214  $this->identifierPrefix = 'TYPO3_' . $identifierHash . '_';
215  }
216 
229  public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
230  {
231  if (strlen($this->identifierPrefix . $entryIdentifier) > 250) {
232  throw new \InvalidArgumentException('Could not set value. Key more than 250 characters (' . $this->identifierPrefix . $entryIdentifier . ').', 1232969508);
233  }
234  if (!$this->cache instanceof FrontendInterface) {
235  throw new Exception('No cache frontend has been set yet via setCache().', 1207149215);
236  }
237  if (!is_string($data)) {
238  throw new Exception\InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1207149231);
239  }
240  $tags[] = '%MEMCACHEBE%' . $this->cacheIdentifier;
241  $expiration = $lifetime !== null ? $lifetime : $this->defaultLifetime;
242  $memcacheIsUsed = $this->usedPeclModule === 'memcache';
243  // Memcached consideres values over 2592000 sec (30 days) as UNIX timestamp
244  // thus $expiration should be converted from lifetime to UNIX timestamp
245  if ($expiration > 2592000) {
246  $expiration += $GLOBALS['EXEC_TIME'];
247  }
248  try {
249  if (strlen($data) > self::MAX_BUCKET_SIZE) {
250  $data = str_split($data, 1024 * 1000);
251  $success = true;
252  $chunkNumber = 1;
253  foreach ($data as $chunk) {
254  if ($memcacheIsUsed) {
255  $success = $success && $this->memcache->set($this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber, $chunk, $this->flags, $expiration);
256  } else {
257  $success = $success && $this->memcache->set($this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber, $chunk, $expiration);
258  }
259 
260  $chunkNumber++;
261  }
262  if ($memcacheIsUsed) {
263  $success = $success && $this->memcache->set($this->identifierPrefix . $entryIdentifier, 'TYPO3*chunked:' . $chunkNumber, $this->flags, $expiration);
264  } else {
265  $success = $success && $this->memcache->set($this->identifierPrefix . $entryIdentifier, 'TYPO3*chunked:' . $chunkNumber, $expiration);
266  }
267  } else {
268  if ($memcacheIsUsed) {
269  $success = $this->memcache->set($this->identifierPrefix . $entryIdentifier, $data, $this->flags, $expiration);
270  } else {
271  $success = $this->memcache->set($this->identifierPrefix . $entryIdentifier, $data, $expiration);
272  }
273  }
274  if ($success === true) {
275  $this->removeIdentifierFromAllTags($entryIdentifier);
276  $this->addIdentifierToTags($entryIdentifier, $tags);
277  } else {
278  throw new Exception('Could not set data to memcache server.', 1275830266);
279  }
280  } catch (\Exception $exception) {
281  GeneralUtility::sysLog('Memcache: could not set value. Reason: ' . $exception->getMessage(), 'core', GeneralUtility::SYSLOG_SEVERITY_WARNING);
282  }
283  }
284 
292  public function get($entryIdentifier)
293  {
294  $value = $this->memcache->get($this->identifierPrefix . $entryIdentifier);
295  if (substr($value, 0, 14) === 'TYPO3*chunked:') {
296  list(, $chunkCount) = explode(':', $value);
297  $value = '';
298  for ($chunkNumber = 1; $chunkNumber < $chunkCount; $chunkNumber++) {
299  $value .= $this->memcache->get($this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber);
300  }
301  }
302  return $value;
303  }
304 
312  public function has($entryIdentifier)
313  {
314  if ($this->usedPeclModule === 'memcache') {
315  return $this->memcache->get($this->identifierPrefix . $entryIdentifier) !== false;
316  }
317 
318  // pecl-memcached supports storing literal FALSE
319  $this->memcache->get($this->identifierPrefix . $entryIdentifier);
320  return $this->memcache->getResultCode() !== \Memcached::RES_NOTFOUND;
321  }
322 
332  public function remove($entryIdentifier)
333  {
334  $this->removeIdentifierFromAllTags($entryIdentifier);
335  return $this->memcache->delete($this->identifierPrefix . $entryIdentifier, 0);
336  }
337 
346  public function findIdentifiersByTag($tag)
347  {
348  $identifiers = $this->memcache->get($this->identifierPrefix . 'tag_' . $tag);
349  if ($identifiers !== false) {
350  return (array)$identifiers;
351  }
352  return [];
353  }
354 
361  public function flush()
362  {
363  if (!$this->cache instanceof FrontendInterface) {
364  throw new Exception('No cache frontend has been set via setCache() yet.', 1204111376);
365  }
366  $this->flushByTag('%MEMCACHEBE%' . $this->cacheIdentifier);
367  }
368 
375  public function flushByTag($tag)
376  {
377  $identifiers = $this->findIdentifiersByTag($tag);
378  foreach ($identifiers as $identifier) {
379  $this->remove($identifier);
380  }
381  }
382 
389  protected function addIdentifierToTags($entryIdentifier, array $tags)
390  {
391  // Get identifier-to-tag index to look for updates
392  $existingTags = $this->findTagsByIdentifier($entryIdentifier);
393  $existingTagsUpdated = false;
394 
395  foreach ($tags as $tag) {
396  // Update tag-to-identifier index
397  $identifiers = $this->findIdentifiersByTag($tag);
398  if (!in_array($entryIdentifier, $identifiers, true)) {
399  $identifiers[] = $entryIdentifier;
400  $this->memcache->set($this->identifierPrefix . 'tag_' . $tag, $identifiers);
401  }
402  // Test if identifier-to-tag index needs update
403  if (!in_array($tag, $existingTags, true)) {
404  $existingTags[] = $tag;
405  $existingTagsUpdated = true;
406  }
407  }
408 
409  // Update identifier-to-tag index if needed
410  if ($existingTagsUpdated) {
411  $this->memcache->set($this->identifierPrefix . 'ident_' . $entryIdentifier, $existingTags);
412  }
413  }
414 
420  protected function removeIdentifierFromAllTags($entryIdentifier)
421  {
422  // Get tags for this identifier
423  $tags = $this->findTagsByIdentifier($entryIdentifier);
424  // De-associate tags with this identifier
425  foreach ($tags as $tag) {
426  $identifiers = $this->findIdentifiersByTag($tag);
427  // Formally array_search() below should never return FALSE due to
428  // the behavior of findTagsByIdentifier(). But if reverse index is
429  // corrupted, we still can get 'FALSE' from array_search(). This is
430  // not a problem because we are removing this identifier from
431  // anywhere.
432  if (($key = array_search($entryIdentifier, $identifiers)) !== false) {
433  unset($identifiers[$key]);
434  if (!empty($identifiers)) {
435  $this->memcache->set($this->identifierPrefix . 'tag_' . $tag, $identifiers);
436  } else {
437  $this->memcache->delete($this->identifierPrefix . 'tag_' . $tag, 0);
438  }
439  }
440  }
441  // Clear reverse tag index for this identifier
442  $this->memcache->delete($this->identifierPrefix . 'ident_' . $entryIdentifier, 0);
443  }
444 
453  protected function findTagsByIdentifier($identifier)
454  {
455  $tags = $this->memcache->get($this->identifierPrefix . 'ident_' . $identifier);
456  return $tags === false ? [] : (array)$tags;
457  }
458 
464  public function collectGarbage()
465  {
466  }
467 }
addIdentifierToTags($entryIdentifier, array $tags)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']