├── .editorconfig ├── LICENSE ├── README.md ├── composer.json ├── res └── php │ └── alias-loader-include.tmpl.php ├── src ├── ClassAliasLoader.php ├── ClassAliasMap.php ├── ClassAliasMapGenerator.php ├── Config.php ├── IncludeFile.php ├── IncludeFile │ ├── CaseSensitiveToken.php │ ├── PrependToken.php │ └── TokenInterface.php └── Plugin.php └── tests └── Unit ├── BaseTestCase.php ├── ClassAliasLoaderTest.php ├── ConfigTest.php └── IncludeFileTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [**.php] 13 | indent_style = space 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Helmut Hummel 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Class Alias Loader [![Build Status](https://github.com/TYPO3/class-alias-loader/actions/workflows/tests.yml/badge.svg)](https://github.com/TYPO3/class-alias-loader/actions/workflows/tests.yml) 2 | ================== 3 | 4 | ## Introduction 5 | The idea behind this composer package is, to provide backwards compatibility for libraries that want to rename classes 6 | but still want to stay compatible for a certain amount of time with consumer packages of these libraries. 7 | 8 | ## What it does? 9 | It provides an additional class loader which amends the composer class loader by rewriting the `vendor/autoload.php` 10 | file when composer dumps the autoload information. This is only done if any of the packages that are installed by composer 11 | provide a class alias map file, which is configured in the respective `composer.json`. 12 | 13 | ## How does it work? 14 | If a package provides a mapping file which holds the mapping from old to new class name, the class loader registers itself 15 | and transparently calls `class_alias()` for classes with an alias. If an old class name is requested, the original class 16 | is loaded and the alias is established, so that third party packages can use old class names transparently. 17 | 18 | ## Configuration in composer.json 19 | 20 | You can define multiple class alias map files in the extra section of the `composer.json` like this: 21 | 22 | ``` 23 | "extra": { 24 | "typo3/class-alias-loader": { 25 | "class-alias-maps": [ 26 | "Migrations/Code/ClassAliasMap.php" 27 | ] 28 | } 29 | }, 30 | ``` 31 | 32 | Currently these files must be PHP files which return an associative array, where the keys are the old class names and the values the new class names. 33 | Such a mapping file can look like this: 34 | 35 | ``` 36 | \TYPO3\CMS\About\Controller\AboutController::class, 39 | 'Tx_About_Domain_Model_Extension' => \TYPO3\CMS\About\Domain\Model\Extension::class, 40 | 'Tx_About_Domain_Repository_ExtensionRepository' => \TYPO3\CMS\About\Domain\Repository\ExtensionRepository::class, 41 | 'Tx_Aboutmodules_Controller_ModulesController' => \TYPO3\CMS\Aboutmodules\Controller\ModulesController::class, 42 | ); 43 | ``` 44 | 45 | The '::class' constant is not available before PHP 5.5. Under a PHP before 5.5 the mapping file can look like this: 46 | 47 | ``` 48 | 'TYPO3\\CMS\\About\\Controller\\AboutController', 51 | 'Tx_About_Domain_Model_Extension' => 'TYPO3\\CMS\\About\\Domain\\Model\\Extension', 52 | 'Tx_About_Domain_Repository_ExtensionRepository' => 'TYPO3\\CMS\\About\\Domain\\Repository\\ExtensionRepository', 53 | 'Tx_Aboutmodules_Controller_ModulesController' => 'TYPO3\\CMS\\Aboutmodules\\Controller\\ModulesController', 54 | ); 55 | ``` 56 | 57 | In your *root* `composer.json` file, you can decide whether to allow classes to be found that are requested with wrong casing. 58 | Since PHP is case insensitive for class names, but PSR class loading standards bound file names to class names, class names de facto 59 | become case sensitive. For legacy packages it may be useful however to allow class names to be loaded even if wrong casing is provided. 60 | For this to work properly, you need to use the composer [optimize class loading information feature](https://getcomposer.org/doc/03-cli.md#global-options). 61 | 62 | 63 | You can activate this feature like this: 64 | 65 | ``` 66 | "extra": { 67 | "typo3/class-alias-loader": { 68 | "autoload-case-sensitivity": false 69 | } 70 | }, 71 | ``` 72 | 73 | The default value of this option is `true`. 74 | 75 | If no alias mapping is found and case sensitivity is set to `true` then by default this package does nothing. It means no additional class loading information is dumped 76 | and the `vendor/autoload.php` is not changed. This enables library vendors to deliver compatibility packages which provide such aliases 77 | for backwards compatibility, but keep the library clean (and faster) for new users. 78 | 79 | In case you want your application to add alias maps during runtime, it may be useful however if the alias loader is always initialized. 80 | Therefore it is possible to set the following option in your *root* `composer.json`: 81 | 82 | ``` 83 | "extra": { 84 | "typo3/class-alias-loader": { 85 | "always-add-alias-loader": true 86 | } 87 | }, 88 | ``` 89 | 90 | 91 | ## Using the API 92 | 93 | The public API is pretty simple and consists of only one class with only three static methods, `TYPO3\ClassAliasLoader\ClassAliasMap::getClassNameForAlias` 94 | being the most important one. 95 | You can use this class method if you have places in your application that deals with class names in strings and want to provide backwards compatibility there. 96 | The API returns the original (new) class name if there is one, or the class name given if no alias is found. 97 | 98 | The remaining methods, deal with adding alias maps during runtime, which generally is not recommended to do. 99 | 100 | ## Feedback appreciated 101 | 102 | I'm happy for feedback, be it [feature requests](https://github.com/TYPO3/class-alias-loader/issues) or [bug reports](https://github.com/TYPO3/class-alias-loader/issues). 103 | 104 | ## Contribute 105 | 106 | If you feel like contributing, please do a regular [pull request](https://github.com/TYPO3/class-alias-loader/pulls). 107 | The package is pretty small. The only thing to respect is to follow [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) coding standard 108 | and to add some tests for functionality you add or change. 109 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typo3/class-alias-loader", 3 | "type": "composer-plugin", 4 | "license": "MIT", 5 | "description": "Amends the composer class loader to support class aliases to provide backwards compatibility for packages", 6 | "keywords": [ 7 | "composer", "autoloader", "classloader", "alias" 8 | ], 9 | "homepage": "http://github.com/TYPO3/class-alias-loader", 10 | "authors": [ 11 | { 12 | "name": "Helmut Hummel", 13 | "email": "info@helhum.io" 14 | } 15 | ], 16 | "autoload": { 17 | "psr-4": { "TYPO3\\ClassAliasLoader\\": "src/"} 18 | }, 19 | "autoload-dev": { 20 | "psr-4": { "TYPO3\\ClassAliasLoader\\Test\\": "tests/"} 21 | }, 22 | "require": { 23 | "php": ">=7.1", 24 | "composer-plugin-api": "^1.0 || ^2.0" 25 | }, 26 | "require-dev": { 27 | "composer/composer": "^1.1@dev || ^2.0@dev", 28 | "mikey179/vfsstream": "~1.4.0@dev", 29 | "phpunit/phpunit": ">4.8 <9" 30 | }, 31 | "replace": { 32 | "helhum/class-alias-loader": "*" 33 | }, 34 | "extra": { 35 | "class": "TYPO3\\ClassAliasLoader\\Plugin", 36 | "branch-alias": { 37 | "dev-main": "1.1.x-dev" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /res/php/alias-loader-include.tmpl.php: -------------------------------------------------------------------------------- 1 | setAliasMap($classAliasMap); 7 | $classAliasLoader->setCaseSensitiveClassLoading('{$sensitive-loading}'); 8 | $classAliasLoader->register('{$prepend}'); 9 | TYPO3\ClassAliasLoader\ClassAliasMap::setClassAliasLoader($classAliasLoader); 10 | -------------------------------------------------------------------------------- /src/ClassAliasLoader.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | use Composer\Autoload\ClassLoader as ComposerClassLoader; 14 | 15 | /** 16 | * The main class loader that amends the composer class loader. 17 | * It deals with the alias maps and the case insensitive class loading if configured. 18 | */ 19 | class ClassAliasLoader 20 | { 21 | /** 22 | * @var ComposerClassLoader 23 | */ 24 | protected $composerClassLoader; 25 | 26 | /** 27 | * @var array 28 | */ 29 | protected $aliasMap = array( 30 | 'aliasToClassNameMapping' => array(), 31 | 'classNameToAliasMapping' => array() 32 | ); 33 | 34 | /** 35 | * @deprecated 36 | * @var bool 37 | */ 38 | protected $caseSensitiveClassLoading = true; 39 | 40 | /** 41 | * @param ComposerClassLoader $composerClassLoader 42 | */ 43 | public function __construct(ComposerClassLoader $composerClassLoader) 44 | { 45 | $this->composerClassLoader = $composerClassLoader; 46 | } 47 | 48 | /** 49 | * Set the alias map 50 | * 51 | * @param array $aliasMap 52 | */ 53 | public function setAliasMap(array $aliasMap) 54 | { 55 | $this->aliasMap = $aliasMap; 56 | } 57 | 58 | /** 59 | * @deprecated 60 | * @param bool $caseSensitiveClassLoading 61 | */ 62 | public function setCaseSensitiveClassLoading($caseSensitiveClassLoading) 63 | { 64 | $this->caseSensitiveClassLoading = $caseSensitiveClassLoading; 65 | } 66 | 67 | /** 68 | * Adds an alias map and merges it with already available map 69 | * 70 | * @param array $aliasMap 71 | */ 72 | public function addAliasMap(array $aliasMap) 73 | { 74 | foreach ($aliasMap['aliasToClassNameMapping'] as $alias => $originalClassName) { 75 | $lowerCaseAlias = strtolower($alias); 76 | $this->aliasMap['aliasToClassNameMapping'][$lowerCaseAlias] = $originalClassName; 77 | $this->aliasMap['classNameToAliasMapping'][$originalClassName][$lowerCaseAlias] = $lowerCaseAlias; 78 | } 79 | } 80 | 81 | /** 82 | * Get final class name of alias 83 | * 84 | * @param string $aliasOrClassName 85 | * @return string 86 | */ 87 | public function getClassNameForAlias($aliasOrClassName) 88 | { 89 | $lookUpClassName = strtolower($aliasOrClassName); 90 | 91 | return isset($this->aliasMap['aliasToClassNameMapping'][$lookUpClassName]) ? $this->aliasMap['aliasToClassNameMapping'][$lookUpClassName] : $aliasOrClassName; 92 | } 93 | 94 | /** 95 | * Registers this instance as an autoloader. 96 | * 97 | * @param bool $prepend Whether to prepend the autoloader or not 98 | */ 99 | public function register($prepend = false) 100 | { 101 | $this->composerClassLoader->unregister(); 102 | spl_autoload_register(array($this, 'loadClassWithAlias'), true, $prepend); 103 | } 104 | 105 | /** 106 | * Unregisters this instance as an autoloader. 107 | */ 108 | public function unregister() 109 | { 110 | spl_autoload_unregister(array($this, 'loadClassWithAlias')); 111 | } 112 | 113 | /** 114 | * Main class loading method registered with spl_autoload_register() 115 | * 116 | * @param string $className 117 | * @return bool 118 | */ 119 | public function loadClassWithAlias($className) 120 | { 121 | $originalClassName = $this->getOriginalClassName($className); 122 | 123 | return $originalClassName 124 | ? $this->loadOriginalClassAndSetAliases($originalClassName) 125 | : $this->loadClass($className); 126 | } 127 | 128 | /** 129 | * Load class with the option to respect case insensitivity 130 | * @deprecated 131 | * 132 | * @param string $className 133 | * @return bool|null 134 | */ 135 | public function loadClass($className) 136 | { 137 | $classFound = $this->composerClassLoader->loadClass($className); 138 | if (!$classFound && !$this->caseSensitiveClassLoading) { 139 | $classFound = $this->composerClassLoader->loadClass(strtolower($className)); 140 | } 141 | return $classFound; 142 | } 143 | 144 | /** 145 | * Looks up the original class name from the alias map 146 | * 147 | * @param string $aliasOrClassName 148 | * @return string|NULL NULL if no alias mapping is found or the original class name as string 149 | */ 150 | protected function getOriginalClassName($aliasOrClassName) 151 | { 152 | // Is an original class which has an alias 153 | if (array_key_exists($aliasOrClassName, $this->aliasMap['classNameToAliasMapping'])) { 154 | return $this->aliasMap['classNameToAliasMapping'][$aliasOrClassName] === array() 155 | ? null 156 | : $aliasOrClassName 157 | ; 158 | } 159 | // Is an alias (we're graceful ignoring casing for alias definitions) 160 | $lowerCasedClassName = strtolower($aliasOrClassName); 161 | if (array_key_exists($lowerCasedClassName, $this->aliasMap['aliasToClassNameMapping'])) { 162 | return $this->aliasMap['aliasToClassNameMapping'][$lowerCasedClassName]; 163 | } 164 | // No alias registered for this class name, return and remember that info 165 | $this->aliasMap['classNameToAliasMapping'][$aliasOrClassName] = array(); 166 | return null; 167 | } 168 | 169 | /** 170 | * Load classes and set aliases. 171 | * The class_exists calls are safety guards to avoid fatals when 172 | * class files were included or aliases were set manually in userland code. 173 | * 174 | * @param string $originalClassName 175 | * @return bool|null 176 | */ 177 | protected function loadOriginalClassAndSetAliases($originalClassName) 178 | { 179 | if ($this->classOrInterfaceExists($originalClassName) || $this->loadClass($originalClassName)) { 180 | foreach ($this->aliasMap['classNameToAliasMapping'][$originalClassName] as $aliasClassName) { 181 | if (!$this->classOrInterfaceExists($aliasClassName)) { 182 | class_alias($originalClassName, $aliasClassName); 183 | } 184 | } 185 | 186 | return true; 187 | } 188 | 189 | return null; 190 | } 191 | 192 | /** 193 | * @param string $className 194 | * @return bool 195 | */ 196 | protected function classOrInterfaceExists($className) 197 | { 198 | $classOrInterfaceExists = class_exists($className, false) || interface_exists($className, false); 199 | if ($classOrInterfaceExists) { 200 | return true; 201 | } 202 | if (function_exists('trait_exists')) { 203 | return trait_exists($className, false); 204 | } 205 | 206 | return false; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/ClassAliasMap.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | /** 14 | * This class is the only public API of this package (besides the composer.json configuration) 15 | * Use the only method in cases described below. 16 | */ 17 | class ClassAliasMap 18 | { 19 | /** 20 | * @var ClassAliasLoader 21 | */ 22 | protected static $classAliasLoader; 23 | 24 | /** 25 | * You can use this method in your code if you compare class names as strings and want to provide compatibility for that as well. 26 | * The impact is pretty low and boils down to a method call. In case no aliases are present in the composer installation, 27 | * the class name given is returned as is, because the vendor/autoload.php will not be rewritten and thus the static member of this 28 | * class will not be set. 29 | * 30 | * @param string $classNameOrAlias 31 | * @return string 32 | */ 33 | public static function getClassNameForAlias($classNameOrAlias) 34 | { 35 | if (!static::$classAliasLoader) { 36 | return $classNameOrAlias; 37 | } 38 | return static::$classAliasLoader->getClassNameForAlias($classNameOrAlias); 39 | } 40 | 41 | /** 42 | * Whether or not alias maps are already registered 43 | * 44 | * @return bool 45 | */ 46 | public static function hasAliasMaps() 47 | { 48 | return is_object(static::$classAliasLoader); 49 | } 50 | 51 | /** 52 | * Adds an alias map if the alias loader is registered, throws an exception otherwise. 53 | * 54 | * @param array $aliasMap 55 | * @throws \RuntimeException 56 | */ 57 | public static function addAliasMap(array $aliasMap) 58 | { 59 | if (!static::$classAliasLoader) { 60 | throw new \RuntimeException('Cannot set an alias map as the alias loader is not registered!', 1439228111); 61 | } 62 | 63 | static::$classAliasLoader->addAliasMap($aliasMap); 64 | } 65 | 66 | /** 67 | * @param ClassAliasLoader $classAliasLoader 68 | */ 69 | public static function setClassAliasLoader(ClassAliasLoader $classAliasLoader) 70 | { 71 | if (static::$classAliasLoader) { 72 | throw new \RuntimeException('Cannot set the alias loader, as it is already registered!', 1439228112); 73 | } 74 | static::$classAliasLoader = $classAliasLoader; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/ClassAliasMapGenerator.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | use Composer\Composer; 14 | use Composer\IO\IOInterface; 15 | use Composer\IO\NullIO; 16 | use Composer\Package\PackageInterface; 17 | use Composer\Util\Filesystem; 18 | use TYPO3\ClassAliasLoader\Config; 19 | use TYPO3\ClassAliasLoader\IncludeFile\CaseSensitiveToken; 20 | use TYPO3\ClassAliasLoader\IncludeFile\PrependToken; 21 | 22 | /** 23 | * This class loops over all packages that are installed by composer and 24 | * looks for configured class alias maps (in composer.json). 25 | * If at least one is found, the vendor/autoload.php file is rewritten to amend the composer class loader. 26 | * Otherwise it does nothing. 27 | */ 28 | class ClassAliasMapGenerator 29 | { 30 | /** 31 | * @var Composer 32 | */ 33 | protected $composer; 34 | 35 | /** 36 | * @var IOInterface 37 | */ 38 | protected $io; 39 | 40 | /** 41 | * @var Config 42 | */ 43 | private $config; 44 | 45 | /** 46 | * @param Composer $composer 47 | * @param IOInterface $io 48 | */ 49 | public function __construct(Composer $composer, ?IOInterface $io = null, $config = null) 50 | { 51 | $this->composer = $composer; 52 | $this->io = $io ?: new NullIO(); 53 | if (\is_bool($config)) { 54 | // Happens during upgrade from older versions, so try to be graceful 55 | $config = new Config($this->composer->getPackage()); 56 | } 57 | $this->config = $config ?: new Config($this->composer->getPackage()); 58 | } 59 | 60 | /** 61 | * @deprecated 62 | * @throws \Exception 63 | */ 64 | public function generateAliasMap() 65 | { 66 | // Is called during upgrade from older plugin versions, so try to be graceful, but output verbose message 67 | $this->io->writeError(' ┌─────────────────────────────────────────────────────────────┐ '); 68 | $this->io->writeError(' │ Upgraded typo3/class-alias-loader from older plugin version.│ '); 69 | $this->io->writeError(' │ Please run "composer dumpautoload" to complete the upgrade. │ '); 70 | $this->io->writeError(' └─────────────────────────────────────────────────────────────┘ '); 71 | } 72 | 73 | /** 74 | * @throws \Exception 75 | * @return bool 76 | */ 77 | public function generateAliasMapFiles() 78 | { 79 | $config = $this->composer->getConfig(); 80 | 81 | $filesystem = new Filesystem(); 82 | $basePath = $filesystem->normalizePath(substr($config->get('vendor-dir'), 0, -strlen($config->get('vendor-dir', $config::RELATIVE_PATHS)))); 83 | $vendorPath = $config->get('vendor-dir'); 84 | $targetDir = $vendorPath . '/composer'; 85 | $filesystem->ensureDirectoryExists($targetDir); 86 | 87 | $mainPackage = $this->composer->getPackage(); 88 | $autoLoadGenerator = $this->composer->getAutoloadGenerator(); 89 | $localRepo = $this->composer->getRepositoryManager()->getLocalRepository(); 90 | $packageMap = $autoLoadGenerator->buildPackageMap($this->composer->getInstallationManager(), $mainPackage, $localRepo->getCanonicalPackages()); 91 | 92 | $aliasToClassNameMapping = array(); 93 | $classNameToAliasMapping = array(); 94 | $classAliasMappingFound = false; 95 | 96 | foreach ($packageMap as $item) { 97 | /** @var PackageInterface $package */ 98 | list($package, $installPath) = $item; 99 | $aliasLoaderConfig = new Config($package, $this->io); 100 | if ($aliasLoaderConfig->get('class-alias-maps') !== null) { 101 | if (!is_array($aliasLoaderConfig->get('class-alias-maps'))) { 102 | throw new \Exception('Configuration option "class-alias-maps" must be an array'); 103 | } 104 | foreach ($aliasLoaderConfig->get('class-alias-maps') as $mapFile) { 105 | $mapFilePath = ($installPath ?: $basePath) . '/' . $filesystem->normalizePath($mapFile); 106 | if (!is_file($mapFilePath)) { 107 | $this->io->writeError(sprintf('The class alias map file "%s" configured in package "%s" was not found!', $mapFile, $package->getName())); 108 | continue; 109 | } 110 | $packageAliasMap = require $mapFilePath; 111 | if (!is_array($packageAliasMap)) { 112 | throw new \Exception('Class alias map files must return an array', 1422625075); 113 | } 114 | if (!empty($packageAliasMap)) { 115 | $classAliasMappingFound = true; 116 | } 117 | foreach ($packageAliasMap as $aliasClassName => $className) { 118 | $lowerCasedAliasClassName = strtolower($aliasClassName); 119 | $aliasToClassNameMapping[$lowerCasedAliasClassName] = $className; 120 | $classNameToAliasMapping[$className][$lowerCasedAliasClassName] = $lowerCasedAliasClassName; 121 | } 122 | } 123 | } 124 | } 125 | 126 | $alwaysAddAliasLoader = $this->config->get('always-add-alias-loader'); 127 | $caseSensitiveClassLoading = $this->config->get('autoload-case-sensitivity'); 128 | 129 | if (!$alwaysAddAliasLoader && !$classAliasMappingFound && $caseSensitiveClassLoading) { 130 | // No mapping found in any package and no insensitive class loading active. We return early and skip rewriting 131 | // Unless user configured alias loader to be always added 132 | return false; 133 | } 134 | 135 | $includeFile = new IncludeFile( 136 | $this->io, 137 | $this->composer, 138 | array( 139 | new CaseSensitiveToken( 140 | $this->io, 141 | $this->config 142 | ), 143 | new PrependToken( 144 | $this->io, 145 | $this->composer->getConfig() 146 | ), 147 | ) 148 | ); 149 | $includeFile->register(); 150 | 151 | $this->io->write('Generating ' . ($classAliasMappingFound ? '' : 'empty ') . 'class alias map file'); 152 | $this->generateAliasMapFile($aliasToClassNameMapping, $classNameToAliasMapping, $targetDir); 153 | 154 | return true; 155 | } 156 | 157 | /** 158 | * @deprecated will be removed with 2.0 159 | * @param $optimizeAutoloadFiles 160 | * @return bool 161 | */ 162 | public function modifyComposerGeneratedFiles($optimizeAutoloadFiles = false) 163 | { 164 | $caseSensitiveClassLoading = $this->config->get('autoload-case-sensitivity'); 165 | $vendorPath = $this->composer->getConfig()->get('vendor-dir'); 166 | if (!$caseSensitiveClassLoading) { 167 | $this->io->writeError('Re-writing class map to support case insensitive class loading is deprecated'); 168 | if (!$optimizeAutoloadFiles) { 169 | $this->io->writeError('Case insensitive class loading only works reliably if you use the optimize class loading feature of composer'); 170 | } 171 | $this->rewriteClassMapWithLowerCaseClassNames($vendorPath . '/composer'); 172 | } 173 | 174 | return true; 175 | } 176 | 177 | /** 178 | * @param array $aliasToClassNameMapping 179 | * @param array $classNameToAliasMapping 180 | * @param string $targetDir 181 | */ 182 | protected function generateAliasMapFile(array $aliasToClassNameMapping, array $classNameToAliasMapping, $targetDir) 183 | { 184 | $exportArray = array( 185 | 'aliasToClassNameMapping' => $aliasToClassNameMapping, 186 | 'classNameToAliasMapping' => $classNameToAliasMapping 187 | ); 188 | 189 | $fileContent = ' /', function ($match) { 206 | return strtolower($match[0]); 207 | }, $classMapContents); 208 | file_put_contents($targetDir . '/autoload_classmap.php', $classMapContents); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/Config.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | use Composer\IO\IOInterface; 14 | use Composer\IO\NullIO; 15 | use Composer\Package\PackageInterface; 16 | 17 | /** 18 | * Class Config 19 | */ 20 | class Config 21 | { 22 | /** 23 | * Default values 24 | * 25 | * @var array 26 | */ 27 | protected $config = array( 28 | 'class-alias-maps' => null, 29 | 'always-add-alias-loader' => false, 30 | 'autoload-case-sensitivity' => true 31 | ); 32 | 33 | /** 34 | * @var IOInterface 35 | */ 36 | protected $io; 37 | 38 | /** 39 | * @param PackageInterface $package 40 | * @param IOInterface $io 41 | */ 42 | public function __construct(PackageInterface $package, ?IOInterface $io = null) 43 | { 44 | $this->io = $io ?: new NullIO(); 45 | $this->setAliasLoaderConfigFromPackage($package); 46 | } 47 | 48 | /** 49 | * @param string $configKey 50 | * @return mixed 51 | */ 52 | public function get($configKey) 53 | { 54 | if (empty($configKey)) { 55 | throw new \InvalidArgumentException('Configuration key must not be empty', 1444039407); 56 | } 57 | // Extract parts of the path 58 | $configKey = str_getcsv($configKey, '.', '"', '\\'); 59 | 60 | // Loop through each part and extract its value 61 | $value = $this->config; 62 | foreach ($configKey as $segment) { 63 | if (array_key_exists($segment, $value)) { 64 | // Replace current value with child 65 | $value = $value[$segment]; 66 | } else { 67 | return null; 68 | } 69 | } 70 | return $value; 71 | } 72 | 73 | /** 74 | * @param PackageInterface $package 75 | */ 76 | protected function setAliasLoaderConfigFromPackage(PackageInterface $package) 77 | { 78 | $extraConfig = $this->handleDeprecatedConfigurationInPackage($package); 79 | if (isset($extraConfig['typo3/class-alias-loader']['class-alias-maps'])) { 80 | $this->config['class-alias-maps'] = (array)$extraConfig['typo3/class-alias-loader']['class-alias-maps']; 81 | } 82 | if (isset($extraConfig['typo3/class-alias-loader']['always-add-alias-loader'])) { 83 | $this->config['always-add-alias-loader'] = (bool)$extraConfig['typo3/class-alias-loader']['always-add-alias-loader']; 84 | } 85 | if (isset($extraConfig['typo3/class-alias-loader']['autoload-case-sensitivity'])) { 86 | $this->config['autoload-case-sensitivity'] = (bool)$extraConfig['typo3/class-alias-loader']['autoload-case-sensitivity']; 87 | } 88 | } 89 | 90 | /** 91 | * Ensures backwards compatibility for packages which used helhum/class-alias-loader 92 | * 93 | * @param PackageInterface $package 94 | * @return array 95 | */ 96 | protected function handleDeprecatedConfigurationInPackage(PackageInterface $package) 97 | { 98 | $extraConfig = $package->getExtra(); 99 | $messages = array(); 100 | if (!isset($extraConfig['typo3/class-alias-loader'])) { 101 | if (isset($extraConfig['helhum/class-alias-loader'])) { 102 | $extraConfig['typo3/class-alias-loader'] = $extraConfig['helhum/class-alias-loader']; 103 | $messages[] = sprintf( 104 | 'The package "%s" uses "helhum/class-alias-loader" section to define class alias maps, which is deprecated. Please use "typo3/class-alias-loader" instead!', 105 | $package->getName() 106 | ); 107 | } else { 108 | $extraConfig['typo3/class-alias-loader'] = array(); 109 | if (isset($extraConfig['class-alias-maps'])) { 110 | $extraConfig['typo3/class-alias-loader']['class-alias-maps'] = $extraConfig['class-alias-maps']; 111 | $messages[] = sprintf( 112 | 'The package "%s" uses "class-alias-maps" section on top level, which is deprecated. Please move this config below the top level key "typo3/class-alias-loader" instead!', 113 | $package->getName() 114 | ); 115 | } 116 | if (isset($extraConfig['autoload-case-sensitivity'])) { 117 | $extraConfig['typo3/class-alias-loader']['autoload-case-sensitivity'] = $extraConfig['autoload-case-sensitivity']; 118 | $messages[] = sprintf( 119 | 'The package "%s" uses "autoload-case-sensitivity" section on top level, which is deprecated. Please move this config below the top level key "typo3/class-alias-loader" instead!', 120 | $package->getName() 121 | ); 122 | } 123 | } 124 | } 125 | if (!empty($messages)) { 126 | $this->io->writeError($messages); 127 | } 128 | return $extraConfig; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/IncludeFile.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | use Composer\Composer; 14 | use Composer\IO\IOInterface; 15 | use Composer\Util\Filesystem; 16 | use TYPO3\CMS\Composer\Plugin\Core\IncludeFile\TokenInterface; 17 | 18 | class IncludeFile 19 | { 20 | const INCLUDE_FILE = '/typo3/alias-loader-include.php'; 21 | const INCLUDE_FILE_TEMPLATE = '/res/php/alias-loader-include.tmpl.php'; 22 | 23 | /** 24 | * @var TokenInterface[] 25 | */ 26 | private $tokens; 27 | 28 | /** 29 | * @var Filesystem 30 | */ 31 | private $filesystem; 32 | 33 | /** 34 | * @var IOInterface 35 | */ 36 | private $io; 37 | 38 | /** 39 | * @var Composer 40 | */ 41 | private $composer; 42 | 43 | /** 44 | * IncludeFile constructor. 45 | * 46 | * @param IOInterface $io 47 | * @param Composer $composer 48 | * @param TokenInterface[] $tokens 49 | * @param Filesystem $filesystem 50 | */ 51 | public function __construct(IOInterface $io, Composer $composer, array $tokens, ?Filesystem $filesystem = null) 52 | { 53 | $this->io = $io; 54 | $this->composer = $composer; 55 | $this->tokens = $tokens; 56 | $this->filesystem = $filesystem ?: new Filesystem(); 57 | } 58 | 59 | public function register() 60 | { 61 | $this->io->writeError('Register typo3/class-alias-loader file in root package autoload definition', true, IOInterface::VERBOSE); 62 | 63 | // Generate and write the file 64 | $includeFile = $this->composer->getConfig()->get('vendor-dir') . self::INCLUDE_FILE; 65 | file_put_contents($includeFile, $this->getIncludeFileContent(dirname($includeFile))); 66 | 67 | // Register the file in the root package 68 | $rootPackage = $this->composer->getPackage(); 69 | $autoloadDefinition = $rootPackage->getAutoload(); 70 | $autoloadDefinition['files'][] = $includeFile; 71 | $rootPackage->setAutoload($autoloadDefinition); 72 | } 73 | 74 | /** 75 | * Constructs the include file content 76 | * 77 | * @param string $includeFilePath 78 | * @throws \RuntimeException 79 | * @throws \InvalidArgumentException 80 | * @return string 81 | */ 82 | protected function getIncludeFileContent($includeFilePath) 83 | { 84 | $includeFileTemplate = $this->filesystem->normalizePath(dirname(__DIR__) . self::INCLUDE_FILE_TEMPLATE); 85 | $includeFileContent = file_get_contents($includeFileTemplate); 86 | foreach ($this->tokens as $token) { 87 | $includeFileContent = self::replaceToken($token->getName(), $token->getContent($includeFilePath), $includeFileContent); 88 | } 89 | 90 | return $includeFileContent; 91 | } 92 | 93 | /** 94 | * Replaces a token in the subject (PHP code) 95 | * 96 | * @param string $name 97 | * @param string $content 98 | * @param string $subject 99 | * @return string 100 | */ 101 | private static function replaceToken($name, $content, $subject) 102 | { 103 | return str_replace('\'{$' . $name . '}\'', $content, $subject); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/IncludeFile/CaseSensitiveToken.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | use Composer\IO\IOInterface; 14 | use TYPO3\ClassAliasLoader\Config; 15 | 16 | /** 17 | * @deprecated 18 | */ 19 | class CaseSensitiveToken implements TokenInterface 20 | { 21 | /** 22 | * @var string 23 | */ 24 | private $name = 'sensitive-loading'; 25 | 26 | /** 27 | * @var IOInterface 28 | */ 29 | private $io; 30 | 31 | /** 32 | * @var Config 33 | */ 34 | private $config; 35 | 36 | /** 37 | * BaseDirToken constructor. 38 | * 39 | * @param IOInterface $io 40 | * @param Config $config 41 | */ 42 | public function __construct(IOInterface $io, Config $config) 43 | { 44 | $this->io = $io; 45 | $this->config = $config; 46 | } 47 | 48 | /** 49 | * @return string 50 | */ 51 | public function getName() 52 | { 53 | return $this->name; 54 | } 55 | 56 | /** 57 | * @param string $includeFilePath 58 | * @throws \InvalidArgumentException 59 | * @return string 60 | */ 61 | public function getContent($includeFilePath) 62 | { 63 | return $this->config->get('autoload-case-sensitivity') ? 'true' : 'false'; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/IncludeFile/PrependToken.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | use Composer\Config; 14 | use Composer\IO\IOInterface; 15 | 16 | class PrependToken implements TokenInterface 17 | { 18 | /** 19 | * @var string 20 | */ 21 | private $name = 'prepend'; 22 | 23 | /** 24 | * @var IOInterface 25 | */ 26 | private $io; 27 | 28 | /** 29 | * @var Config 30 | */ 31 | private $config; 32 | 33 | /** 34 | * BaseDirToken constructor. 35 | * 36 | * @param IOInterface $io 37 | * @param Config $config 38 | */ 39 | public function __construct(IOInterface $io, Config $config) 40 | { 41 | $this->io = $io; 42 | $this->config = $config; 43 | } 44 | 45 | /** 46 | * @return string 47 | */ 48 | public function getName() 49 | { 50 | return $this->name; 51 | } 52 | 53 | /** 54 | * @param string $includeFilePath 55 | * @throws \InvalidArgumentException 56 | * @return string 57 | */ 58 | public function getContent($includeFilePath) 59 | { 60 | return $this->config->get('prepend-autoloader') === false ? 'false' : 'true'; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/IncludeFile/TokenInterface.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | interface TokenInterface 14 | { 15 | /** 16 | * The name of the token that shall be replaced 17 | * 18 | * @return string 19 | */ 20 | public function getName(); 21 | 22 | /** 23 | * The content the token should be replaced with 24 | * 25 | * @param string $includeFilePath 26 | * @return string 27 | */ 28 | public function getContent($includeFilePath); 29 | } 30 | -------------------------------------------------------------------------------- /src/Plugin.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | use Composer\Composer; 14 | use Composer\EventDispatcher\EventSubscriberInterface; 15 | use Composer\IO\IOInterface; 16 | use Composer\Plugin\PluginInterface; 17 | use Composer\Script\Event; 18 | 19 | /** 20 | * Class Plugin 21 | */ 22 | class Plugin implements PluginInterface, EventSubscriberInterface 23 | { 24 | /** 25 | * @var Composer 26 | */ 27 | protected $composer; 28 | 29 | /** 30 | * @var IOInterface 31 | */ 32 | protected $io; 33 | 34 | /** 35 | * @var ClassAliasMapGenerator 36 | */ 37 | private $aliasMapGenerator; 38 | 39 | /** 40 | * Apply plugin modifications to composer 41 | * 42 | * @param Composer $composer 43 | * @param IOInterface $io 44 | */ 45 | public function activate(Composer $composer, IOInterface $io) 46 | { 47 | $this->composer = $composer; 48 | $this->io = $io; 49 | $this->aliasMapGenerator = new ClassAliasMapGenerator( 50 | $this->composer, 51 | $this->io 52 | ); 53 | } 54 | 55 | public function deactivate(Composer $composer, IOInterface $io) 56 | { 57 | // Nothing to do 58 | } 59 | 60 | public function uninstall(Composer $composer, IOInterface $io) 61 | { 62 | // Nothing to do 63 | } 64 | 65 | /** 66 | * Returns an array of event names this subscriber wants to listen to. 67 | * 68 | * The array keys are event names and the value can be: 69 | * 70 | * * The method name to call (priority defaults to 0) 71 | * * An array composed of the method name to call and the priority 72 | * * An array of arrays composed of the method names to call and respective 73 | * priorities, or 0 if unset 74 | * 75 | * For instance: 76 | * 77 | * * array('eventName' => 'methodName') 78 | * * array('eventName' => array('methodName', $priority)) 79 | * * array('eventName' => array(array('methodName1', $priority), array('methodName2')) 80 | * 81 | * @return array The event names to listen to 82 | */ 83 | public static function getSubscribedEvents() 84 | { 85 | return array( 86 | 'pre-autoload-dump' => array('onPreAutoloadDump'), 87 | 'post-autoload-dump' => array('onPostAutoloadDump'), 88 | ); 89 | } 90 | 91 | /** 92 | * @param Event $event 93 | * @throws \Exception 94 | * @return bool 95 | */ 96 | public function onPreAutoloadDump(Event $event) 97 | { 98 | return $this->aliasMapGenerator->generateAliasMapFiles(); 99 | } 100 | 101 | /** 102 | * @param Event $event 103 | * @return bool 104 | */ 105 | public function onPostAutoloadDump(Event $event) 106 | { 107 | $flags = $event->getFlags(); 108 | $config = $event->getComposer()->getConfig(); 109 | $optimizeAutoloadFiles = !empty($flags['optimize']) || $config->get('optimize-autoloader') || $config->get('classmap-authoritative'); 110 | 111 | return $this->aliasMapGenerator->modifyComposerGeneratedFiles($optimizeAutoloadFiles); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/Unit/BaseTestCase.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | use Composer\Autoload\ClassLoader as ComposerClassLoader; 14 | use PHPUnit\Framework\MockObject\MockObject; 15 | use TYPO3\ClassAliasLoader\ClassAliasLoader; 16 | 17 | /** 18 | * Test case for ClassAliasLoader 19 | */ 20 | class ClassAliasLoaderTest extends BaseTestCase 21 | { 22 | /** 23 | * @var ClassAliasLoader 24 | */ 25 | protected $subject; 26 | 27 | /** 28 | * @var ComposerClassLoader|MockObject|PHPUnit_Framework_MockObject_MockObject 29 | */ 30 | protected $composerClassLoaderMock; 31 | 32 | /** 33 | * @before 34 | */ 35 | public function setMeUp() 36 | { 37 | $this->composerClassLoaderMock = $this->getMockBuilder('Composer\\Autoload\\ClassLoader')->getMock(); 38 | $this->subject = new ClassAliasLoader($this->composerClassLoaderMock); 39 | } 40 | 41 | /** 42 | * @after 43 | */ 44 | public function tearMeDown() 45 | { 46 | $this->subject->unregister(); 47 | } 48 | 49 | /** 50 | * @test 51 | */ 52 | public function registeringTheAliasLoaderUnregistersComposerClassLoader() 53 | { 54 | $this->composerClassLoaderMock->expects($this->once())->method('unregister'); 55 | $this->subject->register(); 56 | } 57 | 58 | /** 59 | * @test 60 | */ 61 | public function composerLoadClassIsCalledOnlyOnceWhenCaseSensitiveClassLoadingIsOn() 62 | { 63 | $this->composerClassLoaderMock->expects($this->once())->method('loadClass'); 64 | $this->subject->loadClassWithAlias('TestClass'); 65 | } 66 | 67 | /** 68 | * @test 69 | */ 70 | public function composerLoadClassIsCalledOnlyOnceWhenCaseSensitiveClassLoadingIsOffButClassIsFound() 71 | { 72 | $this->composerClassLoaderMock->expects($this->once())->method('loadClass')->willReturn(true); 73 | $this->subject->setCaseSensitiveClassLoading(false); 74 | $this->subject->loadClassWithAlias('TestClass'); 75 | } 76 | 77 | /** 78 | * @test 79 | */ 80 | public function composerLoadClassIsCalledTwiceWhenCaseSensitiveClassLoadingIsOffAndClassIsNotFound() 81 | { 82 | $this->composerClassLoaderMock->expects($this->exactly(2))->method('loadClass'); 83 | $this->subject->setCaseSensitiveClassLoading(false); 84 | $this->subject->loadClassWithAlias('TestClass'); 85 | } 86 | 87 | /** 88 | * @test 89 | */ 90 | public function loadsClassIfNoAliasIsFound() 91 | { 92 | $testClassName = 'TestClass' . md5(uniqid('bla', true)); 93 | $this->composerClassLoaderMock->expects($this->once())->method('loadClass')->willReturnCallback(function ($className) { 94 | eval('class ' . $className . ' {}'); 95 | return true; 96 | }); 97 | $this->subject->loadClassWithAlias($testClassName); 98 | $this->assertTrue(class_exists($testClassName, false)); 99 | } 100 | 101 | /** 102 | * @test 103 | */ 104 | public function callingLoadClassMultipleTimesInEdgeCasesWillStillWork() 105 | { 106 | $this->composerClassLoaderMock 107 | ->expects($this->exactly(2)) 108 | ->method('loadClass') 109 | ->willReturnOnConsecutiveCalls(false, true); 110 | $this->assertFalse($this->subject->loadClassWithAlias('TestClass')); 111 | $this->assertTrue($this->subject->loadClassWithAlias('TestClass')); 112 | } 113 | 114 | /** 115 | * @test 116 | */ 117 | public function loadClassWithOriginalClassNameSetsAliases() 118 | { 119 | $testClassName = 'TestClass' . md5(uniqid('bla', true)); 120 | $testAlias1 = 'TestAlias' . md5(uniqid('bla', true)); 121 | $testAlias2 = 'TestAlias' . md5(uniqid('bla', true)); 122 | 123 | $this->composerClassLoaderMock->expects($this->once())->method('loadClass')->willReturnCallback(function ($className) { 124 | eval('class ' . $className . ' {}'); 125 | return true; 126 | }); 127 | 128 | $this->subject->setAliasMap(array( 129 | 'aliasToClassNameMapping' => array( 130 | strtolower($testAlias1) => $testClassName, 131 | strtolower($testAlias2) => $testClassName, 132 | ), 133 | 'classNameToAliasMapping' => array( 134 | $testClassName => array(strtolower($testAlias1), strtolower($testAlias2)) 135 | ), 136 | )); 137 | 138 | $this->subject->loadClassWithAlias($testClassName); 139 | $this->assertTrue(class_exists($testAlias1, false)); 140 | $this->assertTrue(class_exists($testAlias2, false)); 141 | } 142 | 143 | /** 144 | * @test 145 | */ 146 | public function getClassNameForAliasReturnsClassNameForEachAlias() 147 | { 148 | $testClassName = 'TestClass' . md5(uniqid('bla', true)); 149 | $testAlias1 = 'TestAlias' . md5(uniqid('bla', true)); 150 | $testAlias2 = 'TestAlias' . md5(uniqid('bla', true)); 151 | 152 | $this->subject->setAliasMap(array( 153 | 'aliasToClassNameMapping' => array( 154 | strtolower($testAlias1) => $testClassName, 155 | strtolower($testAlias2) => $testClassName, 156 | ), 157 | 'classNameToAliasMapping' => array( 158 | $testClassName => array(strtolower($testAlias1), strtolower($testAlias2)) 159 | ), 160 | )); 161 | 162 | $this->assertEquals($testClassName, $this->subject->getClassNameForAlias($testAlias1)); 163 | $this->assertEquals($testClassName, $this->subject->getClassNameForAlias($testAlias2)); 164 | } 165 | 166 | /** 167 | * @test 168 | */ 169 | public function addAliasMapAddsAliasesCorrectlyToTheMap() 170 | { 171 | $testClassName = 'TestClass' . md5(uniqid('bla', true)); 172 | $testAlias1 = 'TestAlias' . md5(uniqid('bla', true)); 173 | $testAlias2 = 'TestAlias' . md5(uniqid('bla', true)); 174 | 175 | $this->subject->setAliasMap(array( 176 | 'aliasToClassNameMapping' => array( 177 | strtolower($testAlias1) => $testClassName, 178 | ), 179 | 'classNameToAliasMapping' => array( 180 | $testClassName => array(strtolower($testAlias1)) 181 | ), 182 | )); 183 | 184 | $this->subject->addAliasMap(array( 185 | 'aliasToClassNameMapping' => array( 186 | $testAlias2 => $testClassName, 187 | ), 188 | )); 189 | 190 | $this->assertEquals($testClassName, $this->subject->getClassNameForAlias($testAlias1)); 191 | $this->assertEquals($testClassName, $this->subject->getClassNameForAlias($testAlias2)); 192 | } 193 | 194 | /** 195 | * @test 196 | */ 197 | public function getClassNameForAliasReturnsClassNameForClassName() 198 | { 199 | $testClassName = 'TestClass' . md5(uniqid('bla', true)); 200 | $testAlias1 = 'TestAlias' . md5(uniqid('bla', true)); 201 | $testAlias2 = 'TestAlias' . md5(uniqid('bla', true)); 202 | 203 | $this->subject->setAliasMap(array( 204 | 'aliasToClassNameMapping' => array( 205 | strtolower($testAlias1) => $testClassName, 206 | strtolower($testAlias2) => $testClassName, 207 | ), 208 | 'classNameToAliasMapping' => array( 209 | $testClassName => array(strtolower($testAlias1), strtolower($testAlias2)) 210 | ), 211 | )); 212 | 213 | $this->assertEquals($testClassName, $this->subject->getClassNameForAlias($testClassName)); 214 | } 215 | 216 | /** 217 | * @test 218 | */ 219 | public function getClassNameForAliasReturnsClassNameForClassNameWithNoAliasMapSet() 220 | { 221 | $testClassName = 'TestClass' . md5(uniqid('bla', true)); 222 | $this->assertEquals($testClassName, $this->subject->getClassNameForAlias($testClassName)); 223 | } 224 | 225 | /** 226 | * @test 227 | */ 228 | public function loadClassWithAliasClassNameSetsAliasesAndLoadsOriginalClass() 229 | { 230 | $testClassName = 'TestClass' . md5(uniqid('bla', true)); 231 | $testAlias1 = 'TestAlias' . md5(uniqid('bla', true)); 232 | $testAlias2 = 'TestAlias' . md5(uniqid('bla', true)); 233 | 234 | $this->composerClassLoaderMock->expects($this->once())->method('loadClass')->willReturnCallback(function ($className) { 235 | eval('class ' . $className . ' {}'); 236 | return true; 237 | }); 238 | 239 | $this->subject->setAliasMap(array( 240 | 'aliasToClassNameMapping' => array( 241 | strtolower($testAlias1) => $testClassName, 242 | strtolower($testAlias2) => $testClassName, 243 | ), 244 | 'classNameToAliasMapping' => array( 245 | $testClassName => array(strtolower($testAlias1), strtolower($testAlias2)) 246 | ), 247 | )); 248 | 249 | $this->subject->loadClassWithAlias($testAlias1); 250 | $this->assertTrue(class_exists($testClassName, false), 'Class name is not loaded'); 251 | $this->assertTrue(class_exists($testAlias1, false), 'First alias is not loaded'); 252 | $this->assertTrue(class_exists($testAlias2, false), 'Second alias is not loaded'); 253 | } 254 | 255 | /** 256 | * @test 257 | */ 258 | public function aliasesInstancesHaveOriginalClassName() 259 | { 260 | $testClassName = 'TestClass' . md5(uniqid('bla', true)); 261 | $testAlias1 = 'TestAlias' . md5(uniqid('bla', true)); 262 | $testAlias2 = 'TestAlias' . md5(uniqid('bla', true)); 263 | 264 | $this->composerClassLoaderMock->expects($this->once())->method('loadClass')->willReturnCallback(function ($className) { 265 | eval('class ' . $className . ' {}'); 266 | return true; 267 | }); 268 | 269 | $this->subject->setAliasMap(array( 270 | 'aliasToClassNameMapping' => array( 271 | strtolower($testAlias1) => $testClassName, 272 | strtolower($testAlias2) => $testClassName, 273 | ), 274 | 'classNameToAliasMapping' => array( 275 | $testClassName => array(strtolower($testAlias1), strtolower($testAlias2)) 276 | ), 277 | )); 278 | 279 | $this->subject->loadClassWithAlias($testClassName); 280 | 281 | $testObject1 = new $testAlias1(); 282 | $testObject2 = new $testAlias2(); 283 | 284 | $this->assertSame($testClassName, get_class($testObject1)); 285 | $this->assertSame($testClassName, get_class($testObject2)); 286 | } 287 | 288 | /** 289 | * @test 290 | */ 291 | public function classAliasesAreGracefullySetIfClassAlreadyExists() 292 | { 293 | $testClassName = 'TestClass' . md5(uniqid('bla', true)); 294 | $testAlias1 = 'TestAlias' . md5(uniqid('bla', true)); 295 | $testAlias2 = 'TestAlias' . md5(uniqid('bla', true)); 296 | $this->composerClassLoaderMock->expects($this->never())->method('loadClass'); 297 | 298 | $this->subject->setAliasMap(array( 299 | 'aliasToClassNameMapping' => array( 300 | strtolower($testAlias1) => $testClassName, 301 | strtolower($testAlias2) => $testClassName, 302 | ), 303 | 'classNameToAliasMapping' => array( 304 | $testClassName => array(strtolower($testAlias1), strtolower($testAlias2)) 305 | ), 306 | )); 307 | 308 | eval('class ' . $testClassName . ' {}'); 309 | 310 | $this->subject->loadClassWithAlias($testClassName); 311 | 312 | $testObject1 = new $testAlias1(); 313 | $testObject2 = new $testAlias2(); 314 | 315 | $this->assertSame($testClassName, get_class($testObject1)); 316 | $this->assertSame($testClassName, get_class($testObject2)); 317 | } 318 | 319 | /** 320 | * @test 321 | */ 322 | public function interfaceAliasesAreGracefullySetIfInterfaceAlreadyExists() 323 | { 324 | $testClassName = 'TestClass' . md5(uniqid('bla', true)); 325 | $testAlias1 = 'TestAlias' . md5(uniqid('bla', true)); 326 | $testAlias2 = 'TestAlias' . md5(uniqid('bla', true)); 327 | $this->composerClassLoaderMock->expects($this->never())->method('loadClass'); 328 | 329 | $this->subject->setAliasMap(array( 330 | 'aliasToClassNameMapping' => array( 331 | strtolower($testAlias1) => $testClassName, 332 | strtolower($testAlias2) => $testClassName, 333 | ), 334 | 'classNameToAliasMapping' => array( 335 | $testClassName => array(strtolower($testAlias1), strtolower($testAlias2)) 336 | ), 337 | )); 338 | 339 | eval('interface ' . $testClassName . ' {}'); 340 | 341 | $this->subject->loadClassWithAlias($testClassName); 342 | 343 | $this->assertTrue(interface_exists($testAlias1, false), 'First alias is not loaded'); 344 | $this->assertTrue(interface_exists($testAlias2, false), 'Second alias is not loaded'); 345 | } 346 | 347 | /** 348 | * @test 349 | */ 350 | public function classAliasesAreNotReEstablishedIfTheyAlreadyExist() 351 | { 352 | $testClassName = 'TestClass' . md5(uniqid('bla', true)); 353 | $testAlias1 = 'TestAlias' . md5(uniqid('bla', true)); 354 | $testAlias2 = 'TestAlias' . md5(uniqid('bla', true)); 355 | $this->composerClassLoaderMock->expects($this->never())->method('loadClass'); 356 | 357 | $this->subject->setAliasMap(array( 358 | 'aliasToClassNameMapping' => array( 359 | strtolower($testAlias1) => $testClassName, 360 | strtolower($testAlias2) => $testClassName, 361 | ), 362 | 'classNameToAliasMapping' => array( 363 | $testClassName => array(strtolower($testAlias1), strtolower($testAlias2)) 364 | ), 365 | )); 366 | 367 | eval('class ' . $testClassName . ' {}'); 368 | class_alias($testClassName, $testAlias1); 369 | 370 | $this->subject->loadClassWithAlias($testClassName); 371 | 372 | $this->assertTrue(class_exists($testAlias2, false), 'Second alias is not loaded'); 373 | } 374 | 375 | /** 376 | * @test 377 | */ 378 | public function loadClassWithAliasReturnsNullIfComposerClassLoaderCannotFindClass() 379 | { 380 | $this->composerClassLoaderMock->expects($this->once())->method('loadClass'); 381 | $this->assertNull($this->subject->loadClassWithAlias('TestClass')); 382 | } 383 | 384 | /** 385 | * @test 386 | */ 387 | public function loadClassWithAliasReturnsNullIfComposerClassLoaderCannotFindClassEvenIfItExistsInMap() 388 | { 389 | $testClassName = 'TestClass' . md5(uniqid('bla', true)); 390 | $testAlias1 = 'TestAlias' . md5(uniqid('bla', true)); 391 | $testAlias2 = 'TestAlias' . md5(uniqid('bla', true)); 392 | 393 | $this->subject->setAliasMap(array( 394 | 'aliasToClassNameMapping' => array( 395 | strtolower($testAlias1) => $testClassName, 396 | strtolower($testAlias2) => $testClassName, 397 | ), 398 | 'classNameToAliasMapping' => array( 399 | $testClassName => array(strtolower($testAlias1), strtolower($testAlias2)) 400 | ), 401 | )); 402 | 403 | $this->composerClassLoaderMock->expects($this->once())->method('loadClass'); 404 | $this->assertNull($this->subject->loadClassWithAlias($testClassName)); 405 | } 406 | } 407 | -------------------------------------------------------------------------------- /tests/Unit/ConfigTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | use Composer\IO\IOInterface; 14 | use Composer\Package\PackageInterface; 15 | use TYPO3\ClassAliasLoader\Config; 16 | 17 | /** 18 | * Test case for Config 19 | */ 20 | class ConfigTest extends BaseTestCase 21 | { 22 | /** 23 | * @var Config 24 | */ 25 | protected $subject; 26 | 27 | /** 28 | * @var IOInterface|\PHPUnit_Framework_MockObject_MockObject 29 | */ 30 | protected $ioMock; 31 | 32 | /** 33 | * @var PackageInterface|\PHPUnit_Framework_MockObject_MockObject 34 | */ 35 | protected $packageMock; 36 | 37 | /** 38 | * @before 39 | */ 40 | public function setMeUp() 41 | { 42 | $this->ioMock = $this->getMockBuilder('Composer\\IO\\IOInterface')->getMock(); 43 | $this->packageMock = $this->getMockBuilder('Composer\\Package\\PackageInterface')->getMock(); 44 | 45 | $this->subject = new Config($this->packageMock, $this->ioMock); 46 | } 47 | 48 | /** 49 | * @test 50 | */ 51 | public function throwsExceptionForEmptyKey() 52 | { 53 | // Use this instead when old PHP versions are dropped and minimum phpunit version can be raised: 54 | /** 55 | $this->expectException('\\InvalidArgumentException'); 56 | $this->expectExceptionCode(1444039407); 57 | $this->subject->get(null); 58 | */ 59 | try { 60 | $result = false; 61 | $this->subject->get(null); 62 | } catch (\InvalidArgumentException $e) { 63 | if ($e->getCode() === 1444039407) { 64 | $result = true; 65 | } 66 | } 67 | $this->assertTrue($result, 'Expected exception with expected code not received'); 68 | } 69 | 70 | /** 71 | * @test 72 | */ 73 | public function defaultConfigIsAppliedWhenNothingIsConfiguredInPackage() 74 | { 75 | $this->assertFalse($this->subject->get('always-add-alias-loader')); 76 | $this->assertTrue($this->subject->get('autoload-case-sensitivity')); 77 | $this->assertNull($this->subject->get('class-alias-maps')); 78 | } 79 | 80 | /** 81 | * @test 82 | */ 83 | public function aliasMapConfigIsExtracted() 84 | { 85 | $this->packageMock->expects($this->any())->method('getExtra')->willReturn( 86 | array( 87 | 'typo3/class-alias-loader' => array( 88 | 'class-alias-maps' => array( 89 | 'path/map.php' 90 | ) 91 | ) 92 | ) 93 | ); 94 | 95 | $subject = new Config($this->packageMock, $this->ioMock); 96 | 97 | $this->assertSame(array('path/map.php'), $subject->get('class-alias-maps')); 98 | } 99 | 100 | /** 101 | * @test 102 | */ 103 | public function aliasMapConfigIsExtractedFromDeprecatedKey() 104 | { 105 | $this->packageMock->expects($this->any())->method('getExtra')->willReturn( 106 | array( 107 | 'helhum/class-alias-loader' => array( 108 | 'class-alias-maps' => array( 109 | 'path/map.php' 110 | ) 111 | ) 112 | ) 113 | ); 114 | $this->ioMock->expects($this->once())->method('writeError'); 115 | 116 | $subject = new Config($this->packageMock, $this->ioMock); 117 | 118 | $this->assertSame(array('path/map.php'), $subject->get('class-alias-maps')); 119 | } 120 | 121 | /** 122 | * @test 123 | */ 124 | public function otherConfigIsExtracted() 125 | { 126 | $this->packageMock->expects($this->any())->method('getExtra')->willReturn( 127 | array( 128 | 'typo3/class-alias-loader' => array( 129 | 'always-add-alias-loader' => true, 130 | 'autoload-case-sensitivity' => false, 131 | ) 132 | ) 133 | ); 134 | 135 | $subject = new Config($this->packageMock, $this->ioMock); 136 | 137 | $this->assertTrue($subject->get('always-add-alias-loader')); 138 | $this->assertFalse($subject->get('autoload-case-sensitivity')); 139 | } 140 | 141 | /** 142 | * @test 143 | */ 144 | public function otherConfigIsExtractedFromDeprecatedKey() 145 | { 146 | $this->packageMock->expects($this->any())->method('getExtra')->willReturn( 147 | array( 148 | 'helhum/class-alias-loader' => array( 149 | 'always-add-alias-loader' => true, 150 | 'autoload-case-sensitivity' => false, 151 | ) 152 | ) 153 | ); 154 | 155 | $subject = new Config($this->packageMock, $this->ioMock); 156 | 157 | $this->assertTrue($subject->get('always-add-alias-loader')); 158 | $this->assertFalse($subject->get('autoload-case-sensitivity')); 159 | } 160 | 161 | /** 162 | * @test 163 | */ 164 | public function caseSensitivityConfigIsExtractedFromVeryDeprecatedKey() 165 | { 166 | $this->packageMock->expects($this->any())->method('getExtra')->willReturn( 167 | array( 168 | 'autoload-case-sensitivity' => false, 169 | ) 170 | ); 171 | 172 | $subject = new Config($this->packageMock, $this->ioMock); 173 | 174 | $this->assertFalse($subject->get('autoload-case-sensitivity')); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /tests/Unit/IncludeFileTest.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | use Composer\Composer; 14 | use Composer\IO\IOInterface; 15 | use Composer\Package\PackageInterface; 16 | use PHPUnit\Framework\MockObject\MockObject; 17 | use TYPO3\ClassAliasLoader\Config; 18 | use TYPO3\ClassAliasLoader\IncludeFile; 19 | 20 | /** 21 | * Test case for IncludeFile 22 | */ 23 | final class IncludeFileTest extends BaseTestCase 24 | { 25 | /** 26 | * @var IncludeFile 27 | */ 28 | private $subject; 29 | 30 | /** 31 | * @var IOInterface|MockObject 32 | */ 33 | private $ioMock; 34 | 35 | /** 36 | * @var PackageInterface|MockObject 37 | */ 38 | private $packageMock; 39 | 40 | /** 41 | * @var Composer|MockObject 42 | */ 43 | private $composerMock; 44 | 45 | private $testDir = __DIR__; 46 | 47 | /** 48 | * @before 49 | */ 50 | public function setMeUp() 51 | { 52 | $this->ioMock = $this->getMockBuilder('Composer\\IO\\IOInterface')->getMock(); 53 | $this->packageMock = $this->getMockBuilder('Composer\\Package\\RootPackageInterface')->getMock(); 54 | $this->composerMock = $this->getMockBuilder('Composer\\Composer')->getMock(); 55 | $configMock = $this->getMockBuilder('Composer\\Config')->getMock(); 56 | $testDir = $this->testDir; 57 | $configMock->expects(self::any()) 58 | ->method('get') 59 | ->willReturnCallback(function ($key) use ($testDir) { 60 | switch ($key) { 61 | case 'prepend-autoloader': 62 | return true; 63 | case 'vendor-dir': 64 | return $testDir; 65 | default: 66 | throw new \RuntimeException('Not expected to be called with ' . $key); 67 | } 68 | }); 69 | mkdir($testDir . '/typo3'); 70 | $this->composerMock->expects(self::any()) 71 | ->method('getPackage') 72 | ->willReturn($this->packageMock); 73 | $this->composerMock->expects(self::any()) 74 | ->method('getConfig') 75 | ->willReturn($configMock); 76 | 77 | $this->subject = new IncludeFile( 78 | $this->ioMock, 79 | $this->composerMock, 80 | array( 81 | new IncludeFile\PrependToken($this->ioMock, $configMock), 82 | new IncludeFile\CaseSensitiveToken($this->ioMock, new Config($this->packageMock, $this->ioMock)) 83 | ) 84 | ); 85 | } 86 | 87 | /** 88 | * @after 89 | */ 90 | public function tearMeDown() 91 | { 92 | unlink($this->testDir . IncludeFile::INCLUDE_FILE); 93 | rmdir(dirname($this->testDir . IncludeFile::INCLUDE_FILE)); 94 | } 95 | 96 | 97 | public function testIncludeFileCanPeWritten() 98 | { 99 | $this->subject->register(); 100 | self::assertTrue(file_exists($this->testDir . IncludeFile::INCLUDE_FILE)); 101 | } 102 | } 103 | --------------------------------------------------------------------------------