From 1af36043038f5221a4b7b5978fe34e9b558152c5 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Mon, 5 Mar 2012 13:26:46 +0100 Subject: [PATCH 1/4] Add support for class-map generation. --- res/composer-schema.json | 4 + src/Composer/Autoload/AutoloadGenerator.php | 13 +- src/Composer/Autoload/ClassLoader.php | 22 ++++ src/Composer/Autoload/ClassMapGenerator.php | 138 ++++++++++++++++++++ 4 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 src/Composer/Autoload/ClassMapGenerator.php diff --git a/res/composer-schema.json b/res/composer-schema.json index 62d6be0ed..308b0d9ba 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -128,6 +128,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..38e44ca2a 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,15 @@ EOF; } } } - $namespacesFile .= ");\n"; + if (isset($autoloads['classmap'])) { + $it = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($autoloads['classmap'])); + ClassMapGenerator::dump(iterator_to_array($it), $targetDir.'/autoload_classmap.php'); + } else { + file_put_contents($targetDir.'/autoload_classmap.php', '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..dad422929 --- /dev/null +++ b/src/Composer/Autoload/ClassMapGenerator.php @@ -0,0 +1,138 @@ + + * + * 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 array|string $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) + { + $dirs = (array) $dirs; + $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; + } +} + From 671cd5ee08b1135dc25527ded0c38e3062417688 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Mon, 5 Mar 2012 14:10:01 +0100 Subject: [PATCH 2/4] Add tests for ClassMapGenerator --- src/Composer/Autoload/AutoloadGenerator.php | 8 +- src/Composer/Autoload/ClassMapGenerator.php | 3 +- .../Test/Autoload/AutoloadGeneratorTest.php | 34 ++++++++ .../Test/Autoload/ClassMapGeneratorTest.php | 83 +++++++++++++++++++ .../Test/Autoload/Fixtures/Namespaced/Bar.php | 8 ++ .../Test/Autoload/Fixtures/Namespaced/Baz.php | 8 ++ .../Test/Autoload/Fixtures/Namespaced/Foo.php | 8 ++ .../Test/Autoload/Fixtures/Pearlike/Bar.php | 6 ++ .../Test/Autoload/Fixtures/Pearlike/Baz.php | 6 ++ .../Test/Autoload/Fixtures/Pearlike/Foo.php | 6 ++ .../beta/NamespaceCollision/A/B/Bar.php | 8 ++ .../beta/NamespaceCollision/A/B/Foo.php | 8 ++ .../Fixtures/beta/PrefixCollision/A/B/Bar.php | 6 ++ .../Fixtures/beta/PrefixCollision/A/B/Foo.php | 6 ++ .../Autoload/Fixtures/classmap/SomeClass.php | 8 ++ .../Fixtures/classmap/SomeInterface.php | 8 ++ .../Autoload/Fixtures/classmap/SomeParent.php | 8 ++ .../Autoload/Fixtures/classmap/multipleNs.php | 11 +++ .../Autoload/Fixtures/classmap/notAClass.php | 3 + .../Autoload/Fixtures/classmap/notPhpFile.md | 1 + .../classmap/sameNsMultipleClasses.php | 6 ++ 21 files changed, 238 insertions(+), 5 deletions(-) create mode 100644 tests/Composer/Test/Autoload/ClassMapGeneratorTest.php create mode 100644 tests/Composer/Test/Autoload/Fixtures/Namespaced/Bar.php create mode 100644 tests/Composer/Test/Autoload/Fixtures/Namespaced/Baz.php create mode 100644 tests/Composer/Test/Autoload/Fixtures/Namespaced/Foo.php create mode 100644 tests/Composer/Test/Autoload/Fixtures/Pearlike/Bar.php create mode 100644 tests/Composer/Test/Autoload/Fixtures/Pearlike/Baz.php create mode 100644 tests/Composer/Test/Autoload/Fixtures/Pearlike/Foo.php create mode 100644 tests/Composer/Test/Autoload/Fixtures/beta/NamespaceCollision/A/B/Bar.php create mode 100644 tests/Composer/Test/Autoload/Fixtures/beta/NamespaceCollision/A/B/Foo.php create mode 100644 tests/Composer/Test/Autoload/Fixtures/beta/PrefixCollision/A/B/Bar.php create mode 100644 tests/Composer/Test/Autoload/Fixtures/beta/PrefixCollision/A/B/Foo.php create mode 100644 tests/Composer/Test/Autoload/Fixtures/classmap/SomeClass.php create mode 100644 tests/Composer/Test/Autoload/Fixtures/classmap/SomeInterface.php create mode 100644 tests/Composer/Test/Autoload/Fixtures/classmap/SomeParent.php create mode 100644 tests/Composer/Test/Autoload/Fixtures/classmap/multipleNs.php create mode 100644 tests/Composer/Test/Autoload/Fixtures/classmap/notAClass.php create mode 100644 tests/Composer/Test/Autoload/Fixtures/classmap/notPhpFile.md create mode 100644 tests/Composer/Test/Autoload/Fixtures/classmap/sameNsMultipleClasses.php diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index 38e44ca2a..59addd0b4 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -115,12 +115,14 @@ EOF; $namespacesFile .= ");\n"; if (isset($autoloads['classmap'])) { - $it = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($autoloads['classmap'])); - ClassMapGenerator::dump(iterator_to_array($it), $targetDir.'/autoload_classmap.php'); + // flatten array + $autoloads['classmap'] = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($autoloads['classmap'])); } else { - file_put_contents($targetDir.'/autoload_classmap.php', '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 @@ + Date: Mon, 5 Mar 2012 14:13:06 +0100 Subject: [PATCH 3/4] Explain classmap generation in documentation --- doc/04-schema.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/doc/04-schema.md b/doc/04-schema.md index 0572da36f..5bf6b49d7 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 is 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. From 590ee419bd3a355f60f46612e3d9bd80a50b49f0 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Mon, 5 Mar 2012 14:21:51 +0100 Subject: [PATCH 4/4] Fix docs --- doc/04-schema.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/04-schema.md b/doc/04-schema.md index 5bf6b49d7..7f451b3c3 100644 --- a/doc/04-schema.md +++ b/doc/04-schema.md @@ -184,7 +184,7 @@ Optional. Autoload mapping for a PHP autoloader. Currently [PSR-0](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md) -autoloading and ClassMap generation is supported. +autoloading and ClassMap generation are supported. Under the `psr-0` key you define a mapping from namespaces to paths, relative to the package root.