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