[FEATURE] Implement extbase naming scheme in core autoloader
authorXavier Perseguers <typo3@perseguers.ch>
Wed, 29 Jun 2011 15:06:04 +0000 (17:06 +0200)
committerHelmut Hummel <typo3@helmut-hummel.de>
Sat, 16 Jul 2011 15:55:06 +0000 (17:55 +0200)
The patch enables the core autoloader to find class files that are named
according to the exbase class and file location naming scheme.
This renders ext_autoload.php entries obsolete for class locations
that stick to the extbase scheme even if extbase is not used
for an extension. The autoloaders within fluid and extbase are not
needed anymore.
Additionally, the class-to-file information is cached in the core
autoloader cache in order to reduce the number of file system lookups
for consecutive calls.
The patch is covered by unit tests to verify the complex logic.

Change-Id: I1af68a2d95cfc590eecffe6d06ea4e4eb58e11ec
Resolves: #21144
Related: #28249
Related: #28250
Reviewed-on: http://review.typo3.org/2985
Reviewed-by: Christian Kuhn
Tested-by: Christian Kuhn
Reviewed-by: Stefan Neufeind
Tested-by: Stefan Neufeind
Reviewed-by: Philipp Gampe
Tested-by: Philipp Gampe
Reviewed-by: Helmut Hummel
Tested-by: Helmut Hummel
t3lib/class.t3lib_autoloader.php
tests/t3lib/class.t3lib_autoloaderTest.php

