‪TYPO3CMS  ‪main
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  public 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  if ($this->usedPeclModule === '') {
103  if (extension_loaded('memcache')) {
104  $this->usedPeclModule = 'memcache';
105  } elseif (extension_loaded('memcached')) {
106  $this->usedPeclModule = 'memcached';
107  }
108  }
109  parent::__construct(‪$context, $options);
110  }
111 
118  protected function ‪setServers(array ‪$servers)
119  {
120  $this->servers = ‪$servers;
121  }
122 
128  protected function ‪setCompression($useCompression)
129  {
130  $compressionFlag = $this->usedPeclModule === 'memcache' ? MEMCACHE_COMPRESSED : \Memcached::OPT_COMPRESSION;
131  if ($useCompression === true) {
132  $this->flags ^= $compressionFlag;
133  } else {
134  $this->flags &= ~$compressionFlag;
135  }
136  }
137 
143  protected function ‪getCompression()
144  {
145  return $this->flags !== 0;
146  }
147 
153  public function ‪initializeObject()
154  {
155  if (empty($this->servers)) {
156  throw new Exception('No servers were given to Memcache', 1213115903);
157  }
158  $memcachedPlugin = '\\' . ucfirst($this->usedPeclModule);
159  $this->memcache = new $memcachedPlugin();
160  $defaultPort = $this->usedPeclModule === 'memcache' ? ini_get('memcache.default_port') : 11211;
161  foreach ($this->servers as $server) {
162  if (str_starts_with($server, 'unix://')) {
163  $host = $server;
164  $port = 0;
165  } else {
166  if (str_starts_with($server, 'tcp://')) {
167  $server = substr($server, 6);
168  }
169  if (str_contains($server, ':')) {
170  [$host, $port] = explode(':', $server, 2);
171  } else {
172  $host = $server;
173  $port = $defaultPort;
174  }
175  }
176  $this->memcache->addserver($host, $port);
177  }
178  if ($this->usedPeclModule === 'memcached') {
179  $this->memcache->setOption(\Memcached::OPT_COMPRESSION, $this->‪getCompression());
180  }
181  }
182 
189  public function ‪setPeclModule($peclModule)
190  {
191  if ($peclModule !== 'memcache' && $peclModule !== 'memcached') {
192  throw new Exception('PECL module must be either "memcache" or "memcached".', 1442239768);
193  }
194 
195  $this->usedPeclModule = $peclModule;
196  }
197 
203  public function ‪setCache(FrontendInterface ‪$cache)
204  {
205  parent::setCache(‪$cache);
206  $identifierHash = substr(md5(‪Environment::getProjectPath() . $this->context . $this->cacheIdentifier), 0, 12);
207  $this->identifierPrefix = 'TYPO3_' . $identifierHash . '_';
208  }
209 
220  public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
221  {
222  if (strlen($this->identifierPrefix . $entryIdentifier) > 250) {
223  throw new \InvalidArgumentException('Could not set value. Key more than 250 characters (' . $this->identifierPrefix . $entryIdentifier . ').', 1232969508);
224  }
225  if (!$this->cache instanceof FrontendInterface) {
226  throw new Exception('No cache frontend has been set yet via setCache().', 1207149215);
227  }
228  $tags[] = '%MEMCACHEBE%' . ‪$this->cacheIdentifier;
229  $expiration = $lifetime ?? ‪$this->defaultLifetime;
230 
231  // Memcached considers values over 2592000 sec (30 days) as UNIX timestamp
232  // thus $expiration should be converted from lifetime to UNIX timestamp
233  if ($expiration > 2592000) {
234  $expiration += ‪$GLOBALS['EXEC_TIME'];
235  }
236  try {
237  if (is_string($data) && strlen($data) > self::MAX_BUCKET_SIZE) {
238  $data = str_split($data, 1024 * 1000);
239  $success = true;
240  $chunkNumber = 1;
241  foreach ($data as $chunk) {
242  $success = $success && $this->‪setInternal($entryIdentifier . '_chunk_' . $chunkNumber, $chunk, $expiration);
243  $chunkNumber++;
244  }
245  $success = $success && $this->‪setInternal($entryIdentifier, 'TYPO3*chunked:' . $chunkNumber, $expiration);
246  } else {
247  $success = $this->‪setInternal($entryIdentifier, $data, $expiration);
248  }
249  if ($success === true) {
250  $this->‪removeIdentifierFromAllTags($entryIdentifier);
251  $this->‪addIdentifierToTags($entryIdentifier, $tags);
252  } else {
253  throw new Exception('Could not set data to memcache server.', 1275830266);
254  }
255  } catch (\Exception $exception) {
256  $this->logger->alert('Memcache: could not set value.', ['exception' => $exception]);
257  }
258  }
259 
268  protected function ‪setInternal($entryIdentifier, $data, $expiration)
269  {
270  if ($this->usedPeclModule === 'memcache') {
271  return $this->memcache->set($this->identifierPrefix . $entryIdentifier, $data, $this->flags, $expiration);
272  }
273  return $this->memcache->set($this->identifierPrefix . $entryIdentifier, $data, $expiration);
274  }
275 
282  public function get($entryIdentifier)
283  {
284  $value = $this->memcache->get($this->identifierPrefix . $entryIdentifier);
285  if (is_string($value) && str_starts_with($value, 'TYPO3*chunked:')) {
286  [, $chunkCount] = explode(':', $value);
287  $value = '';
288  for ($chunkNumber = 1; $chunkNumber < $chunkCount; $chunkNumber++) {
289  $value .= $this->memcache->get($this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber);
290  }
291  }
292  return $value;
293  }
294 
301  public function ‪has($entryIdentifier)
302  {
303  if ($this->usedPeclModule === 'memcache') {
304  return $this->memcache->get($this->identifierPrefix . $entryIdentifier) !== false;
305  }
306 
307  // pecl-memcached supports storing literal FALSE
308  $this->memcache->get($this->identifierPrefix . $entryIdentifier);
309  return $this->memcache->getResultCode() !== \Memcached::RES_NOTFOUND;
310  }
311 
320  public function remove($entryIdentifier)
321  {
322  $this->‪removeIdentifierFromAllTags($entryIdentifier);
323  return $this->memcache->delete($this->identifierPrefix . $entryIdentifier, 0);
324  }
325 
333  public function ‪findIdentifiersByTag($tag)
334  {
335  $identifiers = $this->memcache->get($this->identifierPrefix . 'tag_' . $tag);
336  if ($identifiers !== false) {
337  return (array)$identifiers;
338  }
339  return [];
340  }
341 
347  public function ‪flush()
348  {
349  if (!$this->cache instanceof FrontendInterface) {
350  throw new Exception('No cache frontend has been set via setCache() yet.', 1204111376);
351  }
352  $this->‪flushByTag('%MEMCACHEBE%' . $this->cacheIdentifier);
353  }
354 
360  public function ‪flushByTag($tag)
361  {
362  $identifiers = $this->‪findIdentifiersByTag($tag);
363  foreach ($identifiers as ‪$identifier) {
364  $this->remove(‪$identifier);
365  }
366  }
367 
373  protected function ‪addIdentifierToTags($entryIdentifier, array $tags)
374  {
375  // Get identifier-to-tag index to look for updates
376  $existingTags = $this->‪findTagsByIdentifier($entryIdentifier);
377  $existingTagsUpdated = false;
378 
379  foreach ($tags as $tag) {
380  // Update tag-to-identifier index
381  $identifiers = $this->‪findIdentifiersByTag($tag);
382  if (!in_array($entryIdentifier, $identifiers, true)) {
383  $identifiers[] = $entryIdentifier;
384  $this->memcache->set($this->identifierPrefix . 'tag_' . $tag, $identifiers);
385  }
386  // Test if identifier-to-tag index needs update
387  if (!in_array($tag, $existingTags, true)) {
388  $existingTags[] = $tag;
389  $existingTagsUpdated = true;
390  }
391  }
392 
393  // Update identifier-to-tag index if needed
394  if ($existingTagsUpdated) {
395  $this->memcache->set($this->identifierPrefix . 'ident_' . $entryIdentifier, $existingTags);
396  }
397  }
398 
404  protected function ‪removeIdentifierFromAllTags($entryIdentifier)
405  {
406  // Get tags for this identifier
407  $tags = $this->‪findTagsByIdentifier($entryIdentifier);
408  // De-associate tags with this identifier
409  foreach ($tags as $tag) {
410  $identifiers = $this->‪findIdentifiersByTag($tag);
411  // Formally array_search() below should never return FALSE due to
412  // the behavior of findTagsByIdentifier(). But if reverse index is
413  // corrupted, we still can get 'FALSE' from array_search(). This is
414  // not a problem because we are removing this identifier from
415  // anywhere.
416  if (($key = array_search($entryIdentifier, $identifiers)) !== false) {
417  unset($identifiers[$key]);
418  if (!empty($identifiers)) {
419  $this->memcache->set($this->identifierPrefix . 'tag_' . $tag, $identifiers);
420  } else {
421  $this->memcache->delete($this->identifierPrefix . 'tag_' . $tag, 0);
422  }
423  }
424  }
425  // Clear reverse tag index for this identifier
426  $this->memcache->delete($this->identifierPrefix . 'ident_' . $entryIdentifier, 0);
427  }
428 
436  protected function ‪findTagsByIdentifier(‪$identifier)
437  {
438  $tags = $this->memcache->get($this->identifierPrefix . 'ident_' . ‪$identifier);
439  return $tags === false ? [] : (array)$tags;
440  }
441 
445  public function ‪collectGarbage() {}
446 }
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\flush
‪flush()
Definition: MemcachedBackend.php:342
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\$servers
‪array $servers
Definition: MemcachedBackend.php:70
‪TYPO3\CMS\Core\Cache\Backend\TransientBackendInterface
Definition: TransientBackendInterface.php:32
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\has
‪bool has($entryIdentifier)
Definition: MemcachedBackend.php:296
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\findTagsByIdentifier
‪array findTagsByIdentifier($identifier)
Definition: MemcachedBackend.php:431
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend\$cache
‪FrontendInterface $cache
Definition: AbstractBackend.php:38
‪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:399
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\findIdentifiersByTag
‪array findIdentifiersByTag($tag)
Definition: MemcachedBackend.php:328
‪TYPO3\CMS\Core\Cache\Backend\TaggableBackendInterface
Definition: TaggableBackendInterface.php:22
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\setCompression
‪setCompression($useCompression)
Definition: MemcachedBackend.php:123
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend\$defaultLifetime
‪int $defaultLifetime
Definition: AbstractBackend.php:57
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\setPeclModule
‪setPeclModule($peclModule)
Definition: MemcachedBackend.php:184
‪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:160
‪TYPO3\CMS\Core\Cache\Exception
Definition: DuplicateIdentifierException.php:16
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\getCompression
‪bool getCompression()
Definition: MemcachedBackend.php:138
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\setServers
‪setServers(array $servers)
Definition: MemcachedBackend.php:113
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\setCache
‪setCache(FrontendInterface $cache)
Definition: MemcachedBackend.php:198
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\$flags
‪int $flags
Definition: MemcachedBackend.php:77
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\collectGarbage
‪collectGarbage()
Definition: MemcachedBackend.php:440
‪TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
Definition: FrontendInterface.php:22
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\initializeObject
‪initializeObject()
Definition: MemcachedBackend.php:148
‪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:42
‪TYPO3\CMS\Core\Cache\Backend\AbstractBackend
Definition: AbstractBackend.php:28
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:41
‪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\MemcachedBackend
Definition: MemcachedBackend.php:46
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\setInternal
‪bool setInternal($entryIdentifier, $data, $expiration)
Definition: MemcachedBackend.php:263
‪TYPO3\CMS\Core\Cache\Backend\MemcachedBackend\addIdentifierToTags
‪addIdentifierToTags($entryIdentifier, array $tags)
Definition: MemcachedBackend.php:368
‪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:355
‪TYPO3\CMS\Webhooks\Message\$identifier
‪identifier readonly string $identifier
Definition: FileAddedMessage.php:37
‪TYPO3\CMS\Core\Cache\Exception
Definition: Exception.php:21