5341b1d7f62ad96d11db73a2d62529a65746db08
[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\Utility\GeneralUtility;
31 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
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\PhpFrontend
51 */
52 protected $classesCache;
53
54 /**
55 * @var string
56 */
57 protected $cacheIdentifier;
58
59 /**
60 * @var array<\TYPO3\Flow\Package\Package>
61 */
62 protected $packages = array();
63
64 /**
65 * @var array
66 */
67 protected $earlyClassFileAutoloadRegistry = array();
68
69 /**
70 * @var array A list of namespaces this class loader is definitely responsible for
71 */
72 protected $packageNamespaces = array(
73 'TYPO3\CMS\Core' => 14
74 );
75
76 /**
77 * @var array A list of packages and their replaces pointing to class paths
78 */
79 protected $packageClassesPaths = array();
80
81 public function __construct() {
82 $this->classesCache = new \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend('cache_classes', new \TYPO3\CMS\Core\Cache\Backend\EarlyClassLoaderBackend());
83 }
84
85 /**
86 * Get class alias map list injected
87 *
88 * @param ClassAliasMap
89 */
90 public function injectClassAliasMap(ClassAliasMap $classAliasMap) {
91 $this->classAliasMap = $classAliasMap;
92 static::$staticAliasMap = $classAliasMap;
93 }
94
95 /**
96 * Get classes cache injected
97 *
98 * @param \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend $classesCache
99 */
100 public function injectClassesCache(\TYPO3\CMS\Core\Cache\Frontend\PhpFrontend $classesCache) {
101 /** @var $earlyClassLoaderBackend \TYPO3\CMS\Core\Cache\Backend\EarlyClassLoaderBackend */
102 $earlyClassLoaderBackend = $this->classesCache->getBackend();
103 $this->classesCache = $classesCache;
104 $this->classAliasMap->injectClassesCache($classesCache);
105 foreach ($earlyClassLoaderBackend->getAll() as $cacheEntryIdentifier => $classFilePath) {
106 if (!$this->classesCache->has($cacheEntryIdentifier)) {
107 $this->addClassToCache($classFilePath, $cacheEntryIdentifier);
108 }
109 }
110 }
111
112 /**
113 * Loads php files containing classes or interfaces found in the classes directory of
114 * a package and specifically registered classes.
115 *
116 * @param string $className Name of the class/interface to load
117 * @param bool $require TRUE if file should be required
118 * @return boolean
119 */
120 public function loadClass($className, $require = TRUE) {
121 if ($className[0] === '\\') {
122 $className = substr($className, 1);
123 }
124
125 if (!$this->isValidClassname($className)) {
126 return FALSE;
127 }
128
129 $cacheEntryIdentifier = strtolower(str_replace('\\', '_', $className));
130 $cacheEntryCreated = FALSE;
131
132 // Loads any known class via caching framework
133 if ($require) {
134 if ($this->classesCache->requireOnce($cacheEntryIdentifier) !== FALSE) {
135 $cacheEntryCreated = TRUE;
136 }
137 }
138
139 if (!$cacheEntryCreated) {
140 $cacheEntryCreated = $this->createCacheEntryForClassFromCorePackage($className, $cacheEntryIdentifier);
141 }
142
143 if (!$cacheEntryCreated) {
144 $cacheEntryCreated = $this->createCacheEntryForClassFromEarlyAutoloadRegistry($className, $cacheEntryIdentifier);
145 }
146
147 if (!$cacheEntryCreated) {
148 $cacheEntryCreated = $this->createCacheEntryForClassFromRegisteredPackages($className, $cacheEntryIdentifier);
149 }
150
151 if (!$cacheEntryCreated) {
152 $cacheEntryCreated = $this->createCacheEntryForClassByNamingConvention($className, $cacheEntryIdentifier);
153 }
154
155 if ($cacheEntryCreated && $require) {
156 if ($this->classesCache->requireOnce($cacheEntryIdentifier) !== FALSE) {
157 $cacheEntryCreated = TRUE;
158 }
159 }
160
161 return $cacheEntryCreated;
162 }
163
164 /**
165 * Find out if a class name is valid
166 *
167 * @param string $className
168 * @return bool
169 */
170 protected function isValidClassname($className) {
171 return strpos($className, ' ') === FALSE;
172 }
173
174 /**
175 * Create cache entry for class from core package
176 *
177 * @param string $className
178 * @param string $cacheEntryIdentifier
179 * @return boolean TRUE if cache entry exists
180 */
181 protected function createCacheEntryForClassFromCorePackage($className, $cacheEntryIdentifier) {
182 if (substr($cacheEntryIdentifier, 0, 14) === 'typo3_cms_core') {
183 $classesFolder = substr($cacheEntryIdentifier, 15, 5) === 'tests' ? '' : 'Classes/';
184 $classFilePath = PATH_typo3 . 'sysext/core/' . $classesFolder . str_replace('\\', '/', substr($className, 15)) . '.php';
185 if (@file_exists($classFilePath)) {
186 $this->addClassToCache($classFilePath, $cacheEntryIdentifier);
187 return TRUE;
188 }
189 }
190 return FALSE;
191 }
192
193 /**
194 * Create early class name autoload registry cache
195 *
196 * @param string $className
197 * @param string $cacheEntryIdentifier
198 * @return boolean TRUE if cache file was created
199 */
200 protected function createCacheEntryForClassFromEarlyAutoloadRegistry($className, $cacheEntryIdentifier) {
201 if (isset($this->earlyClassFileAutoloadRegistry[$lowercasedClassName = strtolower($className)])) {
202 if (@file_exists($this->earlyClassFileAutoloadRegistry[$lowercasedClassName])) {
203 $this->addClassToCache($this->earlyClassFileAutoloadRegistry[$lowercasedClassName], $cacheEntryIdentifier);
204 return TRUE;
205 }
206 }
207 return FALSE;
208 }
209
210 /**
211 * Create cache entry from registered packages
212 *
213 * @param string $className
214 * @param string $cacheEntryIdentifier
215 * @return boolean TRUE File was created
216 */
217 protected function createCacheEntryForClassFromRegisteredPackages($className, $cacheEntryIdentifier) {;
218 foreach ($this->packageNamespaces as $packageNamespace => $packageData) {
219 if (substr(str_replace('_', '\\', $className), 0, $packageData['namespaceLength']) === $packageNamespace) {
220 if ($packageData['substituteNamespaceInPath']) {
221 // If it's a TYPO3 package, classes don't comply to PSR-0.
222 // The namespace part is substituted.
223 $classPathAndFilename = '/' . str_replace('\\', '/', ltrim(substr($className, $packageData['namespaceLength']), '\\')) . '.php';
224 } else {
225 // make the classname PSR-0 compliant by replacing underscores only in the classname not in the namespace
226 $classPathAndFilename = '';
227 $lastNamespacePosition = strrpos($className, '\\');
228 if ($lastNamespacePosition !== FALSE) {
229 $namespace = substr($className, 0, $lastNamespacePosition);
230 $className = substr($className, $lastNamespacePosition + 1);
231 $classPathAndFilename = str_replace('\\', '/', $namespace) . '/';
232 }
233 $classPathAndFilename .= str_replace('_', '/', $className) . '.php';
234 }
235 if (strtolower(substr($className, $packageData['namespaceLength'], 5)) === 'tests') {
236 $classPathAndFilename = $packageData['packagePath'] . $classPathAndFilename;
237 } else {
238 $classPathAndFilename = $packageData['classesPath'] . $classPathAndFilename;
239 }
240 if (@file_exists($classPathAndFilename)) {
241 $this->addClassToCache($classPathAndFilename, $cacheEntryIdentifier);
242 return TRUE;
243 }
244 }
245 }
246 return FALSE;
247 }
248
249 /**
250 * Try to load a given class name based on 'extbase' naming convention into the registry.
251 * If the file is found it writes an entry to $classNameToFileMapping and re-caches the
252 * array to the file system to save this lookup for next call.
253 *
254 * @param string $className Class name to find source file of
255 * @param string $classCacheEntryIdentifier
256 * @return boolean TRUE if was created
257 */
258 protected function createCacheEntryForClassByNamingConvention($className, $classCacheEntryIdentifier) {
259 $delimiter = '_';
260 // To handle namespaced class names, split the class name at the
261 // namespace delimiters.
262 if (strpos($className, '\\') !== FALSE) {
263 $delimiter = '\\';
264 }
265
266 $classNameParts = explode($delimiter, $className, 4);
267
268 // We only handle classes that follow the convention Vendor\Product\Classname or is longer
269 // so we won't deal with class names that only have one or two parts
270 if (count($classNameParts) <= 2) {
271 return FALSE;
272 }
273
274 if (isset($classNameParts[0]) && $classNameParts[0] === 'TYPO3' && (isset($classNameParts[1]) && $classNameParts[1] === 'CMS')) {
275 $extensionKey = GeneralUtility::camelCaseToLowerCaseUnderscored($classNameParts[2]);
276 $classNameWithoutVendorAndProduct = $classNameParts[3];
277 } else {
278 $extensionKey = GeneralUtility::camelCaseToLowerCaseUnderscored($classNameParts[1]);
279 $classNameWithoutVendorAndProduct = $classNameParts[2];
280
281 if (isset($classNameParts[3])) {
282 $classNameWithoutVendorAndProduct .= $delimiter . $classNameParts[3];
283 }
284 }
285
286 if ($extensionKey && isset($this->packageClassesPaths[$extensionKey])) {
287 if (substr(strtolower($classNameWithoutVendorAndProduct), 0, 5) === 'tests') {
288 $classesPath = $this->packages[$extensionKey]->getPackagePath();
289 } else {
290 $classesPath = $this->packageClassesPaths[$extensionKey];
291 }
292 $classFilePath = $classesPath . strtr($classNameWithoutVendorAndProduct, $delimiter, '/') . '.php';
293 if (@file_exists($classFilePath)) {
294 $this->addClassToCache($classFilePath, $classCacheEntryIdentifier);
295 return TRUE;
296 }
297 }
298
299 return FALSE;
300 }
301
302 /**
303 * Get cache identifier
304 *
305 * @return string identifier
306 */
307 protected function getCacheIdentifier() {
308 return $this->cacheIdentifier;
309 }
310
311 /**
312 * Get cache entry identifier
313 *
314 * @return string identifier
315 */
316 protected function getCacheEntryIdentifier() {
317 $cacheIdentifier = $this->getCacheIdentifier();
318 return $cacheIdentifier !== NULL ? 'ClassLoader_' . $this->getCacheIdentifier() : NULL;
319 }
320
321 /**
322 * Set cache identifier
323 *
324 * @param string $cacheIdentifier Cache identifier
325 * @return ClassLoader
326 */
327 public function setCacheIdentifier($cacheIdentifier) {
328 $this->cacheIdentifier = $cacheIdentifier;
329 $this->classAliasMap->setCacheIdentifier($cacheIdentifier);
330 return $this;
331 }
332
333 /**
334 * Sets the available packages
335 *
336 * @param array $packages An array of \TYPO3\Flow\Package\Package objects
337 * @return ClassLoader
338 */
339 public function setPackages(array $packages) {
340 $this->packages = $packages;
341 if (!$this->loadPackageNamespacesFromCache()) {
342 $this->buildPackageNamespaces();
343 $this->buildPackageClassesPathsForLegacyExtensions();
344 $this->savePackageNamespacesAndClassesPathsToCache();
345 // Rebuild the class alias map too because ext_autoload can contain aliases
346 $classNameToAliasMapping = $this->classAliasMap->setPackagesButDontBuildMappingFilesReturnClassNameToAliasMappingInstead($packages);
347 $this->buildAutoloadRegistryAndSaveToCache();
348 $this->classAliasMap->buildMappingFiles($classNameToAliasMapping);
349 } else {
350 $this->classAliasMap->setPackages($packages);
351 }
352 return $this;
353 }
354
355 /**
356 * Load package namespaces from cache
357 *
358 * @return boolean TRUE if package namespaces were loaded
359 */
360 protected function loadPackageNamespacesFromCache() {
361 $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
362 if ($cacheEntryIdentifier !== NULL && $this->classesCache->has($cacheEntryIdentifier)) {
363 list($packageNamespaces, $packageClassesPaths) = $this->classesCache->requireOnce($cacheEntryIdentifier);
364 if (is_array($packageNamespaces) && is_array($packageClassesPaths)) {
365 $this->packageNamespaces = $packageNamespaces;
366 $this->packageClassesPaths = $packageClassesPaths;
367 return TRUE;
368 }
369 }
370 return FALSE;
371 }
372
373 /**
374 * Build package namespaces
375 *
376 * @return void
377 */
378 protected function buildPackageNamespaces() {
379 /** @var $package \TYPO3\Flow\Package\Package */
380 foreach ($this->packages as $package) {
381 $packageNamespace = $package->getNamespace();
382 // Ignore legacy extensions with unkown vendor name
383 if ($packageNamespace[0] !== '*') {
384 $this->packageNamespaces[$packageNamespace] = array(
385 'namespaceLength' => strlen($packageNamespace),
386 'classesPath' => $package->getClassesPath(),
387 'packagePath' => $package->getPackagePath(),
388 'substituteNamespaceInPath' => ($package instanceof \TYPO3\CMS\Core\Package\Package)
389 );
390 }
391 }
392 // Sort longer package namespaces first, to find specific matches before generic ones
393 $sortPackages = function($a, $b) {
394 if (($lenA = strlen($a)) === ($lenB = strlen($b))) {
395 return strcmp($a, $b);
396 }
397 return ($lenA > $lenB) ? -1 : 1;
398 };
399 uksort($this->packageNamespaces, $sortPackages);
400 }
401
402 /**
403 * Build autoload registry
404 *
405 * @return void
406 */
407 protected function buildAutoloadRegistryAndSaveToCache() {
408 $classFileAutoloadRegistry = array();
409 foreach ($this->packages as $package) {
410 /** @var $package \TYPO3\CMS\Core\Package\Package */
411 if ($package instanceof \TYPO3\CMS\Core\Package\Package) {
412 $classFilesFromAutoloadRegistry = $package->getClassFilesFromAutoloadRegistry();
413 if (is_array($classFilesFromAutoloadRegistry)) {
414 $classFileAutoloadRegistry = array_merge($classFileAutoloadRegistry, $classFilesFromAutoloadRegistry);
415 }
416 }
417 }
418 foreach ($classFileAutoloadRegistry as $className => $classFilePath) {
419 if (@file_exists($classFilePath)) {
420 $this->addClassToCache($classFilePath, strtolower(str_replace('\\', '_', $className)));
421 }
422 }
423 }
424
425 /**
426 * Builds the classes paths for legacy extensions with unknown vendor name
427 *
428 * @return void
429 */
430 protected function buildPackageClassesPathsForLegacyExtensions() {
431 foreach ($this->packages as $package) {
432 if ($package instanceof \TYPO3\CMS\Core\Package\PackageInterface) {
433 $this->packageClassesPaths[$package->getPackageKey()] = $package->getClassesPath();
434 foreach ($package->getPackageReplacementKeys() as $packageToReplace => $versionConstraint) {
435 $this->packageClassesPaths[$packageToReplace] = $package->getClassesPath();
436 }
437 }
438 }
439 }
440
441 /**
442 * Save package namespaces and classes paths to cache
443 *
444 * @return void
445 */
446 protected function savePackageNamespacesAndClassesPathsToCache() {
447 $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
448 if ($cacheEntryIdentifier !== NULL) {
449 $this->classesCache->set(
450 $this->getCacheEntryIdentifier(),
451 'return ' . var_export(array($this->packageNamespaces, $this->packageClassesPaths), TRUE) . ';'
452 );
453 }
454 }
455
456 /**
457 * Adds a single class to class loader cache.
458 *
459 * @param string $classFilePathAndName Physical path of file containing $className
460 * @param string $classCacheEntryIdentifier
461 */
462 protected function addClassToCache($classFilePathAndName, $classCacheEntryIdentifier) {
463 /** @var $classesCacheBackend \TYPO3\CMS\Core\Cache\Backend\EarlyClassLoaderBackend|\TYPO3\CMS\Core\Cache\Backend\ClassLoaderBackend */
464 $classesCacheBackend = $this->classesCache->getBackend();
465 $classesCacheBackend->setLinkToPhpFile(
466 $classCacheEntryIdentifier,
467 $classFilePathAndName
468 );
469 }
470
471 /**
472 * This method is necessary for the early loading of the cores autoload registry
473 *
474 * @param array $classFileAutoloadRegistry
475 */
476 public function setEarlyClassFileAutoloadRegistry($classFileAutoloadRegistry) {
477 $this->earlyClassFileAutoloadRegistry = $classFileAutoloadRegistry;
478 }
479
480 /**
481 * Set alias for class name
482 *
483 * @param string $aliasClassName
484 * @param string $originalClassName
485 * @return boolean
486 */
487 public function setAliasForClassName($aliasClassName, $originalClassName) {
488 return $this->classAliasMap->setAliasForClassName($aliasClassName, $originalClassName);
489 }
490
491 /**
492 * Get class name for alias
493 *
494 * @param string $alias
495 * @return mixed
496 */
497 static public function getClassNameForAlias($alias) {
498 return static::$staticAliasMap->getClassNameForAlias($alias);
499 }
500
501 /**
502 * Get alias for class name
503 *
504 * @param string $className
505 * @deprecated since 6.2, use getAliasesForClassName instead. will be removed 2 versions later
506 * @return mixed
507 */
508 static public function getAliasForClassName($className) {
509 $aliases = static::$staticAliasMap->getAliasesForClassName($className);
510 return (is_array($aliases) && isset($aliases[0])) ? $aliases[0] : NULL;
511 }
512
513 /**
514 * Get an aliases for a class name
515 *
516 * @param string $className
517 * @return mixed
518 */
519 static public function getAliasesForClassName($className) {
520 return static::$staticAliasMap->getAliasesForClassName($className);
521 }
522
523 }