[TASK] Update php-cs-fixer to 2.5.0
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Cache / Backend / MemcachedBackend.php
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
17 use TYPO3\CMS\Core\Cache\Exception;
18 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
19 use TYPO3\CMS\Core\Utility\GeneralUtility;
20
21 /**
22 * A caching backend which stores cache entries by using Memcached.
23 *
24 * This backend uses the following types of Memcache keys:
25 * - tag_xxx
26 * xxx is tag name, value is array of associated identifiers identifier. This
27 * is "forward" tag index. It is mainly used for obtaining content by tag
28 * (get identifier by tag -> get content by identifier)
29 * - ident_xxx
30 * xxx is identifier, value is array of associated tags. This is "reverse" tag
31 * index. It provides quick access for all tags associated with this identifier
32 * and used when removing the identifier
33 *
34 * Each key is prepended with a prefix. By default prefix consists from two parts
35 * separated by underscore character and ends in yet another underscore character:
36 * - "TYPO3"
37 * - Current site path obtained from the PATH_site constant
38 * This prefix makes sure that keys from the different installations do not
39 * conflict.
40 *
41 * Note: When using the Memcached backend to store values of more than ~1 MB,
42 * the data will be split into chunks to make them fit into the memcached limits.
43 *
44 * This file is a backport from FLOW3 by Ingo Renner.
45 * @api
46 */
47 class MemcachedBackend extends AbstractBackend implements TaggableBackendInterface, TransientBackendInterface
48 {
49 /**
50 * Max bucket size, (1024*1024)-42 bytes
51 *
52 * @var int
53 */
54 const MAX_BUCKET_SIZE = 1048534;
55
56 /**
57 * Instance of the PHP Memcache class
58 *
59 * @var \Memcache|\Memcached
60 */
61 protected $memcache;
62
63 /**
64 * Used PECL module for memcached
65 *
66 * @var string
67 */
68 protected $usedPeclModule = '';
69
70 /**
71 * Array of Memcache server configurations
72 *
73 * @var array
74 */
75 protected $servers = [];
76
77 /**
78 * Indicates whether the memcache uses compression or not (requires zlib),
79 * either 0 or \Memcached::OPT_COMPRESSION / MEMCACHE_COMPRESSED
80 *
81 * @var int
82 */
83 protected $flags;
84
85 /**
86 * A prefix to separate stored data from other data possibly stored in the memcache
87 *
88 * @var string
89 */
90 protected $identifierPrefix;
91
92 /**
93 * Constructs this backend
94 *
95 * @param string $context FLOW3's application context
96 * @param array $options Configuration options - depends on the actual backend
97 * @throws Exception if memcache is not installed
98 */
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
116 /**
117 * Setter for servers to be used. Expects an array, the values are expected
118 * to be formatted like "<host>[:<port>]" or "unix://<path>"
119 *
120 * @param array $servers An array of servers to add.
121 * @api
122 */
123 protected function setServers(array $servers)
124 {
125 $this->servers = $servers;
126 }
127
128 /**
129 * Setter for compression flags bit
130 *
131 * @param bool $useCompression
132 * @api
133 */
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
144 /**
145 * Getter for compression flag
146 *
147 * @return bool
148 * @api
149 */
150 protected function getCompression()
151 {
152 return $this->flags !== 0;
153 }
154
155 /**
156 * Initializes the identifier prefix
157 *
158 * @throws Exception
159 */
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
190 /**
191 * Sets the preferred PECL module
192 *
193 * @param string $peclModule
194 * @throws Exception
195 */
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
205 /**
206 * Initializes the identifier prefix when setting the cache.
207 *
208 * @param FrontendInterface $cache The frontend for this backend
209 */
210 public function setCache(FrontendInterface $cache)
211 {
212 parent::setCache($cache);
213 $identifierHash = substr(md5(PATH_site . $this->context . $this->cacheIdentifier), 0, 12);
214 $this->identifierPrefix = 'TYPO3_' . $identifierHash . '_';
215 }
216
217 /**
218 * Saves data in the cache.
219 *
220 * @param string $entryIdentifier An identifier for this specific cache entry
221 * @param string $data The data to be stored
222 * @param array $tags Tags to associate with this cache entry
223 * @param int $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
224 * @throws Exception if no cache frontend has been set.
225 * @throws \InvalidArgumentException if the identifier is not valid or the final memcached key is longer than 250 characters
226 * @throws Exception\InvalidDataException if $data is not a string
227 * @api
228 */
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 $tags[] = '%MEMCACHEBE%' . $this->cacheIdentifier;
238 $expiration = $lifetime !== null ? $lifetime : $this->defaultLifetime;
239
240 // Memcached consideres values over 2592000 sec (30 days) as UNIX timestamp
241 // thus $expiration should be converted from lifetime to UNIX timestamp
242 if ($expiration > 2592000) {
243 $expiration += $GLOBALS['EXEC_TIME'];
244 }
245 try {
246 if (is_string($data) && strlen($data) > self::MAX_BUCKET_SIZE) {
247 $data = str_split($data, 1024 * 1000);
248 $success = true;
249 $chunkNumber = 1;
250 foreach ($data as $chunk) {
251 $success = $success && $this->setInternal($entryIdentifier . '_chunk_' . $chunkNumber, $chunk, $expiration);
252 $chunkNumber++;
253 }
254 $success = $success && $this->setInternal($entryIdentifier, 'TYPO3*chunked:' . $chunkNumber, $expiration);
255 } else {
256 $success = $this->setInternal($entryIdentifier, $data, $expiration);
257 }
258 if ($success === true) {
259 $this->removeIdentifierFromAllTags($entryIdentifier);
260 $this->addIdentifierToTags($entryIdentifier, $tags);
261 } else {
262 throw new Exception('Could not set data to memcache server.', 1275830266);
263 }
264 } catch (\Exception $exception) {
265 GeneralUtility::sysLog('Memcache: could not set value. Reason: ' . $exception->getMessage(), 'core', GeneralUtility::SYSLOG_SEVERITY_WARNING);
266 }
267 }
268
269 /**
270 * Stores the actual data inside memcache/memcached
271 *
272 * @param string $entryIdentifier
273 * @param mixed $data
274 * @param int $expiration
275 * @return bool
276 */
277 protected function setInternal($entryIdentifier, $data, $expiration)
278 {
279 if ($this->usedPeclModule === 'memcache') {
280 return $this->memcache->set($this->identifierPrefix . $entryIdentifier, $data, $this->flags, $expiration);
281 }
282 return $this->memcache->set($this->identifierPrefix . $entryIdentifier, $data, $expiration);
283 }
284
285 /**
286 * Loads data from the cache.
287 *
288 * @param string $entryIdentifier An identifier which describes the cache entry to load
289 * @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
290 * @api
291 */
292 public function get($entryIdentifier)
293 {
294 $value = $this->memcache->get($this->identifierPrefix . $entryIdentifier);
295 if (is_string($value) && 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
305 /**
306 * Checks if a cache entry with the specified identifier exists.
307 *
308 * @param string $entryIdentifier An identifier specifying the cache entry
309 * @return bool TRUE if such an entry exists, FALSE if not
310 * @api
311 */
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
323 /**
324 * Removes all cache entries matching the specified identifier.
325 * Usually this only affects one entry but if - for what reason ever -
326 * old entries for the identifier still exist, they are removed as well.
327 *
328 * @param string $entryIdentifier Specifies the cache entry to remove
329 * @return bool TRUE if (at least) an entry could be removed or FALSE if no entry was found
330 * @api
331 */
332 public function remove($entryIdentifier)
333 {
334 $this->removeIdentifierFromAllTags($entryIdentifier);
335 return $this->memcache->delete($this->identifierPrefix . $entryIdentifier, 0);
336 }
337
338 /**
339 * Finds and returns all cache entry identifiers which are tagged by the
340 * specified tag.
341 *
342 * @param string $tag The tag to search for
343 * @return array An array of entries with all matching entries. An empty array if no entries matched
344 * @api
345 */
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
355 /**
356 * Removes all cache entries of this cache.
357 *
358 * @throws Exception
359 * @api
360 */
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
369 /**
370 * Removes all cache entries of this cache which are tagged by the specified tag.
371 *
372 * @param string $tag The tag the entries must have
373 * @api
374 */
375 public function flushByTag($tag)
376 {
377 $identifiers = $this->findIdentifiersByTag($tag);
378 foreach ($identifiers as $identifier) {
379 $this->remove($identifier);
380 }
381 }
382
383 /**
384 * Associates the identifier with the given tags
385 *
386 * @param string $entryIdentifier
387 * @param array $tags
388 */
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
415 /**
416 * Removes association of the identifier with the given tags
417 *
418 * @param string $entryIdentifier
419 */
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
445 /**
446 * Finds all tags for the given identifier. This function uses reverse tag
447 * index to search for tags.
448 *
449 * @param string $identifier Identifier to find tags by
450 * @return array
451 * @api
452 */
453 protected function findTagsByIdentifier($identifier)
454 {
455 $tags = $this->memcache->get($this->identifierPrefix . 'ident_' . $identifier);
456 return $tags === false ? [] : (array)$tags;
457 }
458
459 /**
460 * Does nothing, as memcached does GC itself
461 *
462 * @api
463 */
464 public function collectGarbage()
465 {
466 }
467 }