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