[BUGFIX] Flush caches in group should throw exception
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Cache / CacheManager.php
1 <?php
2 namespace TYPO3\CMS\Core\Cache;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2009-2013 Ingo Renner <ingo@typo3.org>
8 * All rights reserved
9 *
10 * This script is part of the TYPO3 project. The TYPO3 project is
11 * free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * The GNU General Public License can be found at
17 * http://www.gnu.org/copyleft/gpl.html.
18 *
19 * This script is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * This copyright notice MUST APPEAR in all copies of the script!
25 ***************************************************************/
26
27 use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheGroupException;
28
29
30 /**
31 * The Cache Manager
32 *
33 * This file is a backport from FLOW3
34 *
35 * @author Robert Lemke <robert@typo3.org>
36 * @scope singleton
37 * @api
38 */
39 class CacheManager implements \TYPO3\CMS\Core\SingletonInterface {
40
41 /**
42 * @var \TYPO3\CMS\Core\Cache\CacheFactory
43 */
44 protected $cacheFactory;
45
46 /**
47 * @var \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface[]
48 */
49 protected $caches = array();
50
51 /**
52 * @var array
53 */
54 protected $cacheConfigurations = array();
55
56 /**
57 * Used to flush caches of a specific group
58 * is an associative array containing the group identifier as key
59 * and the identifier as an array within that group
60 * groups are set via the cache configurations of each cache.
61 *
62 * @var array
63 */
64 protected $cacheGroups = array();
65
66 /**
67 * @var array Default cache configuration as fallback
68 */
69 protected $defaultCacheConfiguration = array(
70 'frontend' => 'TYPO3\\CMS\\Core\\Cache\\Frontend\\VariableFrontend',
71 'backend' => 'TYPO3\\CMS\\Core\\Cache\\Backend\\Typo3DatabaseBackend',
72 'options' => array(),
73 'groups' => array('all')
74 );
75
76 /**
77 * @param \TYPO3\CMS\Core\Cache\CacheFactory $cacheFactory
78 * @return void
79 */
80 public function injectCacheFactory(\TYPO3\CMS\Core\Cache\CacheFactory $cacheFactory) {
81 $this->cacheFactory = $cacheFactory;
82 }
83
84 /**
85 * Sets configurations for caches. The key of each entry specifies the
86 * cache identifier and the value is an array of configuration options.
87 * Possible options are:
88 *
89 * frontend
90 * backend
91 * backendOptions
92 *
93 * If one of the options is not specified, the default value is assumed.
94 * Existing cache configurations are preserved.
95 *
96 * @param array $cacheConfigurations The cache configurations to set
97 * @return void
98 * @throws \InvalidArgumentException If $cacheConfigurations is not an array
99 */
100 public function setCacheConfigurations(array $cacheConfigurations) {
101 foreach ($cacheConfigurations as $identifier => $configuration) {
102 if (!is_array($configuration)) {
103 throw new \InvalidArgumentException('The cache configuration for cache "' . $identifier . '" was not an array as expected.', 1231259656);
104 }
105 $this->cacheConfigurations[$identifier] = $configuration;
106 }
107 }
108
109 /**
110 * Registers a cache so it can be retrieved at a later point.
111 *
112 * @param \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache The cache frontend to be registered
113 * @return void
114 * @throws \TYPO3\CMS\Core\Cache\Exception\DuplicateIdentifierException if a cache with the given identifier has already been registered.
115 * @api
116 */
117 public function registerCache(\TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache) {
118 $identifier = $cache->getIdentifier();
119 if (isset($this->caches[$identifier])) {
120 throw new \TYPO3\CMS\Core\Cache\Exception\DuplicateIdentifierException('A cache with identifier "' . $identifier . '" has already been registered.', 1203698223);
121 }
122 $this->caches[$identifier] = $cache;
123 }
124
125 /**
126 * Returns the cache specified by $identifier
127 *
128 * @param string $identifier Identifies which cache to return
129 * @return \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface The specified cache frontend
130 * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
131 * @api
132 */
133 public function getCache($identifier) {
134 if ($this->hasCache($identifier) === FALSE) {
135 throw new \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException('A cache with identifier "' . $identifier . '" does not exist.', 1203699034);
136 }
137 if (!isset($this->caches[$identifier])) {
138 $this->createCache($identifier);
139 }
140 return $this->caches[$identifier];
141 }
142
143 /**
144 * Checks if the specified cache has been registered.
145 *
146 * @param string $identifier The identifier of the cache
147 * @return boolean TRUE if a cache with the given identifier exists, otherwise FALSE
148 * @api
149 */
150 public function hasCache($identifier) {
151 return isset($this->caches[$identifier]) || isset($this->cacheConfigurations[$identifier]);
152 }
153
154 /**
155 * Flushes all registered caches
156 *
157 * @return void
158 * @api
159 */
160 public function flushCaches() {
161 $this->createAllCaches();
162 foreach ($this->caches as $cache) {
163 $cache->flush();
164 }
165 }
166
167 /**
168 * Flushes all registered caches of a specific group
169 *
170 * @param string $groupIdentifier
171 * @return void
172 * @throws NoSuchCacheGroupException
173 * @api
174 */
175 public function flushCachesInGroup($groupIdentifier) {
176 $this->createAllCaches();
177 if (isset($this->cacheGroups[$groupIdentifier])) {
178 foreach ($this->cacheGroups[$groupIdentifier] as $cacheIdentifier) {
179 if (isset($this->caches[$cacheIdentifier])) {
180 $this->caches[$cacheIdentifier]->flush();
181 }
182 }
183 } else {
184 throw new NoSuchCacheGroupException('No cache in the specified group \'' . $groupIdentifier . '\'', 1390334120);
185 }
186 }
187
188 /**
189 * Flushes entries tagged by the specified tag of all registered
190 * caches of a specific group.
191 *
192 * @param string $groupIdentifier
193 * @param string $tag Tag to search for
194 * @return void
195 * @throws NoSuchCacheGroupException
196 * @api
197 */
198 public function flushCachesInGroupByTag($groupIdentifier, $tag) {
199 $this->createAllCaches();
200 if (isset($this->cacheGroups[$groupIdentifier])) {
201 foreach ($this->cacheGroups[$groupIdentifier] as $cacheIdentifier) {
202 if (isset($this->caches[$cacheIdentifier])) {
203 $this->caches[$cacheIdentifier]->flushByTag($tag);
204 }
205 }
206 } else {
207 throw new NoSuchCacheGroupException('No cache in the specified group \'' . $groupIdentifier . '\'', 1390337129);
208 }
209 }
210
211
212 /**
213 * Flushes entries tagged by the specified tag of all registered
214 * caches.
215 *
216 * @param string $tag Tag to search for
217 * @return void
218 * @api
219 */
220 public function flushCachesByTag($tag) {
221 $this->createAllCaches();
222 foreach ($this->caches as $cache) {
223 $cache->flushByTag($tag);
224 }
225 }
226
227 /**
228 * TYPO3 v4 note: This method is a direct backport from FLOW3 and currently
229 * unused in TYPO3 v4 context.
230 *
231 * Flushes entries tagged with class names if their class source files have changed.
232 * Also flushes AOP proxy caches if a policy was modified.
233 *
234 * This method is used as a slot for a signal sent by the system file monitor
235 * defined in the bootstrap scripts.
236 *
237 * Note: Policy configuration handling is implemented here as well as other parts
238 * of FLOW3 (like the security framework) are not fully initialized at the
239 * time needed.
240 *
241 * @param string $fileMonitorIdentifier Identifier of the File Monitor
242 * @param array $changedFiles A list of full paths to changed files
243 * @return void
244 */
245 public function flushClassFileCachesByChangedFiles($fileMonitorIdentifier, array $changedFiles) {
246 $modifiedClassNamesWithUnderscores = array();
247 $objectClassesCache = $this->getCache('FLOW3_Object_Classes');
248 $objectConfigurationCache = $this->getCache('FLOW3_Object_Configuration');
249 switch ($fileMonitorIdentifier) {
250 case 'FLOW3_ClassFiles':
251 $modifiedAspectClassNamesWithUnderscores = array();
252 foreach ($changedFiles as $pathAndFilename => $status) {
253 $pathAndFilename = str_replace(FLOW3_PATH_PACKAGES, '', $pathAndFilename);
254 $matches = array();
255 if (preg_match('/[^\\/]+\\/(.+)\\/(Classes|Tests)\\/(.+)\\.php/', $pathAndFilename, $matches) === 1) {
256 $classNameWithUnderscores = str_replace(array('/', '.'), '_', $matches[1] . '_' . ($matches[2] === 'Tests' ? 'Tests_' : '') . $matches[3]);
257 $modifiedClassNamesWithUnderscores[$classNameWithUnderscores] = TRUE;
258 // If an aspect was modified, the whole code cache needs to be flushed, so keep track of them:
259 if (substr($classNameWithUnderscores, -6, 6) === 'Aspect') {
260 $modifiedAspectClassNamesWithUnderscores[$classNameWithUnderscores] = TRUE;
261 }
262 // As long as no modified aspect was found, we are optimistic that only part of the cache needs to be flushed:
263 if (count($modifiedAspectClassNamesWithUnderscores) === 0) {
264 $objectClassesCache->remove($classNameWithUnderscores);
265 }
266 }
267 }
268 $flushDoctrineProxyCache = FALSE;
269 if (count($modifiedClassNamesWithUnderscores) > 0) {
270 $reflectionStatusCache = $this->getCache('FLOW3_Reflection_Status');
271 foreach (array_keys($modifiedClassNamesWithUnderscores) as $classNameWithUnderscores) {
272 $reflectionStatusCache->remove($classNameWithUnderscores);
273 if ($flushDoctrineProxyCache === FALSE && preg_match('/_Domain_Model_(.+)/', $classNameWithUnderscores) === 1) {
274 $flushDoctrineProxyCache = TRUE;
275 }
276 }
277 $objectConfigurationCache->remove('allCompiledCodeUpToDate');
278 }
279 if (count($modifiedAspectClassNamesWithUnderscores) > 0) {
280 $this->systemLogger->log('Aspect classes have been modified, flushing the whole proxy classes cache.', LOG_INFO);
281 $objectClassesCache->flush();
282 }
283 if ($flushDoctrineProxyCache === TRUE) {
284 $this->systemLogger->log('Domain model changes have been detected, triggering Doctrine 2 proxy rebuilding.', LOG_INFO);
285 $objectConfigurationCache->remove('doctrineProxyCodeUpToDate');
286 }
287 break;
288 case 'FLOW3_ConfigurationFiles':
289 $policyChangeDetected = FALSE;
290 $routesChangeDetected = FALSE;
291 foreach (array_keys($changedFiles) as $pathAndFilename) {
292 $filename = basename($pathAndFilename);
293 if (!in_array($filename, array('Policy.yaml', 'Routes.yaml'))) {
294 continue;
295 }
296 if ($policyChangeDetected === FALSE && $filename === 'Policy.yaml') {
297 $this->systemLogger->log('The security policies have changed, flushing the policy cache.', LOG_INFO);
298 $this->getCache('FLOW3_Security_Policy')->flush();
299 $policyChangeDetected = TRUE;
300 } elseif ($routesChangeDetected === FALSE && $filename === 'Routes.yaml') {
301 $this->systemLogger->log('A Routes.yaml file has been changed, flushing the routing cache.', LOG_INFO);
302 $this->getCache('FLOW3_Mvc_Routing_FindMatchResults')->flush();
303 $this->getCache('FLOW3_Mvc_Routing_Resolve')->flush();
304 $routesChangeDetected = TRUE;
305 }
306 }
307 $this->systemLogger->log('The configuration has changed, triggering an AOP proxy class rebuild.', LOG_INFO);
308 $objectConfigurationCache->remove('allAspectClassesUpToDate');
309 $objectConfigurationCache->remove('allCompiledCodeUpToDate');
310 $objectClassesCache->flush();
311 break;
312 case 'FLOW3_TranslationFiles':
313 foreach ($changedFiles as $pathAndFilename => $status) {
314 $matches = array();
315 if (preg_match('/\\/Translations\\/.+\\.xlf/', $pathAndFilename, $matches) === 1) {
316 $this->systemLogger->log('The localization files have changed, thus flushing the I18n XML model cache.', LOG_INFO);
317 $this->getCache('FLOW3_I18n_XmlModelCache')->flush();
318 break;
319 }
320 }
321 break;
322 }
323 }
324
325 /**
326 * TYPO3 v4 note: This method is a direct backport from FLOW3 and currently
327 * unused in TYPO3 v4 context.
328 *
329 * Renders a tag which can be used to mark a cache entry as "depends on this class".
330 * Whenever the specified class is modified, all cache entries tagged with the
331 * class are flushed.
332 *
333 * If an empty string is specified as class name, the returned tag means
334 * "this cache entry becomes invalid if any of the known classes changes".
335 *
336 * @param string $className The class name
337 * @return string Class Tag
338 * @api
339 */
340 static public function getClassTag($className = '') {
341 return $className === '' ? \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface::TAG_CLASS : \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface::TAG_CLASS . str_replace('\\', '_', $className);
342 }
343
344 /**
345 * Instantiates all registered caches.
346 *
347 * @return void
348 */
349 protected function createAllCaches() {
350 foreach (array_keys($this->cacheConfigurations) as $identifier) {
351 if (!isset($this->caches[$identifier])) {
352 $this->createCache($identifier);
353 }
354 }
355 }
356
357 /**
358 * Instantiates the cache for $identifier.
359 *
360 * @param string $identifier
361 * @return void
362 */
363 protected function createCache($identifier) {
364 if (isset($this->cacheConfigurations[$identifier]['frontend'])) {
365 $frontend = $this->cacheConfigurations[$identifier]['frontend'];
366 } else {
367 $frontend = $this->defaultCacheConfiguration['frontend'];
368 }
369 if (isset($this->cacheConfigurations[$identifier]['backend'])) {
370 $backend = $this->cacheConfigurations[$identifier]['backend'];
371 } else {
372 $backend = $this->defaultCacheConfiguration['backend'];
373 }
374 if (isset($this->cacheConfigurations[$identifier]['options'])) {
375 $backendOptions = $this->cacheConfigurations[$identifier]['options'];
376 } else {
377 $backendOptions = $this->defaultCacheConfiguration['options'];
378 }
379
380 // Add the cache identifier to the groups that it should be attached to, or use the default ones.
381 if (isset($this->cacheConfigurations[$identifier]['groups']) && is_array($this->cacheConfigurations[$identifier]['groups'])) {
382 $assignedGroups = $this->cacheConfigurations[$identifier]['groups'];
383 } else {
384 $assignedGroups = $this->defaultCacheConfiguration['groups'];
385 }
386 foreach ($assignedGroups as $groupIdentifier) {
387 if (!isset($this->cacheGroups[$groupIdentifier])) {
388 $this->cacheGroups[$groupIdentifier] = array();
389 }
390 $this->cacheGroups[$groupIdentifier][] = $identifier;
391 }
392
393 $this->cacheFactory->create($identifier, $frontend, $backend, $backendOptions);
394 }
395
396 }