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