‪TYPO3CMS  10.4
MemcachedBackend.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 
46 {
52  const ‪MAX_BUCKET_SIZE = 1048534;
53 
59  protected ‪$memcache;
60 
66  protected ‪$usedPeclModule = '';
67 
73  protected ‪$servers = [];
74 
81  protected ‪$flags;
82 
88  protected ‪$identifierPrefix;
89 
97  public function ‪__construct(‪$context, array $options = [])
98  {
99  if (!extension_loaded('memcache') && !extension_loaded('memcached')) {
100  throw new ‪Exception('The PHP extension "memcache" or "memcached" must be installed and loaded in order to use the Memcached backend.', 1213987706);
101  }
102 
103  parent::__construct(‪$context, $options);
104 
105  if ($this->usedPeclModule === '') {
106  if (extension_loaded('memcache')) {
107  $this->usedPeclModule = 'memcache';
108  } elseif (extension_loaded('memcached')) {
109  $this->usedPeclModule = 'memcached';
110  }
111  }
112  }
113 
120  protected function ‪setServers(array ‪$servers)
121  {
122  $this->servers = ‪$servers;
123  }
124 
130  protected function ‪setCompression($useCompression)
131  {
132  $compressionFlag = $this->usedPeclModule === 'memcache' ? MEMCACHE_COMPRESSED : \Memcached::OPT_COMPRESSION;
133  if ($useCompression === true) {
134  $this->flags ^= $compressionFlag;
135  } else {
136  $this->flags &= ~$compressionFlag;
137  }
138  }
139 
145  protected function ‪getCompression()
146  {
147  return $this->flags !== 0;
148  }
149 
155  public function ‪initializeObject()
156  {
157  if (empty($this->servers)) {
158  throw new Exception('No servers were given to Memcache', 1213115903);
159  }
160  $memcachedPlugin = '\\' . ucfirst($this->usedPeclModule);
161  $this->memcache = new $memcachedPlugin();
162  $defaultPort = $this->usedPeclModule === 'memcache' ? ini_get('memcache.default_port') : 11211;
163  foreach ($this->servers as $server) {
164  if (strpos($server, 'unix://') === 0) {
165  $host = $server;
166  $port = 0;
167  } else {
168  if (strpos($server, 'tcp://') === 0) {
169  $server = substr($server, 6);
170  }
171  if (strpos($server, ':') !== false) {
172  [$host, $port] = explode(':', $server, 2);
173  } else {
174  $host = $server;
175  $port = $defaultPort;
176  }
177  }
178  $this->memcache->addserver($host, $port);
179  }
180  if ($this->usedPeclModule === 'memcached') {
181  $this->memcache->setOption(\Memcached::OPT_COMPRESSION, $this->‪getCompression());
182  }
183  }
184 
191  public function ‪setPeclModule($peclModule)
192  {
193  if ($peclModule !== 'memcache' && $peclModule !== 'memcached') {
194  throw new Exception('PECL module must be either "memcache" or "memcached".', 1442239768);
195  }
196 
197  $this->usedPeclModule = $peclModule;
198  }
199 
205  public function ‪setCache(FrontendInterface ‪$cache)
206  {
207  parent::setCache(‪$cache);
208  $identifierHash = substr(md5(‪Environment::getProjectPath() . $this->context . $this->cacheIdentifier), 0, 12);
209  $this->identifierPrefix = 'TYPO3_' . $identifierHash . '_';
210  }
211 
222  public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
223  {
224  if (strlen($this->identifierPrefix . $entryIdentifier) > 250) {
225  throw new \InvalidArgumentException('Could not set value. Key more than 250 characters (' . $this->identifierPrefix . $entryIdentifier . ').', 1232969508);
226  }
227  if (!$this->cache instanceof FrontendInterface) {
228  throw new Exception('No cache frontend has been set yet via setCache().', 1207149215);
229  }
230  $tags[] = '%MEMCACHEBE%' . ‪$this->cacheIdentifier;
231  $expiration = $lifetime ?? ‪$this->defaultLifetime;
232 
233  // Memcached considers values over 2592000 sec (30 days) as UNIX timestamp
234  // thus $expiration should be converted from lifetime to UNIX timestamp
235  if ($expiration > 2592000) {
236  $expiration += ‪$GLOBALS['EXEC_TIME'];
237  }
238  try {
239  if (is_string($data) && strlen($data) > self::MAX_BUCKET_SIZE) {
240  $data = str_split($data, 1024 * 1000);
241  $success = true;
242  $chunkNumber = 1;
243  foreach ($data as $chunk) {
244  $success = $success && $this->‪setInternal($entryIdentifier . '_chunk_' . $chunkNumber, $chunk, $expiration);
245  $chunkNumber++;
246  }
247  $success = $success && $this->‪setInternal($entryIdentifier, 'TYPO3*chunked:' . $chunkNumber, $expiration);
248  } else {
249  $success = $this->‪setInternal($entryIdentifier, $data, $expiration);
250  }
251  if ($success === true) {
252  $this->‪removeIdentifierFromAllTags($entryIdentifier);
253  $this->‪addIdentifierToTags($entryIdentifier, $tags);
254  } else {
255  throw new Exception('Could not set data to memcache server.', 1275830266);
256  }
257  } catch (\Exception $exception) {
258  $this->logger->alert('Memcache: could not set value.', ['exception' => $exception]);
259  }
260  }
261 
270  protected function ‪setInternal($entryIdentifier, $data, $expiration)
271  {
272  if ($this->usedPeclModule === 'memcache') {
273  return $this->memcache->set($this->identifierPrefix . $entryIdentifier, $data, $this->flags, $expiration);
274  }
275  return $this->memcache->set($this->identifierPrefix . $entryIdentifier, $data, $expiration);
276  }
277 
284  public function get($entryIdentifier)
285  {
286  $value = $this->memcache->get($this->identifierPrefix . $entryIdentifier);
287  if (is_string($value) && strpos($value, 'TYPO3*chunked:') === 0) {
288  [, $chunkCount] = explode(':', $value);
289  $value = '';
290  for ($chunkNumber = 1; $chunkNumber < $chunkCount; $chunkNumber++) {
291  $value .= $this->memcache->get($this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber);
292  }
293  }
294  return $value;
295  }
296 
303  public function ‪has($entryIdentifier)
304  {
305  if ($this->usedPeclModule === 'memcache') {
306  return $this->memcache->get($this->identifierPrefix . $entryIdentifier) !== false;
307  }
308 
309  // pecl-memcached supports storing literal FALSE
310  $this->memcache->get($this->identifierPrefix . $entryIdentifier);
311  return $this->memcache->getResultCode() !== \Memcached::RES_NOTFOUND;
312  }
313 
322  public function remove($entryIdentifier)
323  {
324  $this->‪removeIdentifierFromAllTags($entryIdentifier);
325  return $this->memcache->delete($this->identifierPrefix . $entryIdentifier, 0);
326  }
327 
335  public function ‪findIdentifiersByTag($tag)
336  {
337  $identifiers = $this->memcache->get($this->identifierPrefix . 'tag_' . $tag);
338  if ($identifiers !== false) {
339  return (array)$identifiers;
340  }
341  return [];
342  }
343 
349  public function ‪flush()
350  {
351  if (!$this->cache instanceof FrontendInterface) {
352  throw new Exception('No cache frontend has been set via setCache() yet.', 1204111376);
353  }
354  $this->‪flushByTag('%MEMCACHEBE%' . $this->cacheIdentifier);
355  }
356 
362  public function ‪flushByTag($tag)
363  {
364  $identifiers = $this->‪findIdentifiersByTag($tag);
365  foreach ($identifiers as $identifier) {
366  $this->remove($identifier);
367  }
368  }
369 
376  protected function ‪addIdentifierToTags($entryIdentifier, array $tags)
377  {
378  // Get identifier-to-tag index to look for updates
379  $existingTags = $this->‪findTagsByIdentifier($entryIdentifier);
380  $existingTagsUpdated = false;
381 
382  foreach ($tags as $tag) {
383  // Update tag-to-identifier index
384  $identifiers = $this->‪findIdentifiersByTag($tag);
385  if (!in_array($entryIdentifier, $identifiers, true)) {
386  $identifiers[] = $entryIdentifier;
387  $this->memcache->set($this->identifierPrefix . 'tag_' . $tag, $identifiers);
388  }
389  // Test if identifier-to-tag index needs update
390  if (!in_array($tag, $existingTags, true)) {
391  $existingTags[] = $tag;
392  $existingTagsUpdated = true;
393  }
394  }
395 
396  // Update identifier-to-tag index if needed
397  if ($existingTagsUpdated) {
398  $this->memcache->set($this->identifierPrefix . 'ident_' . $entryIdentifier, $existingTags);
399  }
400  }
401 
407  protected function ‪removeIdentifierFromAllTags($entryIdentifier)
408  {
409  // Get tags for this identifier
410  $tags = $this->‪findTagsByIdentifier($entryIdentifier);
411  // De-associate tags with this identifier
412  foreach ($tags as $tag) {
413  $identifiers = $this->‪findIdentifiersByTag($tag);
414  // Formally array_search() below should never return FALSE due to
415  // the behavior of findTagsByIdentifier(). But if reverse index is
416  // corrupted, we still can get 'FALSE' from array_search(). This is
417  // not a problem because we are removing this identifier from
418  // anywhere.
419  if (($key = array_search($entryIdentifier, $identifiers)) !== false) {
420  unset($identifiers[$key]);
421  if (!empty($identifiers)) {
422  $this->memcache->set($this->identifierPrefix . 'tag_' . $tag, $identifiers);
423  } else {
424  $this->memcache->delete($this->identifierPrefix . 'tag_' . $tag, 0);
425  }
426  }
427  }
428  // Clear reverse tag index for this identifier
429  $this->memcache->delete($this->identifierPrefix . 'ident_' . $entryIdentifier, 0);
430  }
431 
439  protected function ‪findTagsByIdentifier($identifier)
440  {
441  $tags = $this->memcache->get($this->identifierPrefix . 'ident_' . $identifier);
442  return $tags === false ? [] : (array)$tags;
443  }
444 
448  public function ‪collectGarbage()
449  {
450  }
451 }
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\flush
‪flush()
Definition: MemcachedBackend.php:344
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\$servers
‪array $servers
Definition: MemcachedBackend.php:70
‪TYPO3\CMS\Core\Cache\Backend\TransientBackendInterface
Definition: TransientBackendInterface.php:33
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\has
‪bool has($entryIdentifier)
Definition: MemcachedBackend.php:298
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\findTagsByIdentifier
‪array findTagsByIdentifier($identifier)
Definition: MemcachedBackend.php:434
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\$memcache
‪Memcache Memcached $memcache
Definition: MemcachedBackend.php:58
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\removeIdentifierFromAllTags
‪removeIdentifierFromAllTags($entryIdentifier)
Definition: MemcachedBackend.php:402
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\findIdentifiersByTag
‪array findIdentifiersByTag($tag)
Definition: MemcachedBackend.php:330
‪TYPO3\CMS\Core\Cache\Backend\TaggableBackendInterface
Definition: TaggableBackendInterface.php:22
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\setCompression
‪setCompression($useCompression)
Definition: MemcachedBackend.php:125
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend\$defaultLifetime
‪int $defaultLifetime
Definition: AbstractBackend.php:56
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\setPeclModule
‪setPeclModule($peclModule)
Definition: MemcachedBackend.php:186
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\MAX_BUCKET_SIZE
‪const MAX_BUCKET_SIZE
Definition: MemcachedBackend.php:52
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\$identifierPrefix
‪string $identifierPrefix
Definition: MemcachedBackend.php:83
‪TYPO3\CMS\Core\Core\Environment\getProjectPath
‪static string getProjectPath()
Definition: Environment.php:169
‪TYPO3\CMS\Core\Cache\Exception
Definition: DuplicateIdentifierException.php:16
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\getCompression
‪bool getCompression()
Definition: MemcachedBackend.php:140
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\setServers
‪setServers(array $servers)
Definition: MemcachedBackend.php:115
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\setCache
‪setCache(FrontendInterface $cache)
Definition: MemcachedBackend.php:200
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\$flags
‪int $flags
Definition: MemcachedBackend.php:77
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend\$cache
‪TYPO3 CMS Core Cache Frontend FrontendInterface $cache
Definition: AbstractBackend.php:37
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\collectGarbage
‪collectGarbage()
Definition: MemcachedBackend.php:443
‪TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
Definition: FrontendInterface.php:22
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\initializeObject
‪initializeObject()
Definition: MemcachedBackend.php:150
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\$usedPeclModule
‪string $usedPeclModule
Definition: MemcachedBackend.php:64
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend\$cacheIdentifier
‪string $cacheIdentifier
Definition: AbstractBackend.php:41
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend
Definition: AbstractBackend.php:28
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:40
‪TYPO3\CMS\Core\Cache\Backend
Definition: AbstractBackend.php:16
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend\$context
‪string $context
Definition: AbstractBackend.php:50
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend
Definition: MemcachedBackend.php:46
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\setInternal
‪bool setInternal($entryIdentifier, $data, $expiration)
Definition: MemcachedBackend.php:265
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\addIdentifierToTags
‪addIdentifierToTags($entryIdentifier, array $tags)
Definition: MemcachedBackend.php:371
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\__construct
‪__construct($context, array $options=[])
Definition: MemcachedBackend.php:92
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\flushByTag
‪flushByTag($tag)
Definition: MemcachedBackend.php:357
‪TYPO3\CMS\Core\Cache\Exception
Definition: Exception.php:22