From ad1f8e6c5afdf869754c9d7092c3bf8d33e8d1ed Mon Sep 17 00:00:00 2001 From: Bryan Davis Date: Sat, 3 Jan 2015 17:35:25 -0700 Subject: [PATCH] Add classmap-authoritative config setting Add a "classmap-authoritative" configuration setting that can be used to disable searching the various prefix and fallback directories for classes that have not been registered with the Composer\Autoload\ClassLoader class map. This setting can be used to optimize performance by avoiding a potentially large number of `file_exists` calls when Composer is being used in a program with additional autoloader facilities. Use of the setting implies "optimize-autoloader" to ensure that the most complete class map possible is generated. Closes #3603 --- doc/04-schema.md | 3 + res/composer-schema.json | 4 + src/Composer/Autoload/AutoloadGenerator.php | 12 ++- src/Composer/Autoload/ClassLoader.php | 25 ++++++ src/Composer/Command/ConfigCommand.php | 1 + src/Composer/Command/DumpAutoloadCommand.php | 2 +- src/Composer/Command/InstallCommand.php | 2 +- src/Composer/Command/UpdateCommand.php | 2 +- src/Composer/Config.php | 1 + .../Test/Autoload/AutoloadGeneratorTest.php | 76 +++++++++++++++---- 10 files changed, 110 insertions(+), 18 deletions(-) diff --git a/doc/04-schema.md b/doc/04-schema.md index 6dd06d635..f88ce11df 100644 --- a/doc/04-schema.md +++ b/doc/04-schema.md @@ -789,6 +789,9 @@ The following options are supported: the generated Composer autoloader. When null a random one will be generated. * **optimize-autoloader** Defaults to `false`. Always optimize when dumping the autoloader. +* **classmap-authoritative:** Defaults to `false`. If true, the composer + autoloader will not scan the filesystem for classes that are not found in + the class map. Implies 'optimize-autoloader'. * **github-domains:** Defaults to `["github.com"]`. A list of domains to use in github mode. This is used for GitHub Enterprise setups. * **github-expose-hostname:** Defaults to `true`. If set to false, the OAuth diff --git a/res/composer-schema.json b/res/composer-schema.json index 49ec0fa7a..4c40bdfb2 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -197,6 +197,10 @@ "type": "boolean", "description": "If false, the composer autoloader will not be prepended to existing autoloaders, defaults to true." }, + "classmap-authoritative": { + "type": "boolean", + "description": "If true, the composer autoloader will not scan the filesystem for classes that are not found in the class map, defaults to false." + }, "github-domains": { "type": "array", "description": "A list of domains to use in github mode. This is used for GitHub Enterprise setups, defaults to [\"github.com\"].", diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index 19f194fa1..c88c34b41 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -63,6 +63,7 @@ class AutoloadGenerator $vendorPath = $filesystem->normalizePath(realpath($config->get('vendor-dir'))); $useGlobalIncludePath = (bool) $config->get('use-include-path'); $prependAutoloader = $config->get('prepend-autoloader') === false ? 'false' : 'true'; + $classMapAuthoritative = $config->get('classmap-authoritative'); $targetDir = $vendorPath.'/'.$targetDir; $filesystem->ensureDirectoryExists($targetDir); @@ -226,7 +227,7 @@ EOF; file_put_contents($targetDir.'/autoload_files.php', $includeFilesFile); } file_put_contents($vendorPath.'/autoload.php', $this->getAutoloadFile($vendorPathToTargetDirCode, $suffix)); - file_put_contents($targetDir.'/autoload_real.php', $this->getAutoloadRealFile(true, (bool) $includePathFile, $targetDirLoader, (bool) $includeFilesFile, $vendorPathCode, $appBaseDirCode, $suffix, $useGlobalIncludePath, $prependAutoloader)); + file_put_contents($targetDir.'/autoload_real.php', $this->getAutoloadRealFile(true, (bool) $includePathFile, $targetDirLoader, (bool) $includeFilesFile, $vendorPathCode, $appBaseDirCode, $suffix, $useGlobalIncludePath, $prependAutoloader, $classMapAuthoritative)); // use stream_copy_to_stream instead of copy // to work around https://bugs.php.net/bug.php?id=64634 @@ -443,7 +444,7 @@ return ComposerAutoloaderInit$suffix::getLoader(); AUTOLOAD; } - protected function getAutoloadRealFile($useClassMap, $useIncludePath, $targetDirLoader, $useIncludeFiles, $vendorPathCode, $appBaseDirCode, $suffix, $useGlobalIncludePath, $prependAutoloader) + protected function getAutoloadRealFile($useClassMap, $useIncludePath, $targetDirLoader, $useIncludeFiles, $vendorPathCode, $appBaseDirCode, $suffix, $useGlobalIncludePath, $prependAutoloader, $classMapAuthoritative) { // TODO the class ComposerAutoloaderInit should be revert to a closure // when APC has been fixed: @@ -520,6 +521,13 @@ PSR4; CLASSMAP; } + if ($classMapAuthoritative) { + $file .= <<<'CLASSMAPAUTHORITATIVE' + $loader->setClassMapAuthoritative(true); + +CLASSMAPAUTHORITATIVE; + } + if ($useGlobalIncludePath) { $file .= <<<'INCLUDEPATH' $loader->setUseIncludePath(true); diff --git a/src/Composer/Autoload/ClassLoader.php b/src/Composer/Autoload/ClassLoader.php index 70d78bc3f..112815324 100644 --- a/src/Composer/Autoload/ClassLoader.php +++ b/src/Composer/Autoload/ClassLoader.php @@ -54,6 +54,8 @@ class ClassLoader private $useIncludePath = false; private $classMap = array(); + private $classMapAuthoratative = false; + public function getPrefixes() { if (!empty($this->prefixesPsr0)) { @@ -248,6 +250,27 @@ class ClassLoader return $this->useIncludePath; } + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoratative + */ + public function setClassMapAuthoritative($classMapAuthoratative) + { + $this->classMapAuthoratative = $classMapAuthoratative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function getClassMapAuthoratative() + { + return $this->classMapAuthoratative; + } + /** * Registers this instance as an autoloader. * @@ -298,6 +321,8 @@ class ClassLoader // class map lookup if (isset($this->classMap[$class])) { return $this->classMap[$class]; + } elseif ($this->classMapAuthoratative) { + return false; } $file = $this->findFileWithExtension($class, '.php'); diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index 832571b3f..b50707f0b 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -323,6 +323,7 @@ EOT ), 'autoloader-suffix' => array('is_string', function ($val) { return $val === 'null' ? null : $val; }), 'optimize-autoloader' => array($booleanValidator, $booleanNormalizer), + 'classmap-authoritative' => array($booleanValidator, $booleanNormalizer), 'prepend-autoloader' => array($booleanValidator, $booleanNormalizer), 'github-expose-hostname' => array($booleanValidator, $booleanNormalizer), ); diff --git a/src/Composer/Command/DumpAutoloadCommand.php b/src/Composer/Command/DumpAutoloadCommand.php index b30fbd140..adcc7adfd 100644 --- a/src/Composer/Command/DumpAutoloadCommand.php +++ b/src/Composer/Command/DumpAutoloadCommand.php @@ -52,7 +52,7 @@ EOT $package = $composer->getPackage(); $config = $composer->getConfig(); - $optimize = $input->getOption('optimize') || $config->get('optimize-autoloader'); + $optimize = $input->getOption('optimize') || $config->get('optimize-autoloader') || $config->get('classmap-authoritative'); if ($optimize) { $output->writeln('Generating optimized autoload files'); diff --git a/src/Composer/Command/InstallCommand.php b/src/Composer/Command/InstallCommand.php index 98cd21b9f..9d359cad0 100644 --- a/src/Composer/Command/InstallCommand.php +++ b/src/Composer/Command/InstallCommand.php @@ -106,7 +106,7 @@ EOT $preferDist = $input->getOption('prefer-dist'); } - $optimize = $input->getOption('optimize-autoloader') || $config->get('optimize-autoloader'); + $optimize = $input->getOption('optimize-autoloader') || $config->get('optimize-autoloader') || $config->get('classmap-authoritative'); $install ->setDryRun($input->getOption('dry-run')) diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index 10fa26987..4e4f003c9 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -110,7 +110,7 @@ EOT $preferDist = $input->getOption('prefer-dist'); } - $optimize = $input->getOption('optimize-autoloader') || $config->get('optimize-autoloader'); + $optimize = $input->getOption('optimize-autoloader') || $config->get('optimize-autoloader') || $config->get('classmap-authoritative'); $install ->setDryRun($input->getOption('dry-run')) diff --git a/src/Composer/Config.php b/src/Composer/Config.php index 86e318765..92ce8246b 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -39,6 +39,7 @@ class Config 'discard-changes' => false, 'autoloader-suffix' => null, 'optimize-autoloader' => false, + 'classmap-authoritative' => false, 'prepend-autoloader' => true, 'github-domains' => array('github.com'), 'github-expose-hostname' => true, diff --git a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php index 7c4ddccd9..7ca90b244 100644 --- a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php +++ b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php @@ -67,6 +67,11 @@ class AutoloadGeneratorTest extends TestCase */ private $eventDispatcher; + /** + * @var array + */ + private $configValueMap; + protected function setUp() { $this->fs = new Filesystem; @@ -79,18 +84,23 @@ class AutoloadGeneratorTest extends TestCase $this->config = $this->getMock('Composer\Config'); - $this->config->expects($this->at(0)) - ->method('get') - ->with($this->equalTo('vendor-dir')) - ->will($this->returnCallback(function () use ($that) { + $this->configValueMap = array( + 'vendor-dir' => function () use ($that) { return $that->vendorDir; - })); + }, + ); - $this->config->expects($this->at(1)) + $this->config->expects($this->atLeastOnce()) ->method('get') - ->with($this->equalTo('vendor-dir')) - ->will($this->returnCallback(function () use ($that) { - return $that->vendorDir; + ->will($this->returnCallback(function ($arg) use ($that) { + $ret = null; + if (isset($that->configValueMap[$arg])) { + $ret = $that->configValueMap[$arg]; + if (is_callable($ret)) { + $ret = $ret(); + } + } + return $ret; })); $this->origDir = getcwd(); @@ -483,6 +493,49 @@ class AutoloadGeneratorTest extends TestCase include $this->vendorDir.'/composer/autoload_classmap.php' ); $this->assertAutoloadFiles('classmap5', $this->vendorDir.'/composer', 'classmap'); + $this->assertNotRegExp('/\$loader->setClassMapAuthoritative\(true\);/', file_get_contents($this->vendorDir.'/composer/autoload_real.php')); + } + + public function testClassMapAutoloadingAuthoritative() + { + $package = new Package('a', '1.0', '1.0'); + + $packages = array(); + $packages[] = $a = new Package('a/a', '1.0', '1.0'); + $packages[] = $b = new Package('b/b', '1.0', '1.0'); + $packages[] = $c = new Package('c/c', '1.0', '1.0'); + $a->setAutoload(array('classmap' => array(''))); + $b->setAutoload(array('classmap' => array('test.php'))); + $c->setAutoload(array('classmap' => array('./'))); + + $this->repository->expects($this->once()) + ->method('getCanonicalPackages') + ->will($this->returnValue($packages)); + + $this->configValueMap['classmap-authoritative'] = true; + + $this->fs->ensureDirectoryExists($this->vendorDir.'/composer'); + $this->fs->ensureDirectoryExists($this->vendorDir.'/a/a/src'); + $this->fs->ensureDirectoryExists($this->vendorDir.'/b/b'); + $this->fs->ensureDirectoryExists($this->vendorDir.'/c/c/foo'); + file_put_contents($this->vendorDir.'/a/a/src/a.php', 'vendorDir.'/b/b/test.php', 'vendorDir.'/c/c/foo/test.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_7'); + $this->assertTrue(file_exists($this->vendorDir.'/composer/autoload_classmap.php'), "ClassMap file needs to be generated."); + $this->assertEquals( + array( + 'ClassMapBar' => $this->vendorDir.'/b/b/test.php', + 'ClassMapBaz' => $this->vendorDir.'/c/c/foo/test.php', + 'ClassMapFoo' => $this->vendorDir.'/a/a/src/a.php', + ), + include $this->vendorDir.'/composer/autoload_classmap.php' + ); + $this->assertAutoloadFiles('classmap5', $this->vendorDir.'/composer', 'classmap'); + + $this->assertRegExp('/\$loader->setClassMapAuthoritative\(true\);/', file_get_contents($this->vendorDir.'/composer/autoload_real.php')); + // FIXME: how can we actually test the ClassLoader implementation? } public function testFilesAutoloadGeneration() @@ -829,10 +882,7 @@ EOF; ->method('getCanonicalPackages') ->will($this->returnValue(array())); - $this->config->expects($this->at(2)) - ->method('get') - ->with($this->equalTo('use-include-path')) - ->will($this->returnValue(true)); + $this->configValueMap['use-include-path'] = true; $this->fs->ensureDirectoryExists($this->vendorDir.'/a');