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