TYPO3 CMS  TYPO3_6-2
ClassLoader.php
Go to the documentation of this file.
1 <?php
2 namespace TYPO3\CMS\Core\Core;
3 
21 
26 class ClassLoader {
27 
28  const VALID_CLASSNAME_PATTERN = '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9\\\\_\x7f-\xff]*$/';
29 
33  protected $context;
34 
38  protected $classAliasMap;
39 
43  static protected $staticAliasMap;
44 
48  protected $classesCache;
49 
53  protected $coreCache;
54 
58  protected $cacheIdentifier;
59 
63  protected $packages = array();
64 
68  protected $isEarlyCache = TRUE;
69 
74 
80  protected $packageNamespaces = array();
81 
87  protected $packageClassesPaths = array();
88 
94  protected $isLoadingLocker = FALSE;
95 
99  protected $lockObject = NULL;
100 
106  protected $shutdownRegistered = FALSE;
107 
114  $this->context = $context;
115  $this->classesCache = new Cache\Frontend\StringFrontend('cache_classes', new Cache\Backend\TransientMemoryBackend($context));
116  }
117 
125  $this->classAliasMap = $classAliasMap;
126  static::$staticAliasMap = $classAliasMap;
127  }
128 
135  public function injectCoreCache(Cache\Frontend\PhpFrontend $coreCache) {
136  $this->coreCache = $coreCache;
137  $this->classAliasMap->injectCoreCache($coreCache);
138  }
139 
146  public function injectClassesCache(Cache\Frontend\StringFrontend $classesCache) {
147  $earlyClassesCache = $this->classesCache;
148  $this->classesCache = $classesCache;
149  $this->isEarlyCache = FALSE;
150  $this->classAliasMap->injectClassesCache($classesCache);
151  foreach ($earlyClassesCache->getByTag('early') as $originalClassLoadingInformation) {
152  $classLoadingInformation = explode("\xff", $originalClassLoadingInformation);
153  $cacheEntryIdentifier = strtolower(str_replace('\\', '_', $classLoadingInformation[1]));
154  if (!$this->classesCache->has($cacheEntryIdentifier)) {
155  $this->classesCache->set($cacheEntryIdentifier, $originalClassLoadingInformation);
156  }
157  }
158  }
159 
169  public function loadClass($className) {
170  if ($className[0] === '\\') {
171  $className = substr($className, 1);
172  }
173 
174  if (!$this->isValidClassName($className)) {
175  return FALSE;
176  }
177 
178  $cacheEntryIdentifier = strtolower(str_replace('\\', '_', $className));
179  $classLoadingInformation = $this->getClassLoadingInformationFromCache($cacheEntryIdentifier);
180  // Handle a cache miss
181  if ($classLoadingInformation === FALSE) {
182  $classLoadingInformation = $this->buildCachedClassLoadingInformation($cacheEntryIdentifier, $className);
183  }
184 
185  // Class loading information structure
186  // array(
187  // 0 => class file path
188  // 1 => original class name
189  // 2 and following => alias class names
190  // )
191  $loadingSuccessful = FALSE;
192  if (!empty($classLoadingInformation)) {
193  // The call to class_exists/interface_exists fixes a rare case when early instances need to be aliased
194  // but PHP fails to recognize the real path of the class. See #55904
195  $loadingSuccessful = class_exists($classLoadingInformation[1], FALSE)
196  || interface_exists($classLoadingInformation[1], FALSE)
197  || (bool)require_once $classLoadingInformation[0];
198  }
199  if ($loadingSuccessful && count($classLoadingInformation) > 2) {
200  $originalClassName = $classLoadingInformation[1];
201  foreach (array_slice($classLoadingInformation, 2) as $aliasClassName) {
202  $this->setAliasForClassName($aliasClassName, $originalClassName);
203  }
204  }
205 
206  return $loadingSuccessful;
207  }
208 
219  public function getClassLoadingInformationFromCache($cacheEntryIdentifier) {
220  $rawClassLoadingInformation = $this->classesCache->get($cacheEntryIdentifier);
221 
222  if ($rawClassLoadingInformation === '') {
223  return array();
224  }
225 
226  if ($rawClassLoadingInformation) {
227  return explode("\xff", $rawClassLoadingInformation);
228  }
229  return FALSE;
230  }
231 
243  protected function buildCachedClassLoadingInformation($cacheEntryIdentifier, $className) {
244  // We do not need locking if we are in earlyCache mode
245  $didLock = FALSE;
246  if (!$this->isEarlyCache) {
247  $didLock = $this->acquireLock();
248  }
249 
250  // Look again into the cache after we got the lock, data might have been generated meanwhile
251  $classLoadingInformation = $this->getClassLoadingInformationFromCache($cacheEntryIdentifier);
252  // Handle repeated cache miss
253  if ($classLoadingInformation === FALSE) {
254  // Generate class information
255  $classLoadingInformation = $this->buildClassLoadingInformation($className);
256 
257  if ($classLoadingInformation !== FALSE) {
258  // If we found class information, cache it
259  $this->classesCache->set(
260  $cacheEntryIdentifier,
261  implode("\xff", $classLoadingInformation),
262  $this->isEarlyCache ? array('early') : array()
263  );
264  } elseif (!$this->isEarlyCache) {
265  if ($this->context->isProduction()) {
266  // Cache that the class is unknown
267  $this->classesCache->set($cacheEntryIdentifier, '');
268  }
269  }
270  }
271 
272  $this->releaseLock($didLock);
273 
274  return $classLoadingInformation;
275  }
276 
284  public function buildClassLoadingInformation($className) {
285  $classLoadingInformation = $this->buildClassLoadingInformationForClassFromCorePackage($className);
286 
287  if ($classLoadingInformation === FALSE) {
288  $classLoadingInformation = $this->fetchClassLoadingInformationFromRuntimeCache($className);
289  }
290 
291  if ($classLoadingInformation === FALSE) {
292  $classLoadingInformation = $this->buildClassLoadingInformationForClassFromRegisteredPackages($className);
293  }
294 
295  if ($classLoadingInformation === FALSE) {
296  $classLoadingInformation = $this->buildClassLoadingInformationForClassByNamingConvention($className);
297  }
298 
299  if ($classLoadingInformation === FALSE) {
300  // As a last resort try to load the class file from the autoload registry.
301  // If this also fails the class will get cached as "unavailable" anyways
302  // (if TYPO3_CONTEXT is Production) and there will be no performance impact.
303  //
304  // On non-production systems this case will generate an error (if not any later
305  // spl_autoload handler makes the class available) and countermeasures will
306  // have to be taken.
307  //
308  // So there will only be a performance impact for non-production systems, where
309  // a class is requested which is made available by an spl_autoload handler
310  // being run after the TYPO3 autoloader.
311  //
312  // The reason for this whole block is reasoned by not being able to determine
313  // whether the classes cache has been built up properly or not.
315  $classLoadingInformation = $this->fetchClassLoadingInformationFromRuntimeCache($className);
316  }
317 
318  return $classLoadingInformation;
319  }
320 
327  protected function isValidClassName($className) {
328  return (bool)preg_match(self::VALID_CLASSNAME_PATTERN, $className);
329  }
330 
338  if (substr($className, 0, 14) === 'TYPO3\\CMS\\Core') {
339  $classesFolder = substr($className, 15, 5) === 'Tests' ? '' : 'Classes/';
340  $classFilePath = PATH_typo3 . 'sysext/core/' . $classesFolder . str_replace('\\', '/', substr($className, 15)) . '.php';
341  if (@file_exists($classFilePath)) {
342  return array($classFilePath, $className);
343  }
344  }
345  return FALSE;
346  }
347 
354  protected function fetchClassLoadingInformationFromRuntimeCache($className) {
355  $lowercasedClassName = strtolower($className);
356  if (!isset($this->runtimeClassLoadingInformationCache[$lowercasedClassName])) {
357  return FALSE;
358  }
359  $classInformation = $this->runtimeClassLoadingInformationCache[$lowercasedClassName];
360  return @file_exists($classInformation[0]) ? $classInformation : FALSE;
361  }
362 
370  foreach ($this->packageNamespaces as $packageNamespace => $packageData) {
371  if (substr(str_replace('_', '\\', $className), 0, $packageData['namespaceLength']) === $packageNamespace) {
372  if ($packageData['substituteNamespaceInPath']) {
373  // If it's a TYPO3 package, classes don't comply to PSR-0.
374  // The namespace part is substituted.
375  $classPathAndFilename = '/' . str_replace('\\', '/', ltrim(substr($className, $packageData['namespaceLength']), '\\')) . '.php';
376  } else {
377  // Make the classname PSR-0 compliant by replacing underscores only in the classname not in the namespace
378  $classPathAndFilename = '';
379  $lastNamespacePosition = strrpos($className, '\\');
380  if ($lastNamespacePosition !== FALSE) {
381  $namespace = substr($className, 0, $lastNamespacePosition);
382  $className = substr($className, $lastNamespacePosition + 1);
383  $classPathAndFilename = str_replace('\\', '/', $namespace) . '/';
384  }
385  $classPathAndFilename .= str_replace('_', '/', $className) . '.php';
386  }
387  if (strtolower(substr($className, $packageData['namespaceLength'], 5)) === 'tests') {
388  $classPathAndFilename = $packageData['packagePath'] . $classPathAndFilename;
389  } else {
390  $classPathAndFilename = $packageData['classesPath'] . $classPathAndFilename;
391  }
392  if (@file_exists($classPathAndFilename)) {
393  return array($classPathAndFilename, $className);
394  }
395  }
396  }
397  return FALSE;
398  }
399 
407  $delimiter = '_';
408  // To handle namespaced class names, split the class name at the
409  // namespace delimiters.
410  if (strpos($className, '\\') !== FALSE) {
411  $delimiter = '\\';
412  }
413 
414  $classNameParts = explode($delimiter, $className, 4);
415 
416  // We only handle classes that follow the convention Vendor\Product\Classname or is longer
417  // so we won't deal with class names that only have one or two parts
418  if (count($classNameParts) <= 2) {
419  return FALSE;
420  }
421 
422  if (
423  isset($classNameParts[0])
424  && isset($classNameParts[1])
425  && $classNameParts[0] === 'TYPO3'
426  && $classNameParts[1] === 'CMS'
427  ) {
428  $extensionKey = GeneralUtility::camelCaseToLowerCaseUnderscored($classNameParts[2]);
429  $classNameWithoutVendorAndProduct = $classNameParts[3];
430  } else {
431  $extensionKey = GeneralUtility::camelCaseToLowerCaseUnderscored($classNameParts[1]);
432  $classNameWithoutVendorAndProduct = $classNameParts[2];
433 
434  if (isset($classNameParts[3])) {
435  $classNameWithoutVendorAndProduct .= $delimiter . $classNameParts[3];
436  }
437  }
438 
439  if ($extensionKey && isset($this->packageClassesPaths[$extensionKey])) {
440  if (substr(strtolower($classNameWithoutVendorAndProduct), 0, 5) === 'tests') {
441  $classesPath = $this->packages[$extensionKey]->getPackagePath();
442  } else {
443  $classesPath = $this->packageClassesPaths[$extensionKey];
444  }
445  $classFilePath = $classesPath . strtr($classNameWithoutVendorAndProduct, $delimiter, '/') . '.php';
446  if (@file_exists($classFilePath)) {
447  return array($classFilePath, $className);
448  }
449  }
450 
451  return FALSE;
452  }
453 
459  protected function getCacheEntryIdentifier() {
460  return $this->cacheIdentifier !== NULL
461  ? 'ClassLoader_' . $this->cacheIdentifier
462  : NULL;
463  }
464 
472  $this->cacheIdentifier = $cacheIdentifier;
473  return $this;
474  }
475 
482  public function setPackages(array $packages) {
483  $this->packages = $packages;
484 
485  if (!$this->loadPackageNamespacesFromCache()) {
487  } else {
488  $this->classAliasMap->setPackages($packages);
489  }
490  // Clear the runtime cache for runtime activated packages
491  $this->runtimeClassLoadingInformationCache = array();
492  return $this;
493  }
494 
501  public function addActivePackage(\TYPO3\Flow\Package\PackageInterface $package) {
502  $packageKey = $package->getPackageKey();
503  if (!isset($this->packages[$packageKey])) {
504  $this->packages[$packageKey] = $package;
505  $this->buildPackageNamespaceAndClassesPath($package);
506  $this->sortPackageNamespaces();
508  }
509  return $this;
510  }
511 
519  $didLock = $this->acquireLock();
520 
521  // Take a look again, after lock is acquired
522  if (!$this->loadPackageNamespacesFromCache()) {
523  try {
524  foreach ($this->packages as $package) {
525  $this->buildPackageNamespaceAndClassesPath($package);
526  }
527  $this->sortPackageNamespaces();
529  // The class alias map has to be rebuilt first, because ext_autoload files can contain
530  // old class names that need established class aliases.
531  $classNameToAliasMapping = $this->classAliasMap->setPackages($this->packages)->buildMappingAndInitializeEarlyInstanceMapping();
533  $this->classAliasMap->buildMappingFiles($classNameToAliasMapping);
535  } catch (\Exception $e) {
536  // Catching all Exceptions, as we don't know where in the process the class cache building breaks we
537  // need to clear our cache and also release our lock before we throw the Exception again to the user.
538  $this->clearClassesCache();
539  $this->releaseLock($didLock);
540  throw $e;
541  }
542  }
543 
544  $this->releaseLock($didLock);
545  }
546 
553  protected function buildPackageNamespaceAndClassesPath(\TYPO3\Flow\Package\PackageInterface $package) {
554  if ($package instanceof \TYPO3\Flow\Package\PackageInterface) {
555  $this->buildPackageNamespace($package);
556  }
557  if ($package instanceof PackageInterface) {
559  }
560  }
561 
567  protected function loadPackageNamespacesFromCache() {
568  $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
569  if ($cacheEntryIdentifier === NULL) {
570  return FALSE;
571  }
572  $packageData = $this->coreCache->requireOnce($cacheEntryIdentifier);
573  if ($packageData !== FALSE) {
574  list($packageNamespaces, $packageClassesPaths) = $packageData;
575  if (is_array($packageNamespaces) && is_array($packageClassesPaths)) {
576  $this->packageNamespaces = $packageNamespaces;
577  $this->packageClassesPaths = $packageClassesPaths;
578  return TRUE;
579  }
580  }
581  return FALSE;
582  }
583 
590  protected function buildPackageNamespace(\TYPO3\Flow\Package\PackageInterface $package) {
591  $packageNamespace = $package->getNamespace();
592  // Ignore legacy extensions with unkown vendor name
593  if ($packageNamespace[0] !== '*') {
594  $this->packageNamespaces[$packageNamespace] = array(
595  'namespaceLength' => strlen($packageNamespace),
596  'classesPath' => $package->getClassesPath(),
597  'packagePath' => $package->getPackagePath(),
598  'substituteNamespaceInPath' => ($package instanceof PackageInterface)
599  );
600  }
601  }
602 
610  $classFileAutoloadRegistry = array();
611  foreach ($packages as $package) {
612  if ($package instanceof PackageInterface) {
613  $classFilesFromAutoloadRegistry = $package->getClassFilesFromAutoloadRegistry();
614  if (is_array($classFilesFromAutoloadRegistry)) {
615  $classFileAutoloadRegistry = array_merge($classFileAutoloadRegistry, $classFilesFromAutoloadRegistry);
616  }
617  }
618  }
619  foreach ($classFileAutoloadRegistry as $className => $classFilePath) {
620  $lowercasedClassName = strtolower($className);
621  if (!isset($this->runtimeClassLoadingInformationCache[$lowercasedClassName]) && @file_exists($classFilePath)) {
622  $this->runtimeClassLoadingInformationCache[$lowercasedClassName] = array($classFilePath, $className);
623  }
624  }
625  }
626 
634  foreach ($this->runtimeClassLoadingInformationCache as $classLoadingInformation) {
635  $cacheEntryIdentifier = strtolower(str_replace('\\', '_', $classLoadingInformation[1]));
636  if (!$this->classesCache->has($cacheEntryIdentifier)) {
637  $this->classesCache->set($cacheEntryIdentifier, implode("\xff", $classLoadingInformation));
638  }
639  }
640  }
641 
647  $this->packageClassesPaths[$package->getPackageKey()] = $package->getClassesPath();
648  foreach ($package->getPackageReplacementKeys() as $packageToReplace => $_) {
649  $this->packageClassesPaths[$packageToReplace] = $package->getClassesPath();
650  }
651  }
652 
659  $cacheEntryIdentifier = $this->getCacheEntryIdentifier();
660  if ($cacheEntryIdentifier !== NULL) {
661  $this->coreCache->set(
662  $this->getCacheEntryIdentifier(),
663  'return ' . var_export(array($this->packageNamespaces, $this->packageClassesPaths), TRUE) . ';'
664  );
665  }
666  }
667 
673  protected function sortPackageNamespaces() {
674  $sortPackages = function ($a, $b) {
675  if (($lenA = strlen($a)) === ($lenB = strlen($b))) {
676  return strcmp($a, $b);
677  }
678  return $lenA > $lenB ? -1 : 1;
679  };
680  uksort($this->packageNamespaces, $sortPackages);
681  }
682 
689  public function setRuntimeClassLoadingInformationFromAutoloadRegistry(array $classFileAutoloadRegistry) {
690  foreach ($classFileAutoloadRegistry as $className => $classFilePath) {
691  $lowercasedClassName = strtolower($className);
692  if (!isset($this->runtimeClassLoadingInformationCache[$lowercasedClassName])) {
693  $this->runtimeClassLoadingInformationCache[$lowercasedClassName] = array($classFilePath, $className);
694  }
695  }
696  }
697 
705  public function setAliasForClassName($aliasClassName, $originalClassName) {
706  return $this->classAliasMap->setAliasForClassName($aliasClassName, $originalClassName);
707  }
708 
715  static public function getClassNameForAlias($alias) {
716  return static::$staticAliasMap->getClassNameForAlias($alias);
717  }
718 
726  static public function getAliasForClassName($className) {
728  $aliases = static::$staticAliasMap->getAliasesForClassName($className);
729  return is_array($aliases) && isset($aliases[0]) ? $aliases[0] : NULL;
730  }
731 
738  static public function getAliasesForClassName($className) {
739  return static::$staticAliasMap->getAliasesForClassName($className);
740  }
741 
748  protected function acquireLock() {
749  if (!$this->isLoadingLocker) {
750  $lockObject = $this->getLocker();
751 
752  if ($lockObject === NULL) {
753  // During installation typo3temp does not yet exist, so the locker can not
754  // do its job. In this case it does not need to be released again.
755  return FALSE;
756  }
757 
758  // We didn't lock yet so do it
759  if (!$lockObject->getLockStatus()) {
760  if (!$lockObject->acquireExclusiveLock()) {
761  throw new \RuntimeException('Could not acquire lock for ClassLoader cache creation.', 1394480725);
762  }
763 
764  if (!$this->shutdownRegistered) {
765  $this->shutdownRegistered = TRUE;
766  register_shutdown_function(array($this, 'checkForCrashAndCleanup'));
767  }
768 
769  return TRUE;
770  }
771  }
772  return FALSE;
773  }
774 
790  public function checkForCrashAndCleanup() {
791  // As we are used as shutdownFunction we need to test if we get called while the lock is set.
792  // If this is the case, the cache creation has crashed.
793  $error = error_get_last();
794 
795  // $this->lockObject can be null in installer context without typo3temp, but then this method shouldn't
796  // be registered as shutdown-function due to caching being disabled in this case.
797  // See @getLocker for more information.
798  if ($error !== NULL && $this->lockObject !== NULL && $this->lockObject->getLockStatus()) {
799  $this->clearClassesCache();
800  $this->releaseLock(TRUE);
801  }
802  }
803 
810  protected function releaseLock($needRelease) {
811  if ($needRelease) {
812  $lockObject = $this->getLocker();
813  $lockObject->release();
814  }
815  }
816 
822  protected function clearClassesCache() {
823  $this->coreCache->flush();
824  $this->classesCache->flush();
825  }
826 
833  protected function getLocker() {
834  if (NULL === $this->lockObject) {
835  $this->isLoadingLocker = TRUE;
836 
837  try {
838  $this->lockObject = new Locker('ClassLoader-cache-classes', Locker::LOCKING_METHOD_SIMPLE);
839  } catch (\RuntimeException $e) {
840  // The RuntimeException in constructor happens if directory typo3temp/locks could not be created.
841  // This usually happens during installation step 1 where typo3temp itself does not exist yet. In
842  // this case we proceed without locking, otherwise a missing typo3temp directory indicates a
843  // hard problem of the instance and we throw up.
844  // @TODO: This solution currently conflicts with separation of concerns since the class loader
845  // handles installation specific stuff. Find a better way to do this.
846  if (defined('TYPO3_enterInstallScript') && TYPO3_enterInstallScript) {
847  // Installer is running => So work without Locking.
848  return NULL;
849  } else {
850  throw $e;
851  }
852  }
853  $this->lockObject->setEnableLogging(FALSE);
854  $this->isLoadingLocker = FALSE;
855  }
856 
857  return $this->lockObject;
858  }
859 }
buildClassLoadingInformationForClassFromCorePackage($className)
injectCoreCache(Cache\Frontend\PhpFrontend $coreCache)
buildPackageNamespace(\TYPO3\Flow\Package\PackageInterface $package)
addActivePackage(\TYPO3\Flow\Package\PackageInterface $package)
const TYPO3_enterInstallScript
Definition: Install.php:97
static getAliasesForClassName($className)
fetchClassLoadingInformationFromRuntimeCache($className)
buildClassLoadingInformationForClassFromRegisteredPackages($className)
loadClassFilesFromAutoloadRegistryIntoRuntimeClassInformationCache(array $packages)
buildClassLoadingInformationForClassByNamingConvention($className)
getClassLoadingInformationFromCache($cacheEntryIdentifier)
injectClassAliasMap(ClassAliasMap $classAliasMap)
setRuntimeClassLoadingInformationFromAutoloadRegistry(array $classFileAutoloadRegistry)
setAliasForClassName($aliasClassName, $originalClassName)
setCacheIdentifier($cacheIdentifier)
injectClassesCache(Cache\Frontend\StringFrontend $classesCache)
__construct(ApplicationContext $context)
buildPackageNamespaceAndClassesPath(\TYPO3\Flow\Package\PackageInterface $package)
buildCachedClassLoadingInformation($cacheEntryIdentifier, $className)
buildPackageClassPathsForLegacyExtension(PackageInterface $package)
static getAliasForClassName($className)