TYPO3  7.6
MemcachedBackend.php
Go to the documentation of this file.
1 <?php
2 namespace TYPO3\CMS\Core\Cache\Backend;
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 
44 {
50  const MAX_BUCKET_SIZE = 1048534;
56  protected $memcache;
57 
63  protected $servers = array();
64 
71  protected $flags;
72 
78  protected $identifierPrefix;
79 
87  public function __construct($context, array $options = array())
88  {
89  if (!extension_loaded('memcache')) {
90  throw new \TYPO3\CMS\Core\Cache\Exception('The PHP extension "memcache" must be installed and loaded in ' . 'order to use the Memcached backend.', 1213987706);
91  }
92  parent::__construct($context, $options);
93  }
94 
103  protected function setServers(array $servers)
104  {
105  $this->servers = $servers;
106  }
107 
115  protected function setCompression($useCompression)
116  {
117  if ($useCompression === true) {
118  $this->flags ^= MEMCACHE_COMPRESSED;
119  } else {
120  $this->flags &= ~MEMCACHE_COMPRESSED;
121  }
122  }
123 
130  public function initializeObject()
131  {
132  if (empty($this->servers)) {
133  throw new \TYPO3\CMS\Core\Cache\Exception('No servers were given to Memcache', 1213115903);
134  }
135  $this->memcache = new \Memcache();
136  $defaultPort = ini_get('memcache.default_port');
137  foreach ($this->servers as $server) {
138  if (substr($server, 0, 7) == 'unix://') {
139  $host = $server;
140  $port = 0;
141  } else {
142  if (substr($server, 0, 6) === 'tcp://') {
143  $server = substr($server, 6);
144  }
145  if (strpos($server, ':') !== false) {
146  list($host, $port) = explode(':', $server, 2);
147  } else {
148  $host = $server;
149  $port = $defaultPort;
150  }
151  }
152  $this->memcache->addserver($host, $port);
153  }
154  }
155 
162  public function setCache(\TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache)
163  {
164  parent::setCache($cache);
165  $identifierHash = substr(md5(PATH_site . $this->context . $this->cacheIdentifier), 0, 12);
166  $this->identifierPrefix = 'TYPO3_' . $identifierHash . '_';
167  }
168 
182  public function set($entryIdentifier, $data, array $tags = array(), $lifetime = null)
183  {
184  if (strlen($this->identifierPrefix . $entryIdentifier) > 250) {
185  throw new \InvalidArgumentException('Could not set value. Key more than 250 characters (' . $this->identifierPrefix . $entryIdentifier . ').', 1232969508);
186  }
187  if (!$this->cache instanceof \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface) {
188  throw new \TYPO3\CMS\Core\Cache\Exception('No cache frontend has been set yet via setCache().', 1207149215);
189  }
190  if (!is_string($data)) {
191  throw new \TYPO3\CMS\Core\Cache\Exception\InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1207149231);
192  }
193  $tags[] = '%MEMCACHEBE%' . $this->cacheIdentifier;
194  $expiration = $lifetime !== null ? $lifetime : $this->defaultLifetime;
195  // Memcached consideres values over 2592000 sec (30 days) as UNIX timestamp
196  // thus $expiration should be converted from lifetime to UNIX timestamp
197  if ($expiration > 2592000) {
198  $expiration += $GLOBALS['EXEC_TIME'];
199  }
200  try {
201  if (strlen($data) > self::MAX_BUCKET_SIZE) {
202  $data = str_split($data, 1024 * 1000);
203  $success = true;
204  $chunkNumber = 1;
205  foreach ($data as $chunk) {
206  $success = $success && $this->memcache->set($this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber, $chunk, $this->flags, $expiration);
207  $chunkNumber++;
208  }
209  $success = $success && $this->memcache->set($this->identifierPrefix . $entryIdentifier, 'TYPO3*chunked:' . $chunkNumber, $this->flags, $expiration);
210  } else {
211  $success = $this->memcache->set($this->identifierPrefix . $entryIdentifier, $data, $this->flags, $expiration);
212  }
213  if ($success === true) {
214  $this->removeIdentifierFromAllTags($entryIdentifier);
215  $this->addIdentifierToTags($entryIdentifier, $tags);
216  } else {
217  throw new \TYPO3\CMS\Core\Cache\Exception('Could not set data to memcache server.', 1275830266);
218  }
219  } catch (\Exception $exception) {
220  \TYPO3\CMS\Core\Utility\GeneralUtility::sysLog('Memcache: could not set value. Reason: ' . $exception->getMessage(), 'core', \TYPO3\CMS\Core\Utility\GeneralUtility::SYSLOG_SEVERITY_WARNING);
221  }
222  }
223 
231  public function get($entryIdentifier)
232  {
233  $value = $this->memcache->get($this->identifierPrefix . $entryIdentifier);
234  if (substr($value, 0, 14) === 'TYPO3*chunked:') {
235  list(, $chunkCount) = explode(':', $value);
236  $value = '';
237  for ($chunkNumber = 1; $chunkNumber < $chunkCount; $chunkNumber++) {
238  $value .= $this->memcache->get($this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber);
239  }
240  }
241  return $value;
242  }
243 
251  public function has($entryIdentifier)
252  {
253  return $this->memcache->get($this->identifierPrefix . $entryIdentifier) !== false;
254  }
255 
265  public function remove($entryIdentifier)
266  {
267  $this->removeIdentifierFromAllTags($entryIdentifier);
268  return $this->memcache->delete($this->identifierPrefix . $entryIdentifier, 0);
269  }
270 
279  public function findIdentifiersByTag($tag)
280  {
281  $identifiers = $this->memcache->get($this->identifierPrefix . 'tag_' . $tag);
282  if ($identifiers !== false) {
283  return (array)$identifiers;
284  } else {
285  return array();
286  }
287  }
288 
296  public function flush()
297  {
298  if (!$this->cache instanceof \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface) {
299  throw new \TYPO3\CMS\Core\Cache\Exception('No cache frontend has been set via setCache() yet.', 1204111376);
300  }
301  $this->flushByTag('%MEMCACHEBE%' . $this->cacheIdentifier);
302  }
303 
311  public function flushByTag($tag)
312  {
313  $identifiers = $this->findIdentifiersByTag($tag);
314  foreach ($identifiers as $identifier) {
315  $this->remove($identifier);
316  }
317  }
318 
326  protected function addIdentifierToTags($entryIdentifier, array $tags)
327  {
328  // Get identifier-to-tag index to look for updates
329  $existingTags = $this->findTagsByIdentifier($entryIdentifier);
330  $existingTagsUpdated = false;
331 
332  foreach ($tags as $tag) {
333  // Update tag-to-identifier index
334  $identifiers = $this->findIdentifiersByTag($tag);
335  if (!in_array($entryIdentifier, $identifiers, true)) {
336  $identifiers[] = $entryIdentifier;
337  $this->memcache->set($this->identifierPrefix . 'tag_' . $tag, $identifiers);
338  }
339  // Test if identifier-to-tag index needs update
340  if (!in_array($tag, $existingTags, true)) {
341  $existingTags[] = $tag;
342  $existingTagsUpdated = true;
343  }
344  }
345 
346  // Update identifier-to-tag index if needed
347  if ($existingTagsUpdated) {
348  $this->memcache->set($this->identifierPrefix . 'ident_' . $entryIdentifier, $existingTags);
349  }
350  }
351 
359  protected function removeIdentifierFromAllTags($entryIdentifier)
360  {
361  // Get tags for this identifier
362  $tags = $this->findTagsByIdentifier($entryIdentifier);
363  // Deassociate tags with this identifier
364  foreach ($tags as $tag) {
365  $identifiers = $this->findIdentifiersByTag($tag);
366  // Formally array_search() below should never return FALSE due to
367  // the behavior of findTagsByIdentifier(). But if reverse index is
368  // corrupted, we still can get 'FALSE' from array_search(). This is
369  // not a problem because we are removing this identifier from
370  // anywhere.
371  if (($key = array_search($entryIdentifier, $identifiers)) !== false) {
372  unset($identifiers[$key]);
373  if (!empty($identifiers)) {
374  $this->memcache->set($this->identifierPrefix . 'tag_' . $tag, $identifiers);
375  } else {
376  $this->memcache->delete($this->identifierPrefix . 'tag_' . $tag, 0);
377  }
378  }
379  }
380  // Clear reverse tag index for this identifier
381  $this->memcache->delete($this->identifierPrefix . 'ident_' . $entryIdentifier, 0);
382  }
383 
392  protected function findTagsByIdentifier($identifier)
393  {
394  $tags = $this->memcache->get($this->identifierPrefix . 'ident_' . $identifier);
395  return $tags === false ? array() : (array)$tags;
396  }
397 
404  public function collectGarbage()
405  {
406  }
407 }