Browse Source

Merge remote-tracking branch 'beberlei/ClassMaps'

Jordi Boggiano 13 years ago
parent
commit
27eb249aab
24 changed files with 426 additions and 5 deletions
  1. 17 4
      doc/04-schema.md
  2. 4 0
      res/composer-schema.json
  3. 14 1
      src/Composer/Autoload/AutoloadGenerator.php
  4. 22 0
      src/Composer/Autoload/ClassLoader.php
  5. 137 0
      src/Composer/Autoload/ClassMapGenerator.php
  6. 34 0
      tests/Composer/Test/Autoload/AutoloadGeneratorTest.php
  7. 83 0
      tests/Composer/Test/Autoload/ClassMapGeneratorTest.php
  8. 8 0
      tests/Composer/Test/Autoload/Fixtures/Namespaced/Bar.php
  9. 8 0
      tests/Composer/Test/Autoload/Fixtures/Namespaced/Baz.php
  10. 8 0
      tests/Composer/Test/Autoload/Fixtures/Namespaced/Foo.php
  11. 6 0
      tests/Composer/Test/Autoload/Fixtures/Pearlike/Bar.php
  12. 6 0
      tests/Composer/Test/Autoload/Fixtures/Pearlike/Baz.php
  13. 6 0
      tests/Composer/Test/Autoload/Fixtures/Pearlike/Foo.php
  14. 8 0
      tests/Composer/Test/Autoload/Fixtures/beta/NamespaceCollision/A/B/Bar.php
  15. 8 0
      tests/Composer/Test/Autoload/Fixtures/beta/NamespaceCollision/A/B/Foo.php
  16. 6 0
      tests/Composer/Test/Autoload/Fixtures/beta/PrefixCollision/A/B/Bar.php
  17. 6 0
      tests/Composer/Test/Autoload/Fixtures/beta/PrefixCollision/A/B/Foo.php
  18. 8 0
      tests/Composer/Test/Autoload/Fixtures/classmap/SomeClass.php
  19. 8 0
      tests/Composer/Test/Autoload/Fixtures/classmap/SomeInterface.php
  20. 8 0
      tests/Composer/Test/Autoload/Fixtures/classmap/SomeParent.php
  21. 11 0
      tests/Composer/Test/Autoload/Fixtures/classmap/multipleNs.php
  22. 3 0
      tests/Composer/Test/Autoload/Fixtures/classmap/notAClass.php
  23. 1 0
      tests/Composer/Test/Autoload/Fixtures/classmap/notPhpFile.md
  24. 6 0
      tests/Composer/Test/Autoload/Fixtures/classmap/sameNsMultipleClasses.php

+ 17 - 4
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) →
+← [Command-line interface](03-cli.md)  |  [Repositories](05-repositories.md) →

+ 4 - 0
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."
                 }
             }
         },

+ 14 - 1
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');

+ 22 - 0
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);
         }

+ 137 - 0
src/Composer/Autoload/ClassMapGenerator.php

@@ -0,0 +1,137 @@
+<?php
+
+/*
+ * This file is copied from the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * 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 <salla016@gmail.com>
+ */
+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('<?php return %s;', var_export($maps, true)));
+    }
+
+    /**
+     * Iterate over all files in the given directory searching for classes
+     *
+     * @param Iterator|string $dir The directory to search in or an iterator
+     *
+     * @return array A class map array
+     */
+    static public function createMap($dir)
+    {
+        if (is_string($dir)) {
+            $dir = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir));
+        }
+
+        $map = array();
+
+        foreach ($dir as $file) {
+            if (!$file->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;
+    }
+}
+

+ 34 - 0
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', '<?php class ClassMapFoo {}');
+        file_put_contents($this->vendorDir.'/b/b/src/b.php', '<?php class ClassMapBar {}');
+        file_put_contents($this->vendorDir.'/b/b/lib/c.php', '<?php class ClassMapBaz {}');
+
+        $this->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()

+ 83 - 0
tests/Composer/Test/Autoload/ClassMapGeneratorTest.php

