025494c8351d5b73bac0b33f9beff9a00b5d011a
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Cache / Backend / XcacheBackend.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 /**
18 * A caching backend which stores cache entries by using xcache opcode cache.
19 *
20 * This backend uses the following types of keys:
21 * - tag_xxx
22 * xxx is tag name, value is array of associated identifiers identifier. This
23 * is "forward" tag index. It is mainly used for obtaining content by tag
24 * (get identifier by tag -> get content by identifier)
25 * - ident_xxx
26 * xxx is identifier, value is array of associated tags. This is "reverse" tag
27 * index. It provides quick access for all tags associated with this identifier
28 * and used when removing the identifier
29 *
30 * Each key is prepended with a prefix. By default prefix consists from two parts
31 * separated by underscore character and ends in yet another underscore character:
32 * - "TYPO3"
33 * - MD5 of script path and filename and SAPI name
34 * This prefix makes sure that keys from the different installations do not
35 * conflict.
36 *
37 * @author Philipp Gampe <philipp.gampe@typo3.org>
38 */
39 class XcacheBackend extends AbstractBackend implements TaggableBackendInterface {
40
41 /**
42 * A prefix to separate stored data from other data possible stored in the xcache
43 *
44 * @var string
45 */
46 protected $identifierPrefix;
47
48 /**
49 * Constructs this backend
50 *
51 * @param string $context FLOW3's application context
52 * @param array $options Configuration options
53 * @throws \TYPO3\CMS\Core\Cache\Exception If xcache PHP extension is not loaded
54 */
55 public function __construct($context, array $options = array()) {
56 if (!extension_loaded('xcache')) {
57 throw new Exception(
58 'The PHP extension "xcache" must be installed and loaded in order to use the xcache backend.',
59 1363116592
60 );
61 }
62 parent::__construct($context, $options);
63 }
64
65 /**
66 * Saves data in the cache
67 *
68 * @param string $entryIdentifier An identifier for this specific cache entry
69 * @param string $data The data to be stored
70 * @param array $tags Tags to associate with this cache entry
71 * @param integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited liftime.
72 * @return void
73 * @throws \TYPO3\CMS\Core\Cache\Exception if no cache frontend has been set
74 * @throws \TYPO3\CMS\Core\Cache\Exception\InvalidDataException if $data is not a string
75 */
76 public function set($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
77 if ($this->runningFromCliOrWrongConfiguration()) {
78 return;
79 }
80 if (!$this->cache instanceof \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface) {
81 throw new Exception(
82 'No cache frontend has been set yet via setCache().',
83 1363117491
84 );
85 }
86 if (!is_string($data)) {
87 throw new Exception\InvalidDataException(
88 'The specified data is of type "' . gettype($data) . '" but a string is expected.',
89 1363117435
90 );
91 }
92 $tags[] = '%XCBE%' . $this->cache->getIdentifier();
93 $expiration = $lifetime !== NULL ? $lifetime : $this->defaultLifetime;
94 $success = xcache_set($this->identifierPrefix . $entryIdentifier, $data, $expiration);
95 if ($success === TRUE) {
96 $this->removeIdentifierFromAllTags($entryIdentifier);
97 $this->addIdentifierToTags($entryIdentifier, $tags);
98 } else {
99 throw new Exception(
100 'Could not set value.',
101 1363117507
102 );
103 }
104 }
105
106 /**
107 * Loads data from the cache
108 *
109 * @param string $entryIdentifier An identifier which describes the cache entry to load
110 * @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
111 */
112 public function get($entryIdentifier) {
113 if ($this->runningFromCliOrWrongConfiguration()) {
114 return FALSE;
115 }
116 $value = xcache_get($this->identifierPrefix . $entryIdentifier);
117 return $value ?: FALSE;
118 }
119
120 /**
121 * Checks if a cache entry with the specified identifier exists
122 *
123 * @param string $entryIdentifier An identifier specifying the cache entry
124 * @return boolean TRUE if such an entry exists, FALSE if not
125 */
126 public function has($entryIdentifier) {
127 if ($this->runningFromCliOrWrongConfiguration()) {
128 return FALSE;
129 }
130 return xcache_isset($this->identifierPrefix . $entryIdentifier);
131 }
132
133 /**
134 * Removes all cache entries matching the specified identifier.
135 * Usually this only affects one entry but if - for what reason ever -
136 * old entries for the identifier still exist, they are removed as well.
137 *
138 * @param string $entryIdentifier Specifies the cache entry to remove
139 * @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
140 */
141 public function remove($entryIdentifier) {
142 if ($this->runningFromCliOrWrongConfiguration()) {
143 return FALSE;
144 }
145 $this->removeIdentifierFromAllTags($entryIdentifier);
146 return xcache_unset($this->identifierPrefix . $entryIdentifier);
147 }
148
149 /**
150 * Finds and returns all cache entry identifiers which are tagged by the
151 * specified tag.
152 *
153 * @param string $tag The tag to search for
154 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
155 */
156 public function findIdentifiersByTag($tag) {
157 if ($this->runningFromCliOrWrongConfiguration()) {
158 return array();
159 }
160 $identifiers = xcache_get($this->identifierPrefix . 'tag_' . $tag);
161 return $identifiers ?: array();
162 }
163
164 /**
165 * Finds all tags for the given identifier. This function uses reverse tag
166 * index to search for tags.
167 *
168 * @param string $identifier Identifier to find tags by
169 * @return array Array with tags
170 */
171 protected function findTagsByIdentifier($identifier) {
172 if ($this->runningFromCliOrWrongConfiguration()) {
173 return array();
174 }
175 $tags = xcache_get($this->identifierPrefix . 'ident_' . $identifier);
176 return $tags ?: array();
177 }
178
179 /**
180 * Removes all cache entries of this cache
181 *
182 * @return void
183 * @throws \TYPO3\CMS\Core\Cache\Exception
184 */
185 public function flush() {
186 if ($this->runningFromCliOrWrongConfiguration()) {
187 return;
188 }
189 if (!$this->cache instanceof \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface) {
190 throw new Exception(
191 'Yet no cache frontend has been set via setCache().',
192 1363117531
193 );
194 }
195 $this->flushByTag('%XCBE%' . $this->cache->getIdentifier());
196 }
197
198 /**
199 * Removes all cache entries of this cache which are tagged by the specified
200 * tag.
201 *
202 * @param string $tag The tag the entries must have
203 * @return void
204 */
205 public function flushByTag($tag) {
206 $identifiers = $this->findIdentifiersByTag($tag);
207 foreach ($identifiers as $identifier) {
208 $this->remove($identifier);
209 }
210 }
211
212 /**
213 * Associates the identifier with the given tags
214 *
215 * @param string $entryIdentifier
216 * @param array $tags
217 * @return void
218 */
219 protected function addIdentifierToTags($entryIdentifier, array $tags) {
220 if ($this->runningFromCliOrWrongConfiguration()) {
221 return;
222 }
223
224 // Get identifier-to-tag index to look for updates
225 $existingTags = $this->findTagsByIdentifier($entryIdentifier);
226 $existingTagsUpdated = FALSE;
227
228
229 foreach ($tags as $tag) {
230 // Update tag-to-identifier index
231 $identifiers = $this->findIdentifiersByTag($tag);
232 if (!in_array($entryIdentifier, $identifiers, TRUE)) {
233 $identifiers[] = $entryIdentifier;
234 xcache_set($this->identifierPrefix . 'tag_' . $tag, $identifiers);
235 }
236 // Test if identifier-to-tag index needs update
237 if (!in_array($tag, $existingTags, TRUE)) {
238 $existingTags[] = $tag;
239 $existingTagsUpdated = TRUE;
240 }
241 }
242
243 // Update identifier-to-tag index if needed
244 if ($existingTagsUpdated) {
245 xcache_set($this->identifierPrefix . 'ident_' . $entryIdentifier, $existingTags);
246 }
247 }
248
249 /**
250 * Removes association of the identifier with the given tags
251 *
252 * @param string $entryIdentifier
253 * @return void
254 */
255 protected function removeIdentifierFromAllTags($entryIdentifier) {
256 if ($this->runningFromCliOrWrongConfiguration()) {
257 return;
258 }
259 // Get tags for this identifier
260 $tags = $this->findTagsByIdentifier($entryIdentifier);
261 // Disassociate tags with this identifier
262 foreach ($tags as $tag) {
263 $identifiers = $this->findIdentifiersByTag($tag);
264 // Formally array_search() below should never return false due to
265 // the behavior of findTagsByIdentifier(). But if reverse index is
266 // corrupted, we still can get 'false' from array_search(). This is
267 // not a problem because we are removing this identifier from
268 // anywhere.
269 if (($key = array_search($entryIdentifier, $identifiers)) !== FALSE) {
270 unset($identifiers[$key]);
271 if (count($identifiers)) {
272 xcache_set($this->identifierPrefix . 'tag_' . $tag, $identifiers);
273 } else {
274 xcache_unset($this->identifierPrefix . 'tag_' . $tag);
275 }
276 }
277 }
278 // Clear reverse tag index for this identifier
279 xcache_unset($this->identifierPrefix . 'ident_' . $entryIdentifier);
280 }
281
282 /**
283 * Does nothing, as xcache does GC itself
284 *
285 * @return void
286 */
287 public function collectGarbage() {
288 }
289
290 /**
291 * Checks if backend is called from CLI context.
292 * In this case all methods fail silently as xcache user cache is not available in CLI context.
293 * xcache.var_size cat be zero or empty if in CLI mode, or if not correctly configured.
294 *
295 * @return boolean TRUE if misconfigured or in CLI mode
296 */
297 protected function runningFromCliOrWrongConfiguration() {
298 $varSize = ini_get('xcache.var_size');
299 return php_sapi_name() === 'cli' || empty($varSize);
300 }
301 }