diff --git a/doc/04-schema.md b/doc/04-schema.md index 5ce67e8b5..e3308630e 100644 --- a/doc/04-schema.md +++ b/doc/04-schema.md @@ -183,9 +183,10 @@ Optional. Autoload mapping for a PHP autoloader. -Currently only [PSR-0](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md) -autoloading is supported. Under the -`psr-0` key you define a mapping from namespaces to paths, relative to the +Currently [PSR-0](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md) +autoloading and ClassMap generation are supported. + +Under the `psr-0` key you define a mapping from namespaces to paths, relative to the package root. Example: @@ -198,6 +199,18 @@ Example: Optional, but it is highly recommended that you follow PSR-0 and use this. +You can use the classmap generation support to define autoloading for all libraries +that do not follow "PSR-0". To configure this you specify all directories +to search for classes. + +Example: + + { + "autoload: { + "classmap": ["src/", "lib/"] + } + } + ## target-dir Defines the installation target. @@ -389,4 +402,4 @@ See (Vendor Bins)[articles/vendor-bins.md] for more details. Optional. -← [Command-line interface](03-cli.md) | [Repositories](05-repositories.md) → \ No newline at end of file +← [Command-line interface](03-cli.md) | [Repositories](05-repositories.md) → diff --git a/res/composer-schema.json b/res/composer-schema.json index ed25a5b07..d35591a1c 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -127,6 +127,10 @@ "type": "object", "description": "This is a hash of namespaces (keys) and the directories they can be found into (values) by the autoloader.", "additionalProperties": true + }, + "classmap": { + "type": "array", + "description": "This is an array of directories that contain classes to be included in the class-map generation process." } } }, diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index e33c8ff99..59addd0b4 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -44,6 +44,11 @@ return call_user_func(function() { $loader->add($namespace, $path); } + $classMap = require __DIR__.'/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + $loader->register(); return $loader; @@ -107,9 +112,17 @@ EOF; } } } - $namespacesFile .= ");\n"; + if (isset($autoloads['classmap'])) { + // flatten array + $autoloads['classmap'] = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($autoloads['classmap'])); + } else { + $autoloads['classmap'] = array(); + } + + ClassMapGenerator::dump($autoloads['classmap'], $targetDir.'/autoload_classmap.php'); + file_put_contents($targetDir.'/autoload.php', $autoloadFile); file_put_contents($targetDir.'/autoload_namespaces.php', $namespacesFile); copy(__DIR__.'/ClassLoader.php', $targetDir.'/ClassLoader.php'); diff --git a/src/Composer/Autoload/ClassLoader.php b/src/Composer/Autoload/ClassLoader.php index d4f21bff4..94fc76ac1 100644 --- a/src/Composer/Autoload/ClassLoader.php +++ b/src/Composer/Autoload/ClassLoader.php @@ -45,6 +45,7 @@ class ClassLoader private $prefixes = array(); private $fallbackDirs = array(); private $useIncludePath = false; + private $classMap = array(); public function getPrefixes() { @@ -56,6 +57,23 @@ class ClassLoader return $this->fallbackDirs; } + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + /** * Registers a set of classes * @@ -142,6 +160,10 @@ class ClassLoader */ public function findFile($class) { + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ('\\' == $class[0]) { $class = substr($class, 1); } diff --git a/src/Composer/Autoload/ClassMapGenerator.php b/src/Composer/Autoload/ClassMapGenerator.php new file mode 100644 index 000000000..c9fa687d6 --- /dev/null +++ b/src/Composer/Autoload/ClassMapGenerator.php @@ -0,0 +1,137 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @license MIT + */ + +namespace Composer\Autoload; + +/** + * ClassMapGenerator + * + * @author Gyula Sallai + */ +class ClassMapGenerator +{ + /** + * Generate a class map file + * + * @param Traversable $dirs Directories or a single path to search in + * @param string $file The name of the class map file + */ + static public function dump($dirs, $file) + { + $maps = array(); + + foreach ($dirs as $dir) { + $maps = array_merge($maps, static::createMap($dir)); + } + + file_put_contents($file, sprintf('isFile()) { + continue; + } + + $path = $file->getRealPath(); + + if (pathinfo($path, PATHINFO_EXTENSION) !== 'php') { + continue; + } + + $classes = self::findClasses($path); + + foreach ($classes as $class) { + $map[$class] = $path; + } + + } + + return $map; + } + + /** + * Extract the classes in the given file + * + * @param string $path The file to check + * + * @return array The found classes + */ + static private function findClasses($path) + { + $contents = file_get_contents($path); + $tokens = token_get_all($contents); + + $classes = array(); + + $namespace = ''; + for ($i = 0, $max = count($tokens); $i < $max; $i++) { + $token = $tokens[$i]; + + if (is_string($token)) { + continue; + } + + $class = ''; + + switch ($token[0]) { + case T_NAMESPACE: + $namespace = ''; + // If there is a namespace, extract it + while (($t = $tokens[++$i]) && is_array($t)) { + if (in_array($t[0], array(T_STRING, T_NS_SEPARATOR))) { + $namespace .= $t[1]; + } + } + $namespace .= '\\'; + break; + case T_CLASS: + case T_INTERFACE: + // Find the classname + while (($t = $tokens[++$i]) && is_array($t)) { + if (T_STRING === $t[0]) { + $class .= $t[1]; + } elseif ($class !== '' && T_WHITESPACE == $t[0]) { + break; + } + } + + if (empty($namespace)) { + $classes[] = $class; + } else { + $classes[] = $namespace . $class; + } + break; + default: + break; + } + } + + return $classes; + } +} + diff --git a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php index 81036e425..123faa5c5 100644 --- a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php +++ b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php @@ -134,6 +134,40 @@ class AutoloadGeneratorTest extends TestCase mkdir($this->vendorDir.'/.composer', 0777, true); $this->generator->dump($this->repository, $package, $this->im, $this->vendorDir.'/.composer'); $this->assertAutoloadFiles('vendors', $this->vendorDir.'/.composer'); + $this->assertTrue(file_exists($this->vendorDir.'/.composer/autoload_classmap.php'), "ClassMap file needs to be generated, even if empty."); + } + + public function testVendorsClassMapAutoloading() + { + $package = new MemoryPackage('a', '1.0', '1.0'); + + $packages = array(); + $packages[] = $a = new MemoryPackage('a/a', '1.0', '1.0'); + $packages[] = $b = new MemoryPackage('b/b', '1.0', '1.0'); + $a->setAutoload(array('classmap' => array('src/'))); + $b->setAutoload(array('classmap' => array('src/', 'lib/'))); + + $this->repository->expects($this->once()) + ->method('getPackages') + ->will($this->returnValue($packages)); + + @mkdir($this->vendorDir.'/.composer', 0777, true); + mkdir($this->vendorDir.'/a/a/src', 0777, true); + mkdir($this->vendorDir.'/b/b/src', 0777, true); + mkdir($this->vendorDir.'/b/b/lib', 0777, true); + file_put_contents($this->vendorDir.'/a/a/src/a.php', 'vendorDir.'/b/b/src/b.php', 'vendorDir.'/b/b/lib/c.php', 'generator->dump($this->repository, $package, $this->im, $this->vendorDir.'/.composer'); + $this->assertTrue(file_exists($this->vendorDir.'/.composer/autoload_classmap.php'), "ClassMap file needs to be generated, even if empty."); + $this->assertEquals(array( + 'ClassMapFoo' => $this->vendorDir.'/a/a/src/a.php', + 'ClassMapBar' => $this->vendorDir.'/b/b/src/b.php', + 'ClassMapBaz' => $this->vendorDir.'/b/b/lib/c.php', + ), + include ($this->vendorDir.'/.composer/autoload_classmap.php') + ); } public function testOverrideVendorsAutoloading() diff --git a/tests/Composer/Test/Autoload/ClassMapGeneratorTest.php b/tests/Composer/Test/Autoload/ClassMapGeneratorTest.php new file mode 100644 index 000000000..d40321768 --- /dev/null +++ b/tests/Composer/Test/Autoload/ClassMapGeneratorTest.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Autoload; + +use Composer\Autoload\ClassMapGenerator; + +class ClassMapGeneratorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider getTestCreateMapTests + */ + public function testCreateMap($directory, $expected) + { + $this->assertEqualsNormalized($expected, ClassMapGenerator::createMap($directory)); + } + + public function getTestCreateMapTests() + { + return array( + array(__DIR__.'/Fixtures/Namespaced', array( + 'Namespaced\\Bar' => realpath(__DIR__).'/Fixtures/Namespaced/Bar.php', + 'Namespaced\\Foo' => realpath(__DIR__).'/Fixtures/Namespaced/Foo.php', + 'Namespaced\\Baz' => realpath(__DIR__).'/Fixtures/Namespaced/Baz.php', + ) + ), + array(__DIR__.'/Fixtures/beta/NamespaceCollision', array( + 'NamespaceCollision\\A\\B\\Bar' => realpath(__DIR__).'/Fixtures/beta/NamespaceCollision/A/B/Bar.php', + 'NamespaceCollision\\A\\B\\Foo' => realpath(__DIR__).'/Fixtures/beta/NamespaceCollision/A/B/Foo.php', + )), + array(__DIR__.'/Fixtures/Pearlike', array( + 'Pearlike_Foo' => realpath(__DIR__).'/Fixtures/Pearlike/Foo.php', + 'Pearlike_Bar' => realpath(__DIR__).'/Fixtures/Pearlike/Bar.php', + 'Pearlike_Baz' => realpath(__DIR__).'/Fixtures/Pearlike/Baz.php', + )), + array(__DIR__.'/Fixtures/classmap', array( + 'Foo\\Bar\\A' => realpath(__DIR__).'/Fixtures/classmap/sameNsMultipleClasses.php', + 'Foo\\Bar\\B' => realpath(__DIR__).'/Fixtures/classmap/sameNsMultipleClasses.php', + 'Alpha\\A' => realpath(__DIR__).'/Fixtures/classmap/multipleNs.php', + 'Alpha\\B' => realpath(__DIR__).'/Fixtures/classmap/multipleNs.php', + 'Beta\\A' => realpath(__DIR__).'/Fixtures/classmap/multipleNs.php', + 'Beta\\B' => realpath(__DIR__).'/Fixtures/classmap/multipleNs.php', + 'ClassMap\\SomeInterface' => realpath(__DIR__).'/Fixtures/classmap/SomeInterface.php', + 'ClassMap\\SomeParent' => realpath(__DIR__).'/Fixtures/classmap/SomeParent.php', + 'ClassMap\\SomeClass' => realpath(__DIR__).'/Fixtures/classmap/SomeClass.php', + )), + ); + } + + public function testCreateMapFinderSupport() + { + if (!class_exists('Symfony\\Component\\Finder\\Finder')) { + $this->markTestSkipped('Finder component is not available'); + } + + $finder = new \Symfony\Component\Finder\Finder(); + $finder->files()->in(__DIR__ . '/Fixtures/beta/NamespaceCollision'); + + $this->assertEqualsNormalized(array( + 'NamespaceCollision\\A\\B\\Bar' => realpath(__DIR__).'/Fixtures/beta/NamespaceCollision/A/B/Bar.php', + 'NamespaceCollision\\A\\B\\Foo' => realpath(__DIR__).'/Fixtures/beta/NamespaceCollision/A/B/Foo.php', + ), ClassMapGenerator::createMap($finder)); + } + + protected function assertEqualsNormalized($expected, $actual, $message = null) + { + foreach ($expected as $ns => $path) { + $expected[$ns] = strtr($path, '\\', '/'); + } + foreach ($actual as $ns => $path) { + $actual[$ns] = strtr($path, '\\', '/'); + } + $this->assertEquals($expected, $actual, $message); + } +} diff --git a/tests/Composer/Test/Autoload/Fixtures/Namespaced/Bar.php b/tests/Composer/Test/Autoload/Fixtures/Namespaced/Bar.php new file mode 100644 index 000000000..f9c519a66 --- /dev/null +++ b/tests/Composer/Test/Autoload/Fixtures/Namespaced/Bar.php @@ -0,0 +1,8 @@ +