[TASK] Replace ClassLoader cache with standard caches
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Core / ClassLoader.php
1 <?php
2 namespace TYPO3\CMS\Core\Core;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2013 Thomas Maroschik <tmaroschik@dfau.de>
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 * A copy is found in the textfile GPL.txt and important notices to the license
19 * from the author is found in LICENSE.txt distributed with these scripts.
20 *
21 *
22 * This script is distributed in the hope that it will be useful,
23 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 * GNU General Public License for more details.
26 *
27 * This copyright notice MUST APPEAR in all copies of the script!
28 ***************************************************************/
29
30 use TYPO3\CMS\Core\Package\Package;
31 use TYPO3\CMS\Core\Utility\GeneralUtility;
32
33 /**
34 * Class Loader implementation which loads .php files found in the classes
35 * directory of an object.
36 */
37 class ClassLoader {
38
39 /**
40 * @var ClassAliasMap
41 */
42 protected $classAliasMap;
43
44 /**
45 * @var ClassAliasMap
46 */
47 static protected $staticAliasMap;
48
49 /**
50 * @var \TYPO3\CMS\Core\Cache\Frontend\StringFrontend
51 */
52 protected $classesCache;
53
54 /**
55 * @var \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend
56 */
57 protected $coreCache;
58
59 /**
60 * @var string
61 */
62 protected $cacheIdentifier;
63
64 /**
65 * @var \TYPO3\Flow\Package\Package[]
66 */
67 protected $packages = array();
68
69 /**
70 * @var boolean
71 */
72 protected $isEarlyCache = TRUE;
73
74 /**
75 * @var array
76 */
77 protected $earlyClassInformationsFromAutoloadRegistry = array();
78
79 /**
80 * @var array A list of namespaces this class loader is definitely responsible for
81 */
82 protected $packageNamespaces = array(
83 'TYPO3\CMS\Core' => 14
84 );
85
86 /**
87 * @var array A list of packages and their replaces pointing to class paths
88 */
89 protected $packageClassesPaths = array();
90
91 public function __construct(ApplicationContext $context) {
92 $this->classesCache = new \TYPO3\CMS\Core\Cache\Frontend\StringFrontend('cache_classes', new \TYPO3\CMS\Core\Cache\Backend\TransientMemoryBackend($context));
93 }
94
95 /**
96 * Get class alias map list injected
97 *
98 * @param ClassAliasMap
99 */
100 public function injectClassAliasMap(ClassAliasMap $classAliasMap) {
101 $this->classAliasMap = $classAliasMap;
102 static::$staticAliasMap = $classAliasMap;
103 }
104
105 /**
106 * @param \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend $coreCache
107 */
108 public function injectCoreCache(\TYPO3\CMS\Core\Cache\Frontend\PhpFrontend $coreCache) {
109 $this->coreCache = $coreCache;
110 $this->classAliasMap->injectCoreCache($coreCache);
111 }
112
113 /**
114 * Get classes cache injected
115 *
116 * @param \TYPO3\CMS\Core\Cache\Frontend\StringFrontend $classesCache
117 */
118 public function injectClassesCache(\TYPO3\CMS\Core\Cache\Frontend\StringFrontend $classesCache) {
119 /** @var $earlyClassLoaderBackend \TYPO3\CMS\Core\Cache\Backend\TransientMemoryBackend */
120 $earlyClassesCache = $this->classesCache;
121 $this->classesCache = $classesCache;
122 $this->isEarlyCache = FALSE;
123 $this->classAliasMap->injectClassesCache($classesCache);
124 foreach ($earlyClassesCache->getByTag('early') as $originalClassLoadingInformation) {
125 $classLoadingInformation = explode("\xff", $originalClassLoadingInformation);
126 $cacheEntryIdentifier = strtolower(str_replace('\\', '_', $classLoadingInformation[1]));
127 if (!$this->classesCache->has($cacheEntryIdentifier)) {
128 $this->classesCache->set($cacheEntryIdentifier, $originalClassLoadingInformation);
129 }
130 }
131 }
132
133 /**
134 * Loads php files containing classes or interfaces found in the classes directory of
135 * a package and specifically registered classes.
136 *
137 * @param string $className Name of the class/interface to load
138 * @return boolean
139 */
140 public function loadClass($className) {
141 if ($className[0] === '\\') {
142 $className = substr($className, 1);
143 }
144
145 if (!$this->isValidClassName($className)) {
146 return FALSE;
147 }
148
149 $cacheEntryIdentifier = strtolower(str_replace('\\', '_', $className));
150 try {
151 if ($this->classesCache->has($cacheEntryIdentifier)) {
152 $classLoadingInformation = explode("\xff", $this->classesCache->get($cacheEntryIdentifier));
153 } else {
154 $classLoadingInformation = $this->buildClassLoadingInformation($className);
155 if ($classLoadingInformation !== NULL) {
156 $this->classesCache->set($cacheEntryIdentifier, implode("\xff", $classLoadingInformation), $this->isEarlyCache ? array('early') : array());
157 }
158 }
159 } catch (\InvalidArgumentException $exception) {
160 return FALSE;
161 }
162
163 // Class loading information structure
164 // array(
165 // 0 => class file path
166 // 1 => original class name
167 // 2 and following => alias class names
168 // )
169
170 $loadingSuccessful = FALSE;
171 if ($classLoadingInformation !== NULL) {
172 $loadingSuccessful = (bool)require_once $classLoadingInformation[0];
173 }
174 if ($loadingSuccessful && count($classLoadingInformation) > 2) {
175 $originalClassName = $classLoadingInformation[1];
176 foreach (array_slice($classLoadingInformation, 2) as $aliasClassName) {
177 $this->setAliasForClassName($aliasClassName, $originalClassName);
178 }
179 }
180
181 return $loadingSuccessful;
182 }
183
184 /**
185 * @param string $className
186 * @return array|null
187 */
188 public function buildClassLoadingInformation($className) {
189 $classLoadingInformation = $this->buildClassLoadingInformationForClassFromCorePackage($className);
190
191 if ($classLoadingInformation === NULL) {
192 $classLoadingInformation = $this->buildClassLoadingInformationForClassFromEarlyAutoloadRegistry($className);
193 }
194
195 if ($classLoadingInformation === NULL) {
196 $classLoadingInformation = $this->buildClassLoadingInformationForClassFromRegisteredPackages($className);
197 }
198
199 if ($classLoadingInformation === NULL) {
200 $classLoadingInformation = $this->buildClassLoadingInformationForClassByNamingConvention($className);
201 }
202
203 return $classLoadingInformation;
204 }
205
206 /**
207 * Find out if a class name is valid
208 *
209 * @param string $className
210 * @return boolean
211 */
212 protected function isValidClassName($className) {
213 return strpos($className, ' ') === FALSE;
214 }
215
216 /**
217 * Retrieve class loading information for class from core package
218 *
219 * @param string $className
220 * @return array|null
221 */
222 protected function buildClassLoadingInformationForClassFromCorePackage($className) {
223 if (substr($className, 0, 14) === 'TYPO3\\CMS\\Core') {
224 $classesFolder = substr($className, 15, 5) === 'Tests' ? '' : 'Classes/';
225 $classFilePath = PATH_typo3 . 'sysext/core/' . $classesFolder . str_replace('\\', '/', substr($className, 15)) . '.php';
226 if (@file_exists($classFilePath)) {
227 return array($classFilePath, $className);
228 }
229 }
230 return NULL;
231 }
232
233 /**
234 * Retrieve class loading information from early class name autoload registry cache
235 *
236 * @param string $className
237 * @return array|null
238 */
239 protected function buildClassLoadingInformationForClassFromEarlyAutoloadRegistry($className) {
240 $lowercasedClassName = strtolower($className);
241 if (isset($this->earlyClassInformationsFromAutoloadRegistry[$lowercasedClassName])) {
242 if (@file_exists($this->earlyClassInformationsFromAutoloadRegistry[$lowercasedClassName][0])) {
243 return $this->earlyClassInformationsFromAutoloadRegistry[$lowercasedClassName];
244 }
245 }
246 return NULL;
247 }
248
249 /**
250 * Retrieve class loading information from registered packages
251 *
252 * @param string $className
253 * @return array|null
254 */
255 protected function buildClassLoadingInformationForClassFromRegisteredPackages($className) {;
256 foreach ($this->packageNamespaces as $packageNamespace => $packageData) {
257 if (substr(str_replace('_', '\\', $className), 0, $packageData['namespaceLength']) === $packageNamespace) {
258 if ($packageData['substituteNamespaceInPath']) {
259 // If it's a TYPO3 package, classes don't comply to PSR-0.
260 // The namespace part is substituted.
261 $classPathAndFilename = '/' . str_replace('\\', '/', ltrim(substr($className, $packageData['namespaceLength']), '\\')) . '.php';
262 } else {
263 // make the classname PSR-0 compliant by replacing underscores only in the classname not in the namespace
264 $classPathAndFilename = '';
265 $lastNamespacePosition = strrpos($className, '\\');
266 if ($lastNamespacePosition !== FALSE) {
267 $namespace = substr($className, 0, $lastNamespacePosition);
268 $className = substr($className, $lastNamespacePosition + 1);
269 $classPathAndFilename = str_replace('\\', '/', $namespace) . '/';
270 }
271 $classPathAndFilename .= str_replace('_', '/', $className) . '.php';
272 }
273 if (strtolower(substr($className, $packageData['namespaceLength'], 5)) === 'tests') {
274 $classPathAndFilename = $packageData['packagePath'] . $classPathAndFilename;
275 } else {
276 $classPathAndFilename = $packageData['classesPath'] . $classPathAndFilename;
277 }
278 if (@file_exists($classPathAndFilename)) {
279 return array($classPathAndFilename, $className);
280 }
281 }
282 }
283 return NULL;
284 }
285
286 /**
287 * Retrieve class loading information based on 'extbase' naming convention into the registry.
288 *
289 * @param string $className Class name to find source file of
290 * @return array|null
291 */
292 protected function buildClassLoadingInformationForClassByNamingConvention($className) {
293 $delimiter = '_';
294 // To handle namespaced class names, split the class name at the
295 // namespace delimiters.
296 if (strpos($className, '\\') !== FALSE) {
297 $delimiter = '\\';
298 }
299
300 $classNameParts = explode($delimiter, $className, 4);
301
302 // We only handle classes that follow the convention Vendor\Product\Classname or is longer
303 // so we won't deal with class names that only have one or two parts
304 if (count($classNameParts) <= 2) {
305 return NULL;
306 }
307
308 if (isset($classNameParts[0]) && isset($classNameParts[1])
309 && $classNameParts[0] === 'TYPO3' && $classNameParts[1] === 'CMS'
310 ) {
311 $extensionKey = GeneralUtility::camelCaseToLowerCaseUnderscored($classNameParts[2]);
312 $classNameWithoutVendorAndProduct = $classNameParts[3];
313 } else {
314 $extensionKey = GeneralUtility::camelCaseToLowerCaseUnderscored($classNameParts[1]);
315 $classNameWithoutVendorAndProduct = $classNameParts[2];
316
317 if (isset($classNameParts[3])) {
318 $classNameWithoutVendorAndProduct .= $delimiter . $classNameParts[3];
319 }
320 }
321
322 if ($extensionKey && isset($this->packageClassesPaths[$extensionKey])) {
323 if (substr(strtolower($classNameWithoutVendorAndProduct), 0, 5) === 'tests') {
324 $classesPath = $this->packages[$extensionKey]->getPackagePath();
325 } else {
326 $classesPath = $this->packageClassesPaths[$extensionKey];
327 }
328 $classFilePath = $classesPath . strtr($classNameWithoutVendorAndProduct, $delimiter, '/') . '.php';
329 if (@file_exists($classFilePath)) {
330 return array($classFilePath, $className);
331 }
332 }
333
334 return NULL;
335 }
336
337 /**
338 * Get cache identifier
339 *
340 * @return string identifier
341 */
342 protected function getCacheIdentifier() {
343 return $this->cacheIdentifier;
344 }
345
346 /**
347 * Get cache entry identifier
348 *
349 * @return string|null identifier
350 */
351 protected function getCacheEntryIdentifier() {
352 return $this->getCacheIdentifier() !== NULL
353 ? 'ClassLoader_' . $this->getCacheIdentifier()
354 : NULL;
355 }
356
357 /**
358 * Set cache identifier
359 *
360 * @param string $cacheIdentifier Cache identifier
361 * @return ClassLoader
362 */
363 public function setCacheIdentifier($cacheIdentifier) {
364 $this->cacheIdentifier = $cacheIdentifier;
365 $this->classAliasMap->setCacheIdentifier($cacheIdentifier);
366 return $this;
367 }
368
369 /**
370 * Sets the available packages
371 *
372 * @param array $packages An array of \TYPO3\Flow\Package\Package objects
373 * @return ClassLoader
374 */
375 public function setPackages(array $packages) {
376 $this->packages = $packages;
377 if (!$this->loadPackageNamespacesFromCache()) {
378 $this->buildPackageNamespaces();
379 $this->buildPackageClassesPathsForLegacyExtensions();
380 $this->savePackageNamespacesAndClassesPathsToCache();
381 // Rebuild the class alias map too because ext_autoload can contain aliases
382 $classNameToAliasMapping = $this->classAliasMap->setPackagesButDontBuildMappingFilesReturnClassNameToAliasMappingInstead($packages);
383 $this->buildClassInformationsFromFullAutoloadRegistry();
384 $this->classAliasMap->buildMappingFiles($classNameToAliasMapping);
385 } else {
386 $this->classAliasMap->setPackages($packages);
387 }
388 return $this;
389 }
390
391 /**
392 * Load package namespaces from cache
393 *
394 * @return boolean TRUE if package namespaces were loaded
395 */
396 protected function loadPackageNamespacesFromCache() {
397 $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
398 if ($cacheEntryIdentifier !== NULL && $this->coreCache->has($cacheEntryIdentifier)) {
399 list($packageNamespaces, $packageClassesPaths) = $this->coreCache->requireOnce($cacheEntryIdentifier);
400 if (is_array($packageNamespaces) && is_array($packageClassesPaths)) {
401 $this->packageNamespaces = $packageNamespaces;
402 $this->packageClassesPaths = $packageClassesPaths;
403 return TRUE;
404 }
405 }
406 return FALSE;
407 }
408
409 /**
410 * Build package namespaces
411 *
412 * @return void
413 */
414 protected function buildPackageNamespaces() {
415 /** @var $package \TYPO3\Flow\Package\Package */
416 foreach ($this->packages as $package) {
417 $packageNamespace = $package->getNamespace();
418 // Ignore legacy extensions with unknown vendor name
419 if ($packageNamespace[0] !== '*') {
420 $this->packageNamespaces[$packageNamespace] = array(
421 'namespaceLength' => strlen($packageNamespace),
422 'classesPath' => $package->getClassesPath(),
423 'packagePath' => $package->getPackagePath(),
424 'substituteNamespaceInPath' => ($package instanceof Package)
425 );
426 }
427 }
428 // Sort longer package namespaces first, to find specific matches before generic ones
429 $sortPackages = function($a, $b) {
430 if (($lenA = strlen($a)) === ($lenB = strlen($b))) {
431 return strcmp($a, $b);
432 }
433 return ($lenA > $lenB) ? -1 : 1;
434 };
435 uksort($this->packageNamespaces, $sortPackages);
436 }
437
438 /**
439 * Builds the early autoload registry, also for usage in the class alias map
440 *
441 * @return void
442 */
443 protected function buildClassInformationsFromFullAutoloadRegistry() {
444 $classFileAutoloadRegistry = array();
445 foreach ($this->packages as $package) {
446 /** @var $package Package */
447 if ($package instanceof Package) {
448 $classFilesFromAutoloadRegistry = $package->getClassFilesFromAutoloadRegistry();
449 if (is_array($classFilesFromAutoloadRegistry)) {
450 $classFileAutoloadRegistry = array_merge($classFileAutoloadRegistry, $classFilesFromAutoloadRegistry);
451 }
452 }
453 }
454 foreach ($classFileAutoloadRegistry as $className => $classFilePath) {
455 $lowercasedClassName = strtolower($className);
456 if (!isset($this->earlyClassInformationsFromAutoloadRegistry[$lowercasedClassName]) && @file_exists($classFilePath)) {
457 $this->earlyClassInformationsFromAutoloadRegistry[$lowercasedClassName] = array($classFilePath, $className);
458 }
459 }
460 }
461
462 /**
463 * Builds the classes paths for legacy extensions with unknown vendor name
464 *
465 * @return void
466 */
467 protected function buildPackageClassesPathsForLegacyExtensions() {
468 foreach ($this->packages as $package) {
469 if ($package instanceof \TYPO3\CMS\Core\Package\PackageInterface) {
470 $classesPath = $package->getClassesPath();
471 $this->packageClassesPaths[$package->getPackageKey()] = $classesPath;
472 foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
473 $this->packageClassesPaths[$packageToReplace] = $classesPath;
474 }
475 }
476 }
477 }
478
479 /**
480 * Save package namespaces and classes paths to cache
481 *
482 * @return void
483 */
484 protected function savePackageNamespacesAndClassesPathsToCache() {
485 $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
486 if ($cacheEntryIdentifier !== NULL) {
487 $this->coreCache->set(
488 $this->getCacheEntryIdentifier(),
489 'return ' . var_export(array($this->packageNamespaces, $this->packageClassesPaths), TRUE) . ';'
490 );
491 }
492 }
493
494 /**
495 * This method is necessary for the early loading of the cores autoload registry
496 *
497 * @param array $classFileAutoloadRegistry
498 * @return void
499 */
500 public function setEarlyClassInformationsFromAutoloadRegistry($classFileAutoloadRegistry) {
501 foreach ($classFileAutoloadRegistry as $className => $classFilePath) {
502 $lowercasedClassName = strtolower($className);
503 if (!isset($this->earlyClassInformationsFromAutoloadRegistry[$lowercasedClassName])) {
504 $this->earlyClassInformationsFromAutoloadRegistry[$lowercasedClassName] = array($classFilePath, $className);
505 }
506 }
507 }
508
509 /**
510 * Set alias for class name
511 *
512 * @param string $aliasClassName
513 * @param string $originalClassName
514 * @return boolean
515 */
516 public function setAliasForClassName($aliasClassName, $originalClassName) {
517 return $this->classAliasMap->setAliasForClassName($aliasClassName, $originalClassName);
518 }
519
520 /**
521 * Get class name for alias
522 *
523 * @param string $alias
524 * @return mixed
525 */
526 static public function getClassNameForAlias($alias) {
527 return static::$staticAliasMap->getClassNameForAlias($alias);
528 }
529
530 /**
531 * Get alias for class name
532 *
533 * @param string $className
534 * @deprecated since 6.2, use getAliasesForClassName instead. will be removed 2 versions later
535 * @return mixed
536 */
537 static public function getAliasForClassName($className) {
538 $aliases = static::$staticAliasMap->getAliasesForClassName($className);
539 return is_array($aliases) && isset($aliases[0]) ? $aliases[0] : NULL;
540 }
541
542 /**
543 * Get an aliases for a class name
544 *
545 * @param string $className
546 * @return mixed
547 */
548 static public function getAliasesForClassName($className) {
549 return static::$staticAliasMap->getAliasesForClassName($className);
550 }
551
552 }