index da7174c..27a4a56 100644 (file)
 /**
  * This class contains TYPO3 autoloader for classes.
  * It handles:
- * - the core of TYPO3
- * - all extensions with an ext_autoload.php file
+ * - The core of TYPO3
+ * - All extensions with an ext_autoload.php file
+ * - All extensions that stick to the 'extbase' like naming convention
  *
  * @author Dmitry Dulepov <dmitry@typo3.org>
  * @author Martin Kutschker <masi@typo3.org>
  * @author Oliver Hader <oliver@typo3.org>
  * @author Sebastian Kurf├╝rst <sebastian@typo3.org>
+ * @author Christian Kuhn <lolli@schwarzbu.ch>
  */
 class t3lib_autoloader {
 
@@ -46,6 +48,13 @@ class t3lib_autoloader {
        protected static $classNameToFileMapping = array();
 
        /**
+        * Name of cache entry identifier in autoload cache
+        *
+        * @var string
+        */
+       protected static $autoloadCacheIdentifier = '';
+
+       /**
         * The autoloader is static, thus we do not allow instances of this class.
         */
        private function __construct() {
@@ -57,6 +66,7 @@ class t3lib_autoloader {
         * @return boolean TRUE in case of success
         */
        public static function registerAutoloader() {
+               self::$autoloadCacheIdentifier = TYPO3_MODE === 'FE' ? 't3lib_autoload_FE' : 't3lib_autoload_BE';
                self::loadCoreAndExtensionRegistry();
                return spl_autoload_register('t3lib_autoloader::autoload', TRUE, TRUE);
        }
@@ -104,23 +114,17 @@ class t3lib_autoloader {
         *
         * @return void
         */
-       public static function loadCoreAndExtensionRegistry() {
+       protected static function loadCoreAndExtensionRegistry() {
                $phpCodeCache = $GLOBALS['typo3CacheManager']->getCache('cache_phpcode');
-               $autoloadCacheIdentifier = TYPO3_MODE === 'FE' ? 't3lib_autoload_FE' : 't3lib_autoload_BE';
 
                        // Create autoloader cache file if it does not exist yet
-               if (!$phpCodeCache->has($autoloadCacheIdentifier)) {
+               if (!$phpCodeCache->has(self::$autoloadCacheIdentifier)) {
                        $classRegistry = self::createCoreAndExtensionRegistry();
-                       $cachedFileContent = 'return array(';
-                       foreach ($classRegistry as $className => $classLocation) {
-                               $cachedFileContent .= chr(10) . '\'' . $className . '\' => \'' . $classLocation . '\',';
-                       }
-                       $cachedFileContent .= chr(10) . ');';
-                       $phpCodeCache->set($autoloadCacheIdentifier, $cachedFileContent, array('t3lib_autoloader'));
+                       self::updateRegistryCacheEntry($classRegistry);
                }
 
                        // Require calculated cache file
-               $mappingArray = $phpCodeCache->requireOnce($autoloadCacheIdentifier);
+               $mappingArray = $phpCodeCache->requireOnce(self::$autoloadCacheIdentifier);
 
                        // This can only happen if the autoloader was already registered
                        // in the same call once, the requireOnce of the cache file then
@@ -138,16 +142,19 @@ class t3lib_autoloader {
         * Get the full path to a class by looking it up in the registry.
         * If not found, returns NULL.
         *
-        * @param string $className Class name
-        * @return string Full name of the file where $className is declared, or NULL if no entry found in registry.
+        * @param string $className Class name to find source file of
+        * @return mixed If String: Full name of the file where $className is declared, NULL if no entry is found
         */
        protected static function getClassPathByRegistryLookup($className) {
-               $className = strtolower($className);
-               if (array_key_exists($className, self::$classNameToFileMapping)) {
-                       return self::$classNameToFileMapping[$className];
-               } else {
-                       return NULL;
+               $classPath = NULL;
+               $classNameLower = strtolower($className);
+               if (!array_key_exists($classNameLower, self::$classNameToFileMapping)) {
+                       self::attemptToLoadRegistryWithNamingConventionForGivenClassName($className);
                }
+               if (array_key_exists($classNameLower, self::$classNameToFileMapping)) {
+                       $classPath = self::$classNameToFileMapping[$classNameLower];
+               }
+               return $classPath;
        }
 
        /**
@@ -168,5 +175,51 @@ class t3lib_autoloader {
                }
                return $classRegistry;
        }
+
+       /**
+        * Try to load a given class name based on 'extbase' naming convention into the registry.
+        * If the file is found it writes an entry to $classNameToFileMapping and re-caches the
+        * array to the file system to save this lookup for next call.
+        *
+        * @param string $className Class name to find source file of
+        * @return void
+        */
+       protected static function attemptToLoadRegistryWithNamingConventionForGivenClassName($className) {
+               $classNameParts = explode('_', $className, 3);
+               $extensionKey = t3lib_div::camelCaseToLowerCaseUnderscored($classNameParts[1]);
+               if ($extensionKey) {
+                       try {
+                                       // This will throw a BadFunctionCallException if the extension is not loaded
+                               $extensionPath = t3lib_extMgm::extPath($extensionKey);
+                               $classFilePathAndName = $extensionPath . 'Classes/' . strtr($classNameParts[2], '_', '/') . '.php';
+                               if (file_exists($classFilePathAndName)) {
+                                       self::$classNameToFileMapping[strtolower($className)] = $classFilePathAndName;
+                                       self::updateRegistryCacheEntry(self::$classNameToFileMapping);
+                               }
+                       } catch (BadFunctionCallException $exception) {
+                                       // Catch the exception and do nothing to give
+                                       // other registered autoloaders a chance to find the file
+                       }
+               }
+       }
+
+       /**
+        * Set or update autoloader cache entry
+        *
+        * @param array $registry Current registry entries
+        * @return void
+        */
+       protected static function updateRegistryCacheEntry(array $registry) {
+               $cachedFileContent = 'return array(';
+               foreach ($registry as $className => $classLocation) {
+                       $cachedFileContent .= LF . '\'' . $className . '\' => \'' . $classLocation . '\',';
+               }
+               $cachedFileContent .= LF . ');';
+               $GLOBALS['typo3CacheManager']->getCache('cache_phpcode')->set(
+                       self::$autoloadCacheIdentifier,
+                       $cachedFileContent,
+                       array('t3lib_autoloader')
+               );
+       }
 }
 ?>
\ No newline at end of file
index cc6b700..b5773fd 100644 (file)
@@ -46,14 +46,40 @@ class t3lib_autoloaderTest extends Tx_Phpunit_TestCase {
        protected $backupGlobalsBlacklist = array('TYPO3_DB');
 
        /**
+        * @var array Backup of typo3CacheManager
+        */
+       protected $typo3CacheManager = NULL;
+
+       /**
         * @var array Register of temporary extensions in typo3temp
         */
        protected $fakedExtensions = array();
 
        /**
+        * Fix a race condition that t3lib_div is not available
+        * during tearDown if fiddling with the autoloader where
+        * backupGlobals is not set up again yet
+        */
+       public function setUp() {
+               $this->typo3CacheManager = $GLOBALS['typo3CacheManager'];
+       }
+
+       /**
         * Clean up
+        * Warning: Since phpunit itself is php and we are fiddling with php
+        * autoloader code here, the tests are a bit fragile. This tearDown
+        * method ensures that all main classes are available again during
+        * tear down of a testcase.
+        * This construct will fail if the class under test is changed and
+        * not compatible anymore. Make sure to always run the whole test
+        * suite if fiddling with the autoloader unit tests to ensure that
+        * there is no fatal error thrown in other unit test classes triggered
+        * by errors in this one.
         */
        public function tearDown() {
+               $GLOBALS['typo3CacheManager'] = $this->typo3CacheManager;
+               t3lib_autoloader::unregisterAutoloader();
+               t3lib_autoloader::registerAutoloader();
                foreach ($this->fakedExtensions as $extension) {
                        t3lib_div::rmdir(PATH_site . 'typo3temp/' . $extension, TRUE);
                }
@@ -66,7 +92,7 @@ class t3lib_autoloaderTest extends Tx_Phpunit_TestCase {
         * @return string The extension key
         */
        protected function createFakeExtension() {
-               $extKey = uniqid('testing');
+               $extKey = strtolower(uniqid('testing'));
                $absExtPath = PATH_site . "typo3temp/$extKey/";
                $relPath = "typo3temp/$extKey/";
                t3lib_div::mkdir($absExtPath);
@@ -85,16 +111,31 @@ class t3lib_autoloaderTest extends Tx_Phpunit_TestCase {
        /**
         * @test
         */
-       public function autoloaderCanBeUnregisteredAndRegisteredAgain() {
+       public function UnregisterAndRegisterAgainDoesNotFatal() {
                t3lib_autoloader::unregisterAutoloader();
                t3lib_autoloader::registerAutoloader();
+                       // If this fatals the autoload re registering went wrong
                t3lib_div::makeInstance('t3lib_timetracknull');
        }
 
        /**
         * @test
         */
-       public function extensionAutoloadFileIsIncludedIfAvailable() {
+       public function registerSetsCacheEntryWithT3libAutoloaderTag() {
+               $mockCache = $this->getMock('t3lib_cache_frontend_AbstractFrontend', array('getIdentifier', 'set', 'get', 'getByTag', 'has', 'remove', 'flush', 'flushByTag', 'requireOnce'), array(), '', FALSE);
+                       // Expect the mock cache set method to be called
+                       // once with t3lib_autoloader as third parameter
+               $mockCache->expects($this->once())->method('set')->with(TRUE, TRUE, array('t3lib_autoloader'));
+               $GLOBALS['typo3CacheManager'] = $this->getMock('t3lib_cache_Manager', array('getCache'));
+               $GLOBALS['typo3CacheManager']->expects($this->any())->method('getCache')->will($this->returnValue($mockCache));
+               t3lib_autoloader::unregisterAutoloader();
+               t3lib_autoloader::registerAutoloader();
+       }
+
+       /**
+        * @test
+        */
+       public function autoloadFindsClassFileDefinedInExtAutoloadFile() {
                $extKey = $this->createFakeExtension();
                $extPath = PATH_site . "typo3temp/$extKey/";
                $autoloaderFile = $extPath . "ext_autoload.php";
@@ -119,5 +160,141 @@ class t3lib_autoloaderTest extends Tx_Phpunit_TestCase {
                $this->setExpectedException('RuntimeException', '', 1310203812);
                t3lib_autoloader::autoload($class);
        }
+
+       /**
+        * @test
+        */
+       public function autoloadFindsClassFileThatRespectsExtbaseNamingSchemeWithoutExtAutoloadFile() {
+               $extKey = $this->createFakeExtension();
+               $extPath = PATH_site . "typo3temp/$extKey/";
+
+                       // Create a class named Tx_Extension_Foo123_Bar456
+                       // to find file extension/Classes/Foo123/Bar456.php
+               $pathSegment = 'Foo' . uniqid();
+               $fileName = 'Bar' . uniqid();
+               $class = 'Tx_' . $extKey . '_' . $pathSegment . '_' . $fileName;
+               $file = $extPath . 'Classes/' . $pathSegment . '/' . $fileName . '.php';
+
+               t3lib_div::mkdir_deep($extPath . 'Classes/' . $pathSegment);
+               file_put_contents($file, "<?php\n\nthrow new RuntimeException('', 1310203813);\n\n?>");
+
+                       // Inject a dummy for the core_phpcode cache to cache
+                       // the calculated cache entry to a dummy cache
+               $mockCache = $this->getMock('t3lib_cache_frontend_AbstractFrontend', array('getIdentifier', 'set', 'get', 'getByTag', 'has', 'remove', 'flush', 'flushByTag', 'requireOnce'), array(), '', FALSE);
+               $GLOBALS['typo3CacheManager'] = $this->getMock('t3lib_cache_Manager', array('getCache'));
+               $GLOBALS['typo3CacheManager']->expects($this->any())->method('getCache')->will($this->returnValue($mockCache));
+
+                       // Expect the exception of the file to be thrown
+               $this->setExpectedException('RuntimeException', '', 1310203813);
+               t3lib_autoloader::autoload($class);
+       }
+
+       /**
+        * @test
+        */
+       public function autoloadWritesClassFileThatRespectsExtbaseNamingSchemeToCacheFile() {
+               $extKey = $this->createFakeExtension();
+               $extPath = PATH_site . "typo3temp/$extKey/";
+
+               $pathSegment = 'Foo' . uniqid();
+               $fileName = 'Bar' . uniqid();
+               $class = 'Tx_' . $extKey . '_' . $pathSegment . '_' . $fileName;
+               $file = $extPath . 'Classes/' . $pathSegment . '/' . $fileName . '.php';
+
+               t3lib_div::mkdir_deep($extPath . 'Classes/' . $pathSegment);
+               file_put_contents($file, "<?php\n\n\$foo = 'bar';\n\n?>");
+
+               $mockCache = $this->getMock('t3lib_cache_frontend_AbstractFrontend', array('getIdentifier', 'set', 'get', 'getByTag', 'has', 'remove', 'flush', 'flushByTag', 'requireOnce'), array(), '', FALSE);
+               $GLOBALS['typo3CacheManager'] = $this->getMock('t3lib_cache_Manager', array('getCache'));
+               $GLOBALS['typo3CacheManager']->expects($this->any())->method('getCache')->will($this->returnValue($mockCache));
+
+                       // Expect that an entry to the cache is written containing the newly found class
+               $mockCache->expects($this->once())->method('set')->with(TRUE, $this->stringContains(strtolower($class), TRUE));
+
+               t3lib_autoloader::autoload($class);
+       }
+
+       /**
+        * @test
+        */
+       public function autoloadWritesClassFileLocationOfClassRespectingExtbaseNamingSchemeToCacheFile() {
+               $extKey = $this->createFakeExtension();
+               $extPath = PATH_site . "typo3temp/$extKey/";
+
+               $pathSegment = 'Foo' . uniqid();
+               $fileName = 'Bar' . uniqid();
+               $class = 'Tx_' . $extKey . '_' . $pathSegment . '_' . $fileName;
+               $file = $extPath . 'Classes/' . $pathSegment . '/' . $fileName . '.php';
+
+               t3lib_div::mkdir_deep($extPath . 'Classes/' . $pathSegment);
+               file_put_contents($file, "<?php\n\n\$foo = 'bar';\n\n?>");
+
+               $mockCache = $this->getMock('t3lib_cache_frontend_AbstractFrontend', array('getIdentifier', 'set', 'get', 'getByTag', 'has', 'remove', 'flush', 'flushByTag', 'requireOnce'), array(), '', FALSE);
+               $GLOBALS['typo3CacheManager'] = $this->getMock('t3lib_cache_Manager', array('getCache'));
+               $GLOBALS['typo3CacheManager']->expects($this->any())->method('getCache')->will($this->returnValue($mockCache));
+
+                       // Expect that an entry to the cache is written containing the newly found class
+               $mockCache->expects($this->once())->method('set')->with(TRUE, $this->stringContains(strtolower($file), TRUE));
+
+               t3lib_autoloader::autoload($class);
+       }
+
+       /**
+        * @test
+        */
+       public function autoloadDoesNotSetCacheEntryForClassThatRespectsExtbaseNamingSchemeOnConsecutiveCallsForSameClass() {
+               $extKey = $this->createFakeExtension();
+               $extPath = PATH_site . "typo3temp/$extKey/";
+
+               $pathSegment = 'Foo' . uniqid();
+               $fileName = 'Bar' . uniqid();
+               $class = 'Tx_' . $extKey . '_' . $pathSegment . '_' . $fileName;
+               $file = $extPath . 'Classes/' . $pathSegment . '/' . $fileName . '.php';
+
+               t3lib_div::mkdir_deep($extPath . 'Classes/' . $pathSegment);
+               file_put_contents($file, "<?php\n\n\$foo = 'bar';\n\n?>");
+
+               $mockCache = $this->getMock('t3lib_cache_frontend_AbstractFrontend', array('getIdentifier', 'set', 'get', 'getByTag', 'has', 'remove', 'flush', 'flushByTag', 'requireOnce'), array(), '', FALSE);
+               $GLOBALS['typo3CacheManager'] = $this->getMock('t3lib_cache_Manager', array('getCache'));
+               $GLOBALS['typo3CacheManager']->expects($this->any())->method('getCache')->will($this->returnValue($mockCache));
+
+                       // Expect the set method is called exactly once, even if the class is called multiple times.
+                       // This means that the internal array of the autoloader class is successfully used
+               $mockCache->expects($this->once())->method('set');
+
+               t3lib_autoloader::autoload($class);
+               t3lib_autoloader::autoload($class);
+       }
+
+       /**
+        * @test
+        */
+       public function autoloadReadsClassFileLocationFromCacheFileForClassThatRespectsExtbaseNamingScheme() {
+               $extKey = $this->createFakeExtension();
+               $extPath = PATH_site . "typo3temp/$extKey/";
+
+               $pathSegment = 'Foo' . uniqid();
+               $fileName = 'Bar' . uniqid();
+               $class = 'Tx_' . $extKey . '_' . $pathSegment . '_' . $fileName;
+               $file = $extPath . 'Classes/' . $pathSegment . '/' . $fileName . '.php';
+
+               t3lib_div::mkdir_deep($extPath . 'Classes/' . $pathSegment);
+               file_put_contents($file, "<?php\n\n\$foo = 'bar';\n\n?>");
+
+               $mockCache = $this->getMock('t3lib_cache_frontend_AbstractFrontend', array('getIdentifier', 'set', 'get', 'getByTag', 'has', 'remove', 'flush', 'flushByTag', 'requireOnce'), array(), '', FALSE);
+               $GLOBALS['typo3CacheManager'] = $this->getMock('t3lib_cache_Manager', array('getCache'));
+               $GLOBALS['typo3CacheManager']->expects($this->any())->method('getCache')->will($this->returnValue($mockCache));
+
+                       // Expect the set method is called exactly once, even if the class is called multiple times.
+                       // This means that the internal array of the autoloader class is successfully used
+               $mockCache->expects($this->once())
+                       ->method('requireOnce')
+                       ->will($this->returnValue(array(strtolower($class) => $file)));
+
+               t3lib_autoloader::autoload($class);
+               t3lib_autoloader::unregisterAutoloader();
+               t3lib_autoloader::registerAutoloader();
+               t3lib_autoloader::autoload($class);
+       }
 }
 ?>
\ No newline at end of file