PageRenderTime 40ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/Classes/TYPO3/FLOW3/Cache/Backend/MemcachedBackend.php

https://github.com/christianjul/FLOW3-Composer
PHP | 375 lines | 152 code | 32 blank | 191 comment | 29 complexity | d4daad0d67e735bdc75658f762ce7b36 MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-3.0
  1. <?php
  2. namespace TYPO3\FLOW3\Cache\Backend;
  3. /* *
  4. * This script belongs to the FLOW3 framework. *
  5. * *
  6. * It is free software; you can redistribute it and/or modify it under *
  7. * the terms of the GNU Lesser General Public License, either version 3 *
  8. * of the License, or (at your option) any later version. *
  9. * *
  10. * The TYPO3 project - inspiring people to share! *
  11. * */
  12. /**
  13. * A caching backend which stores cache entries by using Memcached.
  14. *
  15. * This backend uses the following types of Memcache keys:
  16. * - tag_xxx
  17. * xxx is tag name, value is array of associated identifiers identifier. This
  18. * is "forward" tag index. It is mainly used for obtaining content by tag
  19. * (get identifier by tag -> get content by identifier)
  20. * - ident_xxx
  21. * xxx is identifier, value is array of associated tags. This is "reverse" tag
  22. * index. It provides quick access for all tags associated with this identifier
  23. * and used when removing the identifier
  24. * - tagIndex
  25. * Value is a List of all tags (array)
  26. *
  27. * Each key is prepended with a prefix. By default prefix consists from two parts
  28. * separated by underscore character and ends in yet another underscore character:
  29. * - "FLOW3"
  30. * - MD5 of script path and filename and SAPI name
  31. * This prefix makes sure that keys from the different installations do not
  32. * conflict.
  33. *
  34. * Note: When using the Memcached backend to store values of more than ~1 MB, the
  35. * data will be split into chunks to make them fit into the memcached limits.
  36. *
  37. * @api
  38. */
  39. class MemcachedBackend extends AbstractBackend implements TaggableBackendInterface {
  40. /**
  41. * Max bucket size, (1024*1024)-42 bytes
  42. * @var int
  43. */
  44. const MAX_BUCKET_SIZE = 1048534;
  45. /**
  46. * Instance of the PHP Memcache class
  47. *
  48. * @var \Memcache
  49. */
  50. protected $memcache;
  51. /**
  52. * Array of Memcache server configurations
  53. *
  54. * @var array
  55. */
  56. protected $servers = array();
  57. /**
  58. * Indicates whether the memcache uses compression or not (requires zlib),
  59. * either 0 or MEMCACHE_COMPRESSED
  60. *
  61. * @var int
  62. */
  63. protected $flags;
  64. /**
  65. * A prefix to separate stored data from other data possible stored in the memcache
  66. *
  67. * @var string
  68. */
  69. protected $identifierPrefix;
  70. /**
  71. * Constructs this backend
  72. *
  73. * @param \TYPO3\FLOW3\Core\ApplicationContext $context FLOW3's application context
  74. * @param array $options Configuration options - depends on the actual backend
  75. * @throws \TYPO3\FLOW3\Cache\Exception
  76. */
  77. public function __construct(\TYPO3\FLOW3\Core\ApplicationContext $context, array $options = array()) {
  78. if (!extension_loaded('memcache')) throw new \TYPO3\FLOW3\Cache\Exception('The PHP extension "memcache" must be installed and loaded in order to use the Memcached backend.', 1213987706);
  79. parent::__construct($context, $options);
  80. }
  81. /**
  82. * Setter for servers to be used. Expects an array, the values are expected
  83. * to be formatted like "<host>[:<port>]" or "unix://<path>"
  84. *
  85. * @param array $servers An array of servers to add.
  86. * @return void
  87. * @api
  88. */
  89. protected function setServers(array $servers) {
  90. $this->servers = $servers;
  91. }
  92. /**
  93. * Setter for compression flags bit
  94. *
  95. * @param boolean $useCompression
  96. * @return void
  97. * @api
  98. */
  99. protected function setCompression($useCompression) {
  100. if ($useCompression === TRUE) {
  101. $this->flags ^= MEMCACHE_COMPRESSED;
  102. } else {
  103. $this->flags &= ~MEMCACHE_COMPRESSED;
  104. }
  105. }
  106. /**
  107. * Initializes the identifier prefix
  108. *
  109. * @return void
  110. * @throws \TYPO3\FLOW3\Cache\Exception
  111. */
  112. public function initializeObject() {
  113. if (!count($this->servers)) throw new \TYPO3\FLOW3\Cache\Exception('No servers were given to Memcache', 1213115903);
  114. $this->memcache = new \Memcache();
  115. $defaultPort = ini_get('memcache.default_port');
  116. foreach ($this->servers as $server) {
  117. if (substr($server, 0, 7) === 'unix://') {
  118. $host = $server;
  119. $port = 0;
  120. } else {
  121. if (substr($server, 0, 6) === 'tcp://') {
  122. $server = substr($server, 6);
  123. }
  124. if (strpos($server, ':') !== FALSE) {
  125. list($host, $port) = explode(':', $server, 2);
  126. } else {
  127. $host = $server;
  128. $port = $defaultPort;
  129. }
  130. }
  131. $this->memcache->addServer($host, $port);
  132. }
  133. }
  134. /**
  135. * Initializes the identifier prefix when setting the cache.
  136. *
  137. * @param \TYPO3\FLOW3\Cache\Frontend\FrontendInterface $cache
  138. * @return void
  139. */
  140. public function setCache(\TYPO3\FLOW3\Cache\Frontend\FrontendInterface $cache) {
  141. parent::setCache($cache);
  142. $this->identifierPrefix = 'FLOW3_' . md5($cache->getIdentifier() . \TYPO3\FLOW3\Utility\Files::getUnixStylePath($_SERVER['SCRIPT_FILENAME']) . PHP_SAPI) . '_';
  143. }
  144. /**
  145. * Saves data in the cache.
  146. *
  147. * @param string $entryIdentifier An identifier for this specific cache entry
  148. * @param string $data The data to be stored
  149. * @param array $tags Tags to associate with this cache entry
  150. * @param integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited lifetime.
  151. * @return void
  152. * @throws \TYPO3\FLOW3\Cache\Exception if no cache frontend has been set.
  153. * @throws \InvalidArgumentException if the identifier is not valid or the final memcached key is longer than 250 characters
  154. * @throws \TYPO3\FLOW3\Cache\Exception\InvalidDataException if $data is not a string
  155. * @api
  156. */
  157. public function set($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
  158. if (strlen($this->identifierPrefix . $entryIdentifier) > 250) throw new \InvalidArgumentException('Could not set value. Key more than 250 characters (' . $this->identifierPrefix . $entryIdentifier . ').', 1232969508);
  159. if (!$this->cache instanceof \TYPO3\FLOW3\Cache\Frontend\FrontendInterface) throw new \TYPO3\FLOW3\Cache\Exception('No cache frontend has been set yet via setCache().', 1207149215);
  160. if (!is_string($data)) throw new \TYPO3\FLOW3\Cache\Exception\InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1207149231);
  161. $tags[] = '%MEMCACHEBE%' . $this->cacheIdentifier;
  162. $expiration = $lifetime !== NULL ? $lifetime : $this->defaultLifetime;
  163. // Memcached consideres values over 2592000 sec (30 days) as UNIX timestamp
  164. // thus $expiration should be converted from lifetime to UNIX timestamp
  165. if ($expiration > 2592000) {
  166. $expiration += time();
  167. }
  168. try {
  169. if (strlen($data) > self::MAX_BUCKET_SIZE) {
  170. $data = str_split($data, self::MAX_BUCKET_SIZE - 1024);
  171. $success = TRUE;
  172. $chunkNumber = 1;
  173. foreach ($data as $chunk) {
  174. $success = $success && $this->memcache->set($this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber, $chunk, $this->flags, $expiration);
  175. $chunkNumber++;
  176. }
  177. $success = $success && $this->memcache->set($this->identifierPrefix . $entryIdentifier, 'FLOW3*chunked:' . $chunkNumber, $this->flags, $expiration);
  178. } else {
  179. $success = $this->memcache->set($this->identifierPrefix . $entryIdentifier, $data, $this->flags, $expiration);
  180. }
  181. if ($success === TRUE) {
  182. $this->removeIdentifierFromAllTags($entryIdentifier);
  183. $this->addIdentifierToTags($entryIdentifier, $tags);
  184. } else {
  185. throw new \TYPO3\FLOW3\Cache\Exception('Could not set value on memcache server.', 1275830266);
  186. }
  187. } catch (\Exception $exception) {
  188. throw new \TYPO3\FLOW3\Cache\Exception('Could not set value. ' . $exception->getMessage(), 1207208100);
  189. }
  190. }
  191. /**
  192. * Loads data from the cache.
  193. *
  194. * @param string $entryIdentifier An identifier which describes the cache entry to load
  195. * @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
  196. * @api
  197. */
  198. public function get($entryIdentifier) {
  199. $value = $this->memcache->get($this->identifierPrefix . $entryIdentifier);
  200. if (substr($value, 0, 14) === 'FLOW3*chunked:') {
  201. list( , $chunkCount) = explode(':', $value);
  202. $value = '';
  203. for ($chunkNumber = 1 ; $chunkNumber < $chunkCount; $chunkNumber++) {
  204. $value .= $this->memcache->get($this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber);
  205. }
  206. }
  207. return $value;
  208. }
  209. /**
  210. * Checks if a cache entry with the specified identifier exists.
  211. *
  212. * @param string $entryIdentifier An identifier specifying the cache entry
  213. * @return boolean TRUE if such an entry exists, FALSE if not
  214. * @api
  215. */
  216. public function has($entryIdentifier) {
  217. return $this->memcache->get($this->identifierPrefix . $entryIdentifier) !== FALSE;
  218. }
  219. /**
  220. * Removes all cache entries matching the specified identifier.
  221. * Usually this only affects one entry but if - for what reason ever -
  222. * old entries for the identifier still exist, they are removed as well.
  223. *
  224. * @param string $entryIdentifier Specifies the cache entry to remove
  225. * @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
  226. * @api
  227. */
  228. public function remove($entryIdentifier) {
  229. $this->removeIdentifierFromAllTags($entryIdentifier);
  230. return $this->memcache->delete($this->identifierPrefix . $entryIdentifier, 0);
  231. }
  232. /**
  233. * Finds and returns all cache entry identifiers which are tagged by the
  234. * specified tag.
  235. *
  236. * @param string $tag The tag to search for
  237. * @return array An array with identifiers of all matching entries. An empty array if no entries matched
  238. * @api
  239. */
  240. public function findIdentifiersByTag($tag) {
  241. $identifiers = $this->memcache->get($this->identifierPrefix . 'tag_' . $tag);
  242. if ($identifiers !== FALSE) {
  243. return (array) $identifiers;
  244. } else {
  245. return array();
  246. }
  247. }
  248. /**
  249. * Finds all tags for the given identifier. This function uses reverse tag
  250. * index to search for tags.
  251. *
  252. * @param string $identifier Identifier to find tags by
  253. * @return array Array with tags
  254. */
  255. protected function findTagsByIdentifier($identifier) {
  256. $tags = $this->memcache->get($this->identifierPrefix . 'ident_' . $identifier);
  257. return ($tags === FALSE ? array() : (array)$tags);
  258. }
  259. /**
  260. * Removes all cache entries of this cache.
  261. *
  262. * @return void
  263. * @throws \TYPO3\FLOW3\Cache\Exception
  264. * @api
  265. */
  266. public function flush() {
  267. if (!$this->cache instanceof \TYPO3\FLOW3\Cache\Frontend\FrontendInterface) throw new \TYPO3\FLOW3\Cache\Exception('Yet no cache frontend has been set via setCache().', 1204111376);
  268. $this->flushByTag('%MEMCACHEBE%' . $this->cacheIdentifier);
  269. }
  270. /**
  271. * Removes all cache entries of this cache which are tagged by the specified tag.
  272. *
  273. * @param string $tag The tag the entries must have
  274. * @return void
  275. * @api
  276. */
  277. public function flushByTag($tag) {
  278. $identifiers = $this->findIdentifiersByTag($tag);
  279. foreach ($identifiers as $identifier) {
  280. $this->remove($identifier);
  281. }
  282. }
  283. /**
  284. * Associates the identifier with the given tags
  285. *
  286. * @param string $entryIdentifier
  287. * @param array $tags
  288. * @return void
  289. */
  290. protected function addIdentifierToTags($entryIdentifier, array $tags) {
  291. foreach ($tags as $tag) {
  292. // Update tag-to-identifier index
  293. $identifiers = $this->findIdentifiersByTag($tag);
  294. if (array_search($entryIdentifier, $identifiers) === FALSE) {
  295. $identifiers[] = $entryIdentifier;
  296. $this->memcache->set($this->identifierPrefix . 'tag_' . $tag, $identifiers);
  297. }
  298. // Update identifier-to-tag index
  299. $existingTags = $this->findTagsByIdentifier($entryIdentifier);
  300. if (array_search($tag, $existingTags) === FALSE) {
  301. $this->memcache->set($this->identifierPrefix . 'ident_' . $entryIdentifier, array_merge($existingTags, $tags));
  302. }
  303. }
  304. }
  305. /**
  306. * Removes association of the identifier with the given tags
  307. *
  308. * @param string $entryIdentifier
  309. * @return void
  310. */
  311. protected function removeIdentifierFromAllTags($entryIdentifier) {
  312. // Get tags for this identifier
  313. $tags = $this->findTagsByIdentifier($entryIdentifier);
  314. // Deassociate tags with this identifier
  315. foreach ($tags as $tag) {
  316. $identifiers = $this->findIdentifiersByTag($tag);
  317. // Formally array_search() below should never return false due to
  318. // the behavior of findTagsByIdentifier(). But if reverse index is
  319. // corrupted, we still can get 'false' from array_search(). This is
  320. // not a problem because we are removing this identifier from
  321. // anywhere.
  322. if (($key = array_search($entryIdentifier, $identifiers)) !== FALSE) {
  323. unset($identifiers[$key]);
  324. if (count($identifiers)) {
  325. $this->memcache->set($this->identifierPrefix . 'tag_' . $tag, $identifiers);
  326. } else {
  327. $this->memcache->delete($this->identifierPrefix . 'tag_' . $tag, 0);
  328. }
  329. }
  330. }
  331. // Clear reverse tag index for this identifier
  332. $this->memcache->delete($this->identifierPrefix . 'ident_' . $entryIdentifier, 0);
  333. }
  334. /**
  335. * Does nothing, as memcached does GC itself
  336. *
  337. * @return void
  338. * @api
  339. */
  340. public function collectGarbage() {
  341. }
  342. }
  343. ?>