[CLEANUP] ClassLoader
[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 text file 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\PackageInterface;
31 use TYPO3\CMS\Core\Utility\GeneralUtility;
32 use TYPO3\CMS\Core\Cache;
33
34 /**
35 * Class Loader implementation which loads .php files found in the classes
36 * directory of an object.
37 */
38 class ClassLoader {
39
40 /**
41 * @var ClassAliasMap
42 */
43 protected $classAliasMap;
44
45 /**
46 * @var ClassAliasMap
47 */
48 static protected $staticAliasMap;
49
50 /**
51 * @var \TYPO3\CMS\Core\Cache\Frontend\StringFrontend
52 */
53 protected $classesCache;
54
55 /**
56 * @var \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend
57 */
58 protected $coreCache;
59
60 /**
61 * @var string
62 */
63 protected $cacheIdentifier;
64
65 /**
66 * @var \TYPO3\Flow\Package\Package[]
67 */
68 protected $packages = array();
69
70 /**
71 * @var bool
72 */
73 protected $isEarlyCache = TRUE;
74
75 /**
76 * @var array
77 */
78 protected $runtimeClassLoadingInformationCache = array();
79
80 /**
81 * @var array A list of namespaces this class loader is definitely responsible for
82 */
83 protected $packageNamespaces = array();
84
85 /**
86 * @var array A list of packages and their replaces pointing to class paths
87 */
88 protected $packageClassesPaths = array();
89
90 /**
91 * Constructor
92 *
93 * @param ApplicationContext $context
94 */
95 public function __construct(ApplicationContext $context) {
96 $this->classesCache = new Cache\Frontend\StringFrontend('cache_classes', new Cache\Backend\TransientMemoryBackend($context));
97 }
98
99 /**
100 * Get class alias map list injected
101 *
102 * @param ClassAliasMap
103 */
104 public function injectClassAliasMap(ClassAliasMap $classAliasMap) {
105 $this->classAliasMap = $classAliasMap;
106 static::$staticAliasMap = $classAliasMap;
107 }
108
109 /**
110 * Get core cache injected
111 *
112 * @param \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend $coreCache
113 */
114 public function injectCoreCache(Cache\Frontend\PhpFrontend $coreCache) {
115 $this->coreCache = $coreCache;
116 $this->classAliasMap->injectCoreCache($coreCache);
117 }
118
119 /**
120 * Get classes cache injected
121 *
122 * @param \TYPO3\CMS\Core\Cache\Frontend\StringFrontend $classesCache
123 */
124 public function injectClassesCache(Cache\Frontend\StringFrontend $classesCache) {
125 $earlyClassesCache = $this->classesCache;
126 $this->classesCache = $classesCache;
127 $this->isEarlyCache = FALSE;
128 $this->classAliasMap->injectClassesCache($classesCache);
129 foreach ($earlyClassesCache->getByTag('early') as $originalClassLoadingInformation) {
130 $classLoadingInformation = explode("\xff", $originalClassLoadingInformation);
131 $cacheEntryIdentifier = strtolower(str_replace('\\', '_', $classLoadingInformation[1]));
132 if (!$this->classesCache->has($cacheEntryIdentifier)) {
133 $this->classesCache->set($cacheEntryIdentifier, $originalClassLoadingInformation);
134 }
135 }
136 }
137
138 /**
139 * Loads php files containing classes or interfaces found in the classes directory of
140 * a package and specifically registered classes.
141 *
142 * @param string $className Name of the class/interface to load
143 * @return bool
144 */
145 public function loadClass($className) {
146 if ($className[0] === '\\') {
147 $className = substr($className, 1);
148 }
149
150 if (!$this->isValidClassName($className)) {
151 return FALSE;
152 }
153
154 $cacheEntryIdentifier = strtolower(str_replace('\\', '_', $className));
155 $classLoadingInformation = $this->getClassLoadingInformationFromCache($cacheEntryIdentifier);
156 if ($classLoadingInformation === NULL) {
157 $classLoadingInformation = $this->buildCachedClassLoadingInformation($cacheEntryIdentifier, $className);
158 }
159
160 // Class loading information structure
161 // array(
162 // 0 => class file path
163 // 1 => original class name
164 // 2 and following => alias class names
165 // )
166 $loadingSuccessful = FALSE;
167 if ($classLoadingInformation !== NULL) {
168 // The call to class_exists fixes a rare case when early instances need to be aliased
169 // but PHP fails to recognize the real path of the class. See #55904
170 $loadingSuccessful = class_exists($classLoadingInformation[1], FALSE) || (bool)require_once $classLoadingInformation[0];
171 }
172 if ($loadingSuccessful && count($classLoadingInformation) > 2) {
173 $originalClassName = $classLoadingInformation[1];
174 foreach (array_slice($classLoadingInformation, 2) as $aliasClassName) {
175 $this->setAliasForClassName($aliasClassName, $originalClassName);
176 }
177 }
178
179 return $loadingSuccessful;
180 }
181
182 /**
183 * Get class loading information for the given identifier for cache
184 *
185 * @param string $cacheEntryIdentifier The identifier to fetch entry from cache
186 * @return array|null The class information or NULL if class information was not found in cache
187 */
188 public function getClassLoadingInformationFromCache($cacheEntryIdentifier) {
189 try {
190 $rawClassLoadingInformation = $this->classesCache->get($cacheEntryIdentifier);
191 } catch (\InvalidArgumentException $exception) {
192 return NULL;
193 }
194
195 if ($rawClassLoadingInformation !== FALSE && $rawClassLoadingInformation !== '') {
196 return explode("\xff", $rawClassLoadingInformation);
197 }
198 return NULL;
199 }
200
201 /**
202 * Builds the class loading information and writes it to the cache. It handles Locking for this cache.
203 *
204 * @param string $cacheEntryIdentifier Cache identifier for this class
205 * @param string $className Name of class this information is for
206 *
207 * @return array|null The class information or NULL if class was not found
208 */
209 protected function buildCachedClassLoadingInformation($cacheEntryIdentifier, $className) {
210 // Look again into cache after we got the look
211 $classLoadingInformation = $this->getClassLoadingInformationFromCache($cacheEntryIdentifier);
212 if ($classLoadingInformation === NULL) {
213 $classLoadingInformation = $this->buildClassLoadingInformation($className);
214
215 if ($classLoadingInformation !== NULL) {
216 $this->classesCache->set(
217 $cacheEntryIdentifier,
218 implode("\xff", $classLoadingInformation),
219 $this->isEarlyCache ? array('early') : array()
220 );
221 } elseif (!$this->isEarlyCache) {
222 $this->classesCache->set($cacheEntryIdentifier, '');
223 }
224 }
225 return $classLoadingInformation;
226 }
227
228 /**
229 * @param string $className
230 * @return array|null
231 */
232 public function buildClassLoadingInformation($className) {
233 $classLoadingInformation = $this->buildClassLoadingInformationForClassFromCorePackage($className);
234
235 if ($classLoadingInformation === NULL) {
236 $classLoadingInformation = $this->fetchClassLoadingInformationFromRuntimeCache($className);
237 }
238
239 if ($classLoadingInformation === NULL) {
240 $classLoadingInformation = $this->buildClassLoadingInformationForClassFromRegisteredPackages($className);
241 }
242
243 if ($classLoadingInformation === NULL) {
244 $classLoadingInformation = $this->buildClassLoadingInformationForClassByNamingConvention($className);
245 }
246
247 return $classLoadingInformation;
248 }
249
250 /**
251 * Find out if a class name is valid
252 *
253 * @param string $className
254 * @return bool
255 */
256 protected function isValidClassName($className) {
257 return strpos($className, ' ') === FALSE;
258 }
259
260 /**
261 * Retrieve class loading information for class from core package
262 *
263 * @param string $className
264 * @return array|null
265 */
266 protected function buildClassLoadingInformationForClassFromCorePackage($className) {
267 if (substr($className, 0, 14) === 'TYPO3\\CMS\\Core') {
268 $classesFolder = substr($className, 15, 5) === 'Tests' ? '' : 'Classes/';
269 $classFilePath = PATH_typo3 . 'sysext/core/' . $classesFolder . str_replace('\\', '/', substr($className, 15)) . '.php';
270 if (@file_exists($classFilePath)) {
271 return array($classFilePath, $className);
272 }
273 }
274 return NULL;
275 }
276
277 /**
278 * Retrieve class loading information from early class name autoload registry cache
279 *
280 * @param string $className
281 * @return array|null
282 */
283 protected function fetchClassLoadingInformationFromRuntimeCache($className) {
284 $lowercasedClassName = strtolower($className);
285 if (!isset($this->runtimeClassLoadingInformationCache[$lowercasedClassName])) {
286 return NULL;
287 }
288 $classInformation = $this->runtimeClassLoadingInformationCache[$lowercasedClassName];
289 return @file_exists($classInformation[0]) ? $classInformation : NULL;
290 }
291
292 /**
293 * Retrieve class loading information from registered packages
294 *
295 * @param string $className
296 * @return array|null
297 */
298 protected function buildClassLoadingInformationForClassFromRegisteredPackages($className) {;
299 foreach ($this->packageNamespaces as $packageNamespace => $packageData) {
300 if (substr(str_replace('_', '\\', $className), 0, $packageData['namespaceLength']) === $packageNamespace) {
301 if ($packageData['substituteNamespaceInPath']) {
302 // If it's a TYPO3 package, classes don't comply to PSR-0.
303 // The namespace part is substituted.
304 $classPathAndFilename = '/' . str_replace('\\', '/', ltrim(substr($className, $packageData['namespaceLength']), '\\')) . '.php';
305 } else {
306 // Make the classname PSR-0 compliant by replacing underscores only in the classname not in the namespace
307 $classPathAndFilename = '';
308 $lastNamespacePosition = strrpos($className, '\\');
309 if ($lastNamespacePosition !== FALSE) {
310 $namespace = substr($className, 0, $lastNamespacePosition);
311 $className = substr($className, $lastNamespacePosition + 1);
312 $classPathAndFilename = str_replace('\\', '/', $namespace) . '/';
313 }
314 $classPathAndFilename .= str_replace('_', '/', $className) . '.php';
315 }
316 if (strtolower(substr($className, $packageData['namespaceLength'], 5)) === 'tests') {
317 $classPathAndFilename = $packageData['packagePath'] . $classPathAndFilename;
318 } else {
319 $classPathAndFilename = $packageData['classesPath'] . $classPathAndFilename;
320 }
321 if (@file_exists($classPathAndFilename)) {
322 return array($classPathAndFilename, $className);
323 }
324 }
325 }
326 return NULL;
327 }
328
329 /**
330 * Retrieve class loading information based on 'extbase' naming convention into the registry.
331 *
332 * @param string $className Class name to find source file of
333 * @return array|null
334 */
335 protected function buildClassLoadingInformationForClassByNamingConvention($className) {
336 $delimiter = '_';
337 // To handle namespaced class names, split the class name at the
338 // namespace delimiters.
339 if (strpos($className, '\\') !== FALSE) {
340 $delimiter = '\\';
341 }
342
343 $classNameParts = explode($delimiter, $className, 4);
344
345 // We only handle classes that follow the convention Vendor\Product\Classname or is longer
346 // so we won't deal with class names that only have one or two parts
347 if (count($classNameParts) <= 2) {
348 return NULL;
349 }
350
351 if (
352 isset($classNameParts[0])
353 && isset($classNameParts[1])
354 && $classNameParts[0] === 'TYPO3'
355 && $classNameParts[1] === 'CMS'
356 ) {
357 $extensionKey = GeneralUtility::camelCaseToLowerCaseUnderscored($classNameParts[2]);
358 $classNameWithoutVendorAndProduct = $classNameParts[3];
359 } else {
360 $extensionKey = GeneralUtility::camelCaseToLowerCaseUnderscored($classNameParts[1]);
361 $classNameWithoutVendorAndProduct = $classNameParts[2];
362
363 if (isset($classNameParts[3])) {
364 $classNameWithoutVendorAndProduct .= $delimiter . $classNameParts[3];
365 }
366 }
367
368 if ($extensionKey && isset($this->packageClassesPaths[$extensionKey])) {
369 if (substr(strtolower($classNameWithoutVendorAndProduct), 0, 5) === 'tests') {
370 $classesPath = $this->packages[$extensionKey]->getPackagePath();
371 } else {
372 $classesPath = $this->packageClassesPaths[$extensionKey];
373 }
374 $classFilePath = $classesPath . strtr($classNameWithoutVendorAndProduct, $delimiter, '/') . '.php';
375 if (@file_exists($classFilePath)) {
376 return array($classFilePath, $className);
377 }
378 }
379
380 return NULL;
381 }
382
383 /**
384 * Get cache entry identifier for the package namespaces cache
385 *
386 * @return string|null identifier
387 */
388 protected function getCacheEntryIdentifier() {
389 return $this->cacheIdentifier !== NULL
390 ? 'ClassLoader_' . $this->$this->cacheIdentifier
391 : NULL;
392 }
393
394 /**
395 * Set cache identifier
396 *
397 * @param string $cacheIdentifier Cache identifier for package namespaces cache
398 * @return ClassLoader
399 */
400 public function setCacheIdentifier($cacheIdentifier) {
401 $this->cacheIdentifier = $cacheIdentifier;
402 return $this;
403 }
404
405 /**
406 * Sets the available packages
407 *
408 * @param array $packages An array of \TYPO3\Flow\Package\Package objects
409 * @return ClassLoader
410 */
411 public function setPackages(array $packages) {
412 $this->packages = $packages;
413 if (!$this->loadPackageNamespacesFromCache()) {
414 $this->buildPackageNamespacesAndClassesPaths();
415 } else {
416 $this->classAliasMap->setPackages($packages);
417 }
418 // Clear the runtime cache for runtime activated packages
419 $this->runtimeClassLoadingInformationCache = array();
420 return $this;
421 }
422
423 /**
424 * Add a package to class loader just during runtime, so classes can be loaded without the need for a new request
425 *
426 * @param \TYPO3\Flow\Package\PackageInterface $package
427 * @return ClassLoader
428 */
429 public function addActivePackage(\TYPO3\Flow\Package\PackageInterface $package) {
430 $packageKey = $package->getPackageKey();
431 if (!isset($this->packages[$packageKey])) {
432 $this->packages[$packageKey] = $package;
433 $this->buildPackageNamespaceAndClassesPath($package);
434 $this->sortPackageNamespaces();
435 $this->loadClassFilesFromAutoloadRegistryIntoRuntimeClassInformationCache(array($package));
436 }
437 return $this;
438 }
439
440 /**
441 * Builds the package namespaces and classes paths for the given packages
442 *
443 * @return void
444 */
445 protected function buildPackageNamespacesAndClassesPaths() {
446 foreach ($this->packages as $package) {
447 $this->buildPackageNamespaceAndClassesPath($package);
448 }
449 $this->sortPackageNamespaces();
450 $this->savePackageNamespacesAndClassesPathsToCache();
451 // The class alias map has to be rebuilt first, because ext_autoload files can contain
452 // old class names that need established class aliases.
453 $classNameToAliasMapping = $this->classAliasMap->setPackages($this->packages)->buildMappingAndInitializeEarlyInstanceMapping();
454 $this->loadClassFilesFromAutoloadRegistryIntoRuntimeClassInformationCache($this->packages);
455 $this->classAliasMap->buildMappingFiles($classNameToAliasMapping);
456 $this->transferRuntimeClassInformationCacheEntriesToClassesCache();
457 }
458
459 /**
460 * Builds the namespace and class paths for a single package
461 *
462 * @param \TYPO3\Flow\Package\PackageInterface $package
463 * @return void
464 */
465 protected function buildPackageNamespaceAndClassesPath(\TYPO3\Flow\Package\PackageInterface $package) {
466 if ($package instanceof \TYPO3\Flow\Package\PackageInterface) {
467 $this->buildPackageNamespace($package);
468 }
469 if ($package instanceof PackageInterface) {
470 $this->buildPackageClassPathsForLegacyExtension($package);
471 }
472 }
473
474 /**
475 * Load package namespaces from cache
476 *
477 * @return bool TRUE if package namespaces were loaded
478 */
479 protected function loadPackageNamespacesFromCache() {
480 $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
481 if ($cacheEntryIdentifier === NULL) {
482 return FALSE;
483 }
484 $packageData = $this->coreCache->requireOnce($cacheEntryIdentifier);
485 if ($packageData !== FALSE) {
486 list($packageNamespaces, $packageClassesPaths) = $packageData;
487 if (is_array($packageNamespaces) && is_array($packageClassesPaths)) {
488 $this->packageNamespaces = $packageNamespaces;
489 $this->packageClassesPaths = $packageClassesPaths;
490 return TRUE;
491 }
492 }
493 return FALSE;
494 }
495
496 /**
497 * Extracts the namespace from a package
498 *
499 * @param \TYPO3\Flow\Package\PackageInterface $package
500 */
501 protected function buildPackageNamespace(\TYPO3\Flow\Package\PackageInterface $package) {
502 $packageNamespace = $package->getNamespace();
503 // Ignore legacy extensions with unkown vendor name
504 if ($packageNamespace[0] !== '*') {
505 $this->packageNamespaces[$packageNamespace] = array(
506 'namespaceLength' => strlen($packageNamespace),
507 'classesPath' => $package->getClassesPath(),
508 'packagePath' => $package->getPackagePath(),
509 'substituteNamespaceInPath' => ($package instanceof PackageInterface)
510 );
511 }
512 }
513
514 /**
515 * Save autoload registry to cache
516 *
517 * @param array $packages
518 * @return void
519 */
520 protected function loadClassFilesFromAutoloadRegistryIntoRuntimeClassInformationCache(array $packages) {
521 $classFileAutoloadRegistry = array();
522 foreach ($packages as $package) {
523 if ($package instanceof PackageInterface) {
524 $classFilesFromAutoloadRegistry = $package->getClassFilesFromAutoloadRegistry();
525 if (is_array($classFilesFromAutoloadRegistry)) {
526 $classFileAutoloadRegistry = array_merge($classFileAutoloadRegistry, $classFilesFromAutoloadRegistry);
527 }
528 }
529 }
530 foreach ($classFileAutoloadRegistry as $className => $classFilePath) {
531 $lowercasedClassName = strtolower($className);
532 if (!isset($this->runtimeClassLoadingInformationCache[$lowercasedClassName]) && @file_exists($classFilePath)) {
533 $this->runtimeClassLoadingInformationCache[$lowercasedClassName] = array($classFilePath, $className);
534 }
535 }
536 }
537
538 /**
539 * Transfers all entries from the early class information cache to
540 * the classes cache in order to make them persistent
541 *
542 * @return void
543 */
544 protected function transferRuntimeClassInformationCacheEntriesToClassesCache() {
545 foreach ($this->runtimeClassLoadingInformationCache as $classLoadingInformation) {
546 $cacheEntryIdentifier = strtolower(str_replace('\\', '_', $classLoadingInformation[1]));
547 if (!$this->classesCache->has($cacheEntryIdentifier)) {
548 $this->classesCache->set($cacheEntryIdentifier, implode("\xff", $classLoadingInformation));
549 }
550 }
551 }
552
553 /**
554 * @param PackageInterface $package
555 * @return void
556 */
557 protected function buildPackageClassPathsForLegacyExtension(PackageInterface $package) {
558 $this->packageClassesPaths[$package->getPackageKey()] = $package->getClassesPath();
559 foreach (array_keys($package->getPackageReplacementKeys()) as $packageToReplace) {
560 $this->packageClassesPaths[$packageToReplace] = $package->getClassesPath();
561 }
562 }
563
564 /**
565 * Save package namespaces and classes paths to cache
566 *
567 * @return void
568 */
569 protected function savePackageNamespacesAndClassesPathsToCache() {
570 $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
571 if ($cacheEntryIdentifier !== NULL) {
572 $this->coreCache->set(
573 $this->getCacheEntryIdentifier(),
574 'return ' . var_export(array($this->packageNamespaces, $this->packageClassesPaths), TRUE) . ';'
575 );
576 }
577 }
578
579 /**
580 * Sorts longer package namespaces first, to find specific matches before generic ones
581 *
582 * @return void
583 */
584 protected function sortPackageNamespaces() {
585 $sortPackages = function ($a, $b) {
586 if (($lenA = strlen($a)) === ($lenB = strlen($b))) {
587 return strcmp($a, $b);
588 }
589 return $lenA > $lenB ? -1 : 1;
590 };
591 uksort($this->packageNamespaces, $sortPackages);
592 }
593
594 /**
595 * This method is necessary for the early loading of the cores autoload registry
596 *
597 * @param array $classFileAutoloadRegistry
598 * @return void
599 */
600 public function setRuntimeClassLoadingInformationFromAutoloadRegistry(array $classFileAutoloadRegistry) {
601 foreach ($classFileAutoloadRegistry as $className => $classFilePath) {
602 $lowercasedClassName = strtolower($className);
603 if (!isset($this->runtimeClassLoadingInformationCache[$lowercasedClassName])) {
604 $this->runtimeClassLoadingInformationCache[$lowercasedClassName] = array($classFilePath, $className);
605 }
606 }
607 }
608
609 /**
610 * Set alias for class name
611 *
612 * @param string $aliasClassName
613 * @param string $originalClassName
614 * @return bool
615 */
616 public function setAliasForClassName($aliasClassName, $originalClassName) {
617 return $this->classAliasMap->setAliasForClassName($aliasClassName, $originalClassName);
618 }
619
620 /**
621 * Get class name for alias
622 *
623 * @param string $alias
624 * @return mixed
625 */
626 static public function getClassNameForAlias($alias) {
627 return static::$staticAliasMap->getClassNameForAlias($alias);
628 }
629
630 /**
631 * Get alias for class name
632 *
633 * @param string $className
634 * @return mixed
635 * @deprecated since 6.2, will be removed 2 versions later - use getAliasesForClassName() instead
636 */
637 static public function getAliasForClassName($className) {
638 GeneralUtility::logDeprecatedFunction();
639 $aliases = static::$staticAliasMap->getAliasesForClassName($className);
640 return is_array($aliases) && isset($aliases[0]) ? $aliases[0] : NULL;
641 }
642
643 /**
644 * Get an aliases for a class name
645 *
646 * @param string $className
647 * @return mixed
648 */
649 static public function getAliasesForClassName($className) {
650 return static::$staticAliasMap->getAliasesForClassName($className);
651 }
652
653 }