@@ -0,0 +1,83 @@
+<?php
+
+/*
+ * This file was copied from the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * 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);
+    }
+}

+ 8 - 0
tests/Composer/Test/Autoload/Fixtures/Namespaced/Bar.php

@@ -0,0 +1,8 @@
+<?php
+
+namespace Namespaced;
+
+class Bar
+{
+    public static $loaded = true;
+}

+ 8 - 0
tests/Composer/Test/Autoload/Fixtures/Namespaced/Baz.php

@@ -0,0 +1,8 @@
+<?php
+
+namespace Namespaced;
+
+class Baz
+{
+    public static $loaded = true;
+}

+ 8 - 0
tests/Composer/Test/Autoload/Fixtures/Namespaced/Foo.php

@@ -0,0 +1,8 @@
+<?php
+
+namespace Namespaced;
+
+class Foo
+{
+    public static $loaded = true;
+}

+ 6 - 0
tests/Composer/Test/Autoload/Fixtures/Pearlike/Bar.php

@@ -0,0 +1,6 @@
+<?php
+
+class Pearlike_Bar
+{
+    public static $loaded = true;
+}

+ 6 - 0
tests/Composer/Test/Autoload/Fixtures/Pearlike/Baz.php

@@ -0,0 +1,6 @@
+<?php
+
+class Pearlike_Baz
+{
+    public static $loaded = true;
+}

+ 6 - 0
tests/Composer/Test/Autoload/Fixtures/Pearlike/Foo.php

@@ -0,0 +1,6 @@
+<?php
+
+class Pearlike_Foo
+{
+    public static $loaded = true;
+}

+ 8 - 0
tests/Composer/Test/Autoload/Fixtures/beta/NamespaceCollision/A/B/Bar.php

@@ -0,0 +1,8 @@
+<?php
+
+namespace NamespaceCollision\A\B;
+
+class Bar
+{
+    public static $loaded = true;
+}

+ 8 - 0
tests/Composer/Test/Autoload/Fixtures/beta/NamespaceCollision/A/B/Foo.php

@@ -0,0 +1,8 @@
+<?php
+
+namespace NamespaceCollision\A\B;
+
+class Foo
+{
+    public static $loaded = true;
+}

+ 6 - 0
tests/Composer/Test/Autoload/Fixtures/beta/PrefixCollision/A/B/Bar.php

@@ -0,0 +1,6 @@
+<?php
+
+class PrefixCollision_A_B_Bar
+{
+    public static $loaded = true;
+}

+ 6 - 0
tests/Composer/Test/Autoload/Fixtures/beta/PrefixCollision/A/B/Foo.php

@@ -0,0 +1,6 @@
+<?php
+
+class PrefixCollision_A_B_Foo
+{
+    public static $loaded = true;
+}

+ 8 - 0
tests/Composer/Test/Autoload/Fixtures/classmap/SomeClass.php

@@ -0,0 +1,8 @@
+<?php
+
+namespace ClassMap;
+
+class SomeClass extends SomeParent implements SomeInterface
+{
+
+}

+ 8 - 0
tests/Composer/Test/Autoload/Fixtures/classmap/SomeInterface.php

@@ -0,0 +1,8 @@
+<?php
+
+namespace ClassMap;
+
+interface SomeInterface
+{
+
+}

+ 8 - 0
tests/Composer/Test/Autoload/Fixtures/classmap/SomeParent.php

@@ -0,0 +1,8 @@
+<?php
+
+namespace ClassMap;
+
+abstract class SomeParent
+{
+
+}

+ 11 - 0
tests/Composer/Test/Autoload/Fixtures/classmap/multipleNs.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace Alpha {
+    class A {}
+    class B {}
+}
+
+namespace Beta {
+    class A {}
+    class B {}
+}

+ 3 - 0
tests/Composer/Test/Autoload/Fixtures/classmap/notAClass.php

@@ -0,0 +1,3 @@
+<?php
+
+$a = new stdClass();

+ 1 - 0
tests/Composer/Test/Autoload/Fixtures/classmap/notPhpFile.md

@@ -0,0 +1 @@
+This file should be skipped.

+ 6 - 0
tests/Composer/Test/Autoload/Fixtures/classmap/sameNsMultipleClasses.php

@@ -0,0 +1,6 @@
+<?php
+
+namespace Foo\Bar;
+
+class A {}
+class B {}