diff --git a/doc/04-schema.md b/doc/04-schema.md index 85f8aff2d..33f82769d 100644 --- a/doc/04-schema.md +++ b/doc/04-schema.md @@ -552,6 +552,22 @@ Example: } ``` +#### Exclude files from classmaps + +If you want to exclude some files or folders from the classmap you can use the 'exclude-from-classmap' property. +This might be useful to exclude test classes in your live environment, for example. + +The classmap generator will ignore all files in the paths configured here. + +Example: + +```json +{ + "autoload": { + "exclude-from-classmap": ["/Tests/", "/test/", "/tests/"] + } +} + ### autoload-dev ([root-only](04-schema.md#root-package)) This section allows to define autoload rules for development purposes. @@ -730,7 +746,7 @@ override packages from it. ### config ([root-only](04-schema.md#root-package)) -A set of configuration options. It is only used for projects. See +A set of configuration options. It is only used for projects. See [Config](06-config.md) for a description of each individual option. ### scripts ([root-only](04-schema.md#root-package)) @@ -791,7 +807,7 @@ Optional. ### non-feature-branches -A list of regex patterns of branch names that are non-numeric (e.g. "latest" or something), +A list of regex patterns of branch names that are non-numeric (e.g. "latest" or something), that will NOT be handled as feature branches. This is an array of strings. If you have non-numeric branch names, for example like "latest", "current", "latest-stable" diff --git a/res/composer-schema.json b/res/composer-schema.json index f74a9c9b8..80ce27718 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -257,6 +257,10 @@ "files": { "type": "array", "description": "This is an array of files that are always required on every request." + }, + "exclude-from-classmap": { + "type": "array", + "description": "This is an array of patterns to exclude from autoload classmap generation. (e.g. \"exclude-from-classmap\": [\"/test/\", \"/tests/\", \"/Tests/\"]" } } }, diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index c170d41f6..ec604bcd2 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -28,6 +28,8 @@ use Composer\Script\ScriptEvents; */ class AutoloadGenerator { + const EXCLUDE_PATTERN = '.*%s'; + /** * @var EventDispatcher */ @@ -193,6 +195,11 @@ EOF; EOF; } + $blacklist = null; + if (!empty($autoloads['exclude-from-classmap'])) { + $blacklist = '{(' . implode('|', $autoloads['exclude-from-classmap']) . ')}'; + } + // flatten array $classMap = array(); if ($scanPsr0Packages) { @@ -215,21 +222,21 @@ EOF; if (!is_dir($dir)) { continue; } - $whitelist = sprintf( - '{%s/%s.+$}', - preg_quote($dir), - ($psrType === 'psr-0' && strpos($namespace, '_') === false) ? preg_quote(strtr($namespace, '\\', '/')) : '' - ); +// $whitelist = sprintf( +// '{%s/%s.+$}', +// preg_quote($dir), +// ($psrType === 'psr-0' && strpos($namespace, '_') === false) ? preg_quote(strtr($namespace, '\\', '/')) : '' +// ); $namespaceFilter = $namespace === '' ? null : $namespace; - $classMap = $this->addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $whitelist, $namespaceFilter, $classMap); + $classMap = $this->addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $blacklist, $namespaceFilter, $classMap); } } } } foreach ($autoloads['classmap'] as $dir) { - $classMap = $this->addClassMapCode($filesystem, $basePath, $vendorPath, $dir, null, null, $classMap); + $classMap = $this->addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $blacklist, null, $classMap); } ksort($classMap); @@ -277,9 +284,9 @@ EOF; )); } - private function addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $whitelist = null, $namespaceFilter = null, array $classMap = array()) + private function addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $blacklist = null, $namespaceFilter = null, array $classMap = array()) { - foreach ($this->generateClassMap($dir, $whitelist, $namespaceFilter) as $class => $path) { + foreach ($this->generateClassMap($dir, $blacklist, $namespaceFilter) as $class => $path) { $pathCode = $this->getPathCode($filesystem, $basePath, $vendorPath, $path).",\n"; if (!isset($classMap[$class])) { $classMap[$class] = $pathCode; @@ -294,9 +301,9 @@ EOF; return $classMap; } - private function generateClassMap($dir, $whitelist = null, $namespaceFilter = null) + private function generateClassMap($dir, $blacklist = null, $namespaceFilter = null) { - return ClassMapGenerator::createMap($dir, $whitelist, $this->io, $namespaceFilter); + return ClassMapGenerator::createMap($dir, $blacklist, $this->io, $namespaceFilter); } public function buildPackageMap(InstallationManager $installationManager, PackageInterface $mainPackage, array $packages) @@ -359,11 +366,18 @@ EOF; $psr4 = $this->parseAutoloadsType($packageMap, 'psr-4', $mainPackage); $classmap = $this->parseAutoloadsType(array_reverse($sortedPackageMap), 'classmap', $mainPackage); $files = $this->parseAutoloadsType($sortedPackageMap, 'files', $mainPackage); + $exclude = $this->parseAutoloadsType($sortedPackageMap, 'exclude-from-classmap', $mainPackage); krsort($psr0); krsort($psr4); - return array('psr-0' => $psr0, 'psr-4' => $psr4, 'classmap' => $classmap, 'files' => $files); + return array( + 'psr-0' => $psr0, + 'psr-4' => $psr4, + 'classmap' => $classmap, + 'files' => $files, + 'exclude-from-classmap' => $exclude + ); } /** @@ -674,6 +688,22 @@ FOOTER; } } + if ($type === 'exclude-from-classmap') { + // first escape user input + $path = sprintf(self::EXCLUDE_PATTERN, preg_quote($path)); + + if ($package === $mainPackage && $package->getTargetDir() && !is_readable($installPath.'/'.$path)) { + // remove target-dir from classmap entries of the root package + $targetDir = str_replace('\\', '[\\\\/]', preg_quote(str_replace(array('/', '\\'), '', $package->getTargetDir()))); + $path = ltrim(preg_replace('{^'.$targetDir.'}', '', ltrim($path, '\\/')), '\\/'); + } elseif ($package !== $mainPackage && $package->getTargetDir() && !is_readable($installPath.'/'.$path)) { + // add target-dir to exclude entries that don't have it + $path = preg_quote($package->getTargetDir()) . '/' . $path; + } + $autoloads[] = empty($installPath) ? $path : preg_quote($installPath) . '/' . $path; + continue; + } + $relativePath = empty($installPath) ? (empty($path) ? '.' : $path) : $installPath.'/'.$path; if ($type === 'files' || $type === 'classmap') { diff --git a/src/Composer/Autoload/ClassMapGenerator.php b/src/Composer/Autoload/ClassMapGenerator.php index 7f7e7e1eb..b487ed6ed 100644 --- a/src/Composer/Autoload/ClassMapGenerator.php +++ b/src/Composer/Autoload/ClassMapGenerator.php @@ -50,14 +50,14 @@ class ClassMapGenerator * Iterate over all files in the given directory searching for classes * * @param \Iterator|string $path The path to search in or an iterator - * @param string $whitelist Regex that matches against the file path + * @param string $blacklist Regex that matches against the file path that exclude from the classmap. * @param IOInterface $io IO object * @param string $namespace Optional namespace prefix to filter by * * @throws \RuntimeException When the path is neither an existing file nor directory * @return array A class map array */ - public static function createMap($path, $whitelist = null, IOInterface $io = null, $namespace = null) + public static function createMap($path, $blacklist = null, IOInterface $io = null, $namespace = null) { if (is_string($path)) { if (is_file($path)) { @@ -81,7 +81,7 @@ class ClassMapGenerator continue; } - if ($whitelist && !preg_match($whitelist, strtr($filePath, '\\', '/'))) { + if ($blacklist && preg_match($blacklist, strtr($filePath, '\\', '/'))) { continue; } diff --git a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php index 8c7727058..efc3a6a93 100644 --- a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php +++ b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php @@ -1261,6 +1261,50 @@ EOF; $this->assertEquals($expectedPsr4, file_get_contents($this->vendorDir.'/composer/autoload_psr4.php')); } + public function testExcludeFromClassmap() + { + $package = new Package('a', '1.0', '1.0'); + $package->setAutoload(array( + 'psr-0' => array( + 'Main' => 'src/', + 'Lala' => array('src/', 'lib/'), + ), + 'psr-4' => array( + 'Acme\Fruit\\' => 'src-fruit/', + 'Acme\Cake\\' => array('src-cake/', 'lib-cake/'), + ), + 'classmap' => array('composersrc/'), + 'exclude-from-classmap' => array('/tests/', 'Exclude.php'), + )); + + $this->repository->expects($this->once()) + ->method('getCanonicalPackages') + ->will($this->returnValue(array())); + + $this->fs->ensureDirectoryExists($this->workingDir.'/composer'); + $this->fs->ensureDirectoryExists($this->workingDir.'/src/Lala'); + $this->fs->ensureDirectoryExists($this->workingDir.'/lib'); + file_put_contents($this->workingDir.'/src/Lala/ClassMapMain.php', 'fs->ensureDirectoryExists($this->workingDir.'/src-fruit'); + $this->fs->ensureDirectoryExists($this->workingDir.'/src-cake'); + $this->fs->ensureDirectoryExists($this->workingDir.'/lib-cake'); + file_put_contents($this->workingDir.'/src-cake/ClassMapBar.php', 'fs->ensureDirectoryExists($this->workingDir.'/composersrc'); + $this->fs->ensureDirectoryExists($this->workingDir.'/composersrc/tests'); + file_put_contents($this->workingDir.'/composersrc/foo.php', 'workingDir.'/composersrc/tests/bar.php', 'workingDir.'/composersrc/ClassToExclude.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_1'); + + // Assert that autoload_classmap.php was correctly generated. + $this->assertAutoloadFiles('classmap', $this->vendorDir.'/composer', 'classmap'); + } + private function assertAutoloadFiles($name, $dir, $type = 'namespaces') { $a = __DIR__.'/Fixtures/autoload_'.$name.'.php';