Browse Source

Merge remote-tracking branch 'donquixote/feature/psr4-complete'

Jordi Boggiano 11 years ago
parent
commit
b23742e30c

+ 1 - 0
.travis.yml

@@ -15,6 +15,7 @@ before_script:
     - sudo apt-get install parallel
     - rm -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini
     - composer install --dev --prefer-source
+    - bin/composer install --dev --prefer-source
     - git config --global user.name travis-ci
     - git config --global user.email travis@example.com
 

+ 4 - 0
doc/04-schema.md

@@ -438,6 +438,10 @@ use an empty prefix like:
         }
     }
 
+#### PSR-4
+
+Stub: Similar to PSR-0.
+
 #### Classmap
 
 The `classmap` references are all combined, during install/update, into a single

+ 5 - 0
res/composer-schema.json

@@ -207,6 +207,11 @@
                     "description": "This is a hash of namespaces (keys) and the directories they can be found into (values, can be arrays of paths) by the autoloader.",
                     "additionalProperties": true
                 },
+                "psr-4": {
+                    "type": "object",
+                    "description": "This is a hash of namespaces (keys) and the PSR-4 directories they can be found into (values, can be arrays of paths) 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."

+ 91 - 5
src/Composer/Autoload/AutoloadGenerator.php

@@ -69,9 +69,23 @@ return array(
 
 EOF;
 
+        $psr4File = <<<EOF
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+\$vendorDir = $vendorPathCode52;
+\$baseDir = $appBaseDirCode;
+
+return array(
+
+EOF;
+
+        // Collect information from all packages.
         $packageMap = $this->buildPackageMap($installationManager, $mainPackage, $localRepo->getCanonicalPackages());
         $autoloads = $this->parseAutoloads($packageMap, $mainPackage);
 
+        // Process the 'psr-0' base directories.
         foreach ($autoloads['psr-0'] as $namespace => $paths) {
             $exportedPaths = array();
             foreach ($paths as $path) {
@@ -83,6 +97,21 @@ EOF;
         }
         $namespacesFile .= ");\n";
 
+        // Process the 'psr-4' base directories.
+        foreach ($autoloads['psr-4'] as $namespace => $paths) {
+            if ('\\' !== $namespace[strlen($namespace) - 1]) {
+                throw new \Exception("PSR-4 namespaces must end with a namespace separator. '$namespace' does not.");
+            }
+            $exportedPaths = array();
+            foreach ($paths as $path) {
+                $exportedPaths[] = $this->getPathCode($filesystem, $basePath, $vendorPath, $path);
+            }
+            $exportedPrefix = var_export($namespace, true);
+            $psr4File .= "    $exportedPrefix => ";
+            $psr4File .= "array(".implode(', ', $exportedPaths)."),\n";
+        }
+        $psr4File .= ");\n";
+
         $classmapFile = <<<EOF
 <?php
 
@@ -131,6 +160,8 @@ EOF;
         // flatten array
         $classMap = array();
         if ($scanPsr0Packages) {
+            // Scan the PSR-0 directories for class files, and add them to the
+            // class map.
             foreach ($autoloads['psr-0'] as $namespace => $paths) {
                 foreach ($paths as $dir) {
                     $dir = $filesystem->normalizePath($filesystem->isAbsolutePath($dir) ? $dir : $basePath.'/'.$dir);
@@ -152,6 +183,29 @@ EOF;
                     }
                 }
             }
+            // Scan the PSR-4 directories for class files, and add them to the
+            // class map.
+            foreach ($autoloads['psr-4'] as $namespace => $paths) {
+                foreach ($paths as $dir) {
+                    $dir = $filesystem->normalizePath($filesystem->isAbsolutePath($dir) ? $dir : $basePath.'/'.$dir);
+                    if (!is_dir($dir)) {
+                        continue;
+                    }
+                    $whitelist = sprintf(
+                        '{%s/%s.+(?<!(?<!/)Test\.php)$}',
+                        preg_quote($dir),
+                        strpos($namespace, '_') === false ? preg_quote(strtr($namespace, '\\', '/')) : ''
+                    );
+                    foreach (ClassMapGenerator::createMap($dir, $whitelist) as $class => $path) {
+                        if ('' === $namespace || 0 === strpos($class, $namespace)) {
+                            if (!isset($classMap[$class])) {
+                                $path = $this->getPathCode($filesystem, $basePath, $vendorPath, $path);
+                                $classMap[$class] = $path.",\n";
+                            }
+                        }
+                    }
+                }
+            }
         }
 
         $autoloads['classmap'] = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($autoloads['classmap']));
@@ -173,6 +227,7 @@ EOF;
         }
 
         file_put_contents($targetDir.'/autoload_namespaces.php', $namespacesFile);
+        file_put_contents($targetDir.'/autoload_psr4.php', $psr4File);
         file_put_contents($targetDir.'/autoload_classmap.php', $classmapFile);
         if ($includePathFile = $this->getIncludePathsFile($packageMap, $filesystem, $basePath, $vendorPath, $vendorPathCode52, $appBaseDirCode)) {
             file_put_contents($targetDir.'/include_paths.php', $includePathFile);
@@ -181,7 +236,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, 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));
 
         // use stream_copy_to_stream instead of copy
         // to work around https://bugs.php.net/bug.php?id=64634
@@ -204,6 +259,7 @@ EOF;
             if ($package instanceof AliasPackage) {
                 continue;
             }
+            $this->validatePackage($package);
 
             $packageMap[] = array(
                 $package,
@@ -214,6 +270,21 @@ EOF;
         return $packageMap;
     }
 
+    /**
+     * @param PackageInterface $package
+     *
+     * @throws \Exception
+     *   Throws an exception, if the package has illegal settings.
+     */
+    protected function validatePackage(PackageInterface $package) {
+        $autoload = $package->getAutoload();
+        if (!empty($autoload['psr-4']) && null !== $package->getTargetDir()) {
+            $name = $package->getName();
+            $package->getTargetDir();
+            throw new \Exception("The ['autoload']['psr-4'] setting is incompatible with the ['target-dir'] setting, in package '$name'.");
+        }
+    }
+
     /**
      * Compiles an ordered list of namespace => path mappings
      *
@@ -229,12 +300,14 @@ EOF;
         array_unshift($packageMap, $mainPackageMap);
 
         $psr0 = $this->parseAutoloadsType($packageMap, 'psr-0', $mainPackage);
+        $psr4 = $this->parseAutoloadsType($packageMap, 'psr-4', $mainPackage);
         $classmap = $this->parseAutoloadsType($sortedPackageMap, 'classmap', $mainPackage);
         $files = $this->parseAutoloadsType($sortedPackageMap, 'files', $mainPackage);
 
         krsort($psr0);
+        krsort($psr4);
 
-        return array('psr-0' => $psr0, 'classmap' => $classmap, 'files' => $files);
+        return array('psr-0' => $psr0, 'psr-4' => $psr4, 'classmap' => $classmap, 'files' => $files);
     }
 
     /**
@@ -253,6 +326,12 @@ EOF;
             }
         }
 
+        if (isset($autoloads['psr-4'])) {
+            foreach ($autoloads['psr-4'] as $namespace => $path) {
+                $loader->addPsr4($namespace, $path);
+            }
+        }
+
         return $loader;
     }
 
@@ -366,7 +445,7 @@ return ComposerAutoloaderInit$suffix::getLoader();
 AUTOLOAD;
     }
 
-    protected function getAutoloadRealFile($usePSR0, $useClassMap, $useIncludePath, $targetDirLoader, $useIncludeFiles, $vendorPathCode, $appBaseDirCode, $suffix, $useGlobalIncludePath, $prependAutoloader)
+    protected function getAutoloadRealFile($useClassMap, $useIncludePath, $targetDirLoader, $useIncludeFiles, $vendorPathCode, $appBaseDirCode, $suffix, $useGlobalIncludePath, $prependAutoloader)
     {
         // TODO the class ComposerAutoloaderInit should be revert to a closure
         // when APC has been fixed:
@@ -417,8 +496,7 @@ HEADER;
 INCLUDE_PATH;
         }
 
-        if ($usePSR0) {
-            $file .= <<<'PSR0'
+        $file .= <<<'PSR0'
         $map = require __DIR__ . '/autoload_namespaces.php';
         foreach ($map as $namespace => $path) {
             $loader->set($namespace, $path);
@@ -426,8 +504,16 @@ INCLUDE_PATH;
 
 
 PSR0;
+
+        $file .= <<<'PSR4'
+        $map = require __DIR__ . '/autoload_psr4.php';
+        foreach ($map as $namespace => $path) {
+            $loader->setPsr4($namespace, $path);
         }
 
+
+PSR4;
+
         if ($useClassMap) {
             $file .= <<<'CLASSMAP'
         $classMap = require __DIR__ . '/autoload_classmap.php';

+ 149 - 39
src/Composer/Autoload/ClassLoader.php

@@ -42,19 +42,36 @@ namespace Composer\Autoload;
  */
 class ClassLoader
 {
-    private $prefixes = array();
-    private $fallbackDirs = array();
+    // PSR-4
+    private $prefixLengthsPsr4 = array();
+    private $prefixDirsPsr4 = array();
+    private $fallbackDirsPsr4 = array();
+
+    // PSR-0
+    private $prefixesPsr0 = array();
+    private $fallbackDirsPsr0 = array();
+
     private $useIncludePath = false;
     private $classMap = array();
 
     public function getPrefixes()
     {
-        return call_user_func_array('array_merge', $this->prefixes);
+        return call_user_func_array('array_merge', $this->prefixesPsr0);
+    }
+
+    public function getPrefixesPsr4()
+    {
+        return $this->prefixDirsPsr4;
     }
 
     public function getFallbackDirs()
     {
-        return $this->fallbackDirs;
+        return $this->fallbackDirsPsr0;
+    }
+
+    public function getFallbackDirsPsr4()
+    {
+        return $this->fallbackDirsPsr4;
     }
 
     public function getClassMap()
@@ -75,23 +92,24 @@ class ClassLoader
     }
 
     /**
-     * Registers a set of classes, merging with any others previously set.
+     * Registers a set of PSR-0 directories for a given prefix, either
+     * appending or prepending to the ones previously set for this prefix.
      *
-     * @param string       $prefix  The classes prefix
-     * @param array|string $paths   The location(s) of the classes
-     * @param bool         $prepend Prepend the location(s)
+     * @param string       $prefix  The prefix
+     * @param array|string $paths   The PSR-0 root directories
+     * @param bool         $prepend Whether to prepend the directories
      */
     public function add($prefix, $paths, $prepend = false)
     {
         if (!$prefix) {
             if ($prepend) {
-                $this->fallbackDirs = array_merge(
+                $this->fallbackDirsPsr0 = array_merge(
                     (array) $paths,
-                    $this->fallbackDirs
+                    $this->fallbackDirsPsr0
                 );
             } else {
-                $this->fallbackDirs = array_merge(
-                    $this->fallbackDirs,
+                $this->fallbackDirsPsr0 = array_merge(
+                    $this->fallbackDirsPsr0,
                     (array) $paths
                 );
             }
@@ -100,38 +118,104 @@ class ClassLoader
         }
 
         $first = $prefix[0];
-        if (!isset($this->prefixes[$first][$prefix])) {
-            $this->prefixes[$first][$prefix] = (array) $paths;
+        if (!isset($this->prefixesPsr0[$first][$prefix])) {
+            $this->prefixesPsr0[$first][$prefix] = (array) $paths;
 
             return;
         }
         if ($prepend) {
-            $this->prefixes[$first][$prefix] = array_merge(
+            $this->prefixesPsr0[$first][$prefix] = array_merge(
                 (array) $paths,
-                $this->prefixes[$first][$prefix]
+                $this->prefixesPsr0[$first][$prefix]
             );
         } else {
-            $this->prefixes[$first][$prefix] = array_merge(
-                $this->prefixes[$first][$prefix],
+            $this->prefixesPsr0[$first][$prefix] = array_merge(
+                $this->prefixesPsr0[$first][$prefix],
                 (array) $paths
             );
         }
     }
 
     /**
-     * Registers a set of classes, replacing any others previously set.
+     * Registers a set of PSR-4 directories for a given namespace, either
+     * appending or prepending to the ones previously set for this namespace.
      *
-     * @param string       $prefix The classes prefix
-     * @param array|string $paths  The location(s) of the classes
+     * @param string       $prefix  The prefix/namespace, with trailing '\\'
+     * @param array|string $paths   The PSR-0 base directories
+     * @param bool         $prepend Whether to prepend the directories
+     */
+    public function addPsr4($prefix, $paths, $prepend = false)
+    {
+        if (!$prefix) {
+            // Register directories for the root namespace.
+            if ($prepend) {
+                $this->fallbackDirsPsr4 = array_merge(
+                    (array) $paths,
+                    $this->fallbackDirsPsr4
+                );
+            } else {
+                $this->fallbackDirsPsr4 = array_merge(
+                    $this->fallbackDirsPsr4,
+                    (array) $paths
+                );
+            }
+        } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+            // Register directories for a new namespace.
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \Exception("A non-empty PSR-4 prefix must end with a namespace separator.");
+            }
+            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+        } elseif ($prepend) {
+            // Prepend directories for an already registered namespace.
+            $this->prefixDirsPsr4[$prefix] = array_merge(
+                (array) $paths,
+                $this->prefixDirsPsr4[$prefix]
+            );
+        } else {
+            // Append directories for an already registered namespace.
+            $this->prefixDirsPsr4[$prefix] = array_merge(
+                $this->prefixDirsPsr4[$prefix],
+                (array) $paths
+            );
+        }
+    }
+
+    /**
+     * Registers a set of PSR-0 directories for a given prefix,
+     * replacing any others previously set for this prefix.
+     *
+     * @param string       $prefix The prefix
+     * @param array|string $paths  The PSR-0 base directories
      */
     public function set($prefix, $paths)
     {
         if (!$prefix) {
-            $this->fallbackDirs = (array) $paths;
+            $this->fallbackDirsPsr0 = (array) $paths;
+        } else {
+            $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+        }
+    }
 
-            return;
+    /**
+     * Registers a set of PSR-4 directories for a given namespace,
+     * replacing any others previously set for this namespace.
+     *
+     * @param string       $prefix  The prefix/namespace, with trailing '\\'
+     * @param array|string $paths   The PSR-4 base directories
+     */
+    public function setPsr4($prefix, $paths) {
+        if (!$prefix) {
+            $this->fallbackDirsPsr4 = (array) $paths;
+        } else {
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \Exception("A non-empty PSR-4 prefix must end with a namespace separator.");
+            }
+            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            $this->prefixDirsPsr4[$prefix] = (array) $paths;
         }
-        $this->prefixes[substr($prefix, 0, 1)][$prefix] = (array) $paths;
     }
 
     /**
@@ -202,45 +286,71 @@ class ClassLoader
             $class = substr($class, 1);
         }
 
+        // class map lookup
         if (isset($this->classMap[$class])) {
             return $this->classMap[$class];
         }
 
+        // PSR-4 lookup
+        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . '.php';
+
+        $first = $class[0];
+        if (isset($this->prefixLengthsPsr4[$first])) {
+            foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) {
+                if (0 === strpos($class, $prefix)) {
+                    foreach ($this->prefixDirsPsr4[$prefix] as $dir) {
+                        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // PSR-4 fallback dirs
+        foreach ($this->fallbackDirsPsr4 as $dir) {
+            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+                return $file;
+            }
+        }
+
+        // PSR-0 lookup
         if (false !== $pos = strrpos($class, '\\')) {
             // namespaced class name
-            $classPath = strtr(substr($class, 0, $pos), '\\', DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
-            $className = substr($class, $pos + 1);
+            $logicalPathPsr0
+              = substr($logicalPathPsr4, 0, $pos + 1)
+              . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR)
+            ;
         } else {
             // PEAR-like class name
-            $classPath = null;
-            $className = $class;
+            $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . '.php';
         }
 
-        $classPath .= strtr($className, '_', DIRECTORY_SEPARATOR) . '.php';
-
-        $first = $class[0];
-        if (isset($this->prefixes[$first])) {
-            foreach ($this->prefixes[$first] as $prefix => $dirs) {
+        if (isset($this->prefixesPsr0[$first])) {
+            foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
                 if (0 === strpos($class, $prefix)) {
                     foreach ($dirs as $dir) {
-                        if (file_exists($dir . DIRECTORY_SEPARATOR . $classPath)) {
-                            return $dir . DIRECTORY_SEPARATOR . $classPath;
+                        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+                            return $file;
                         }
                     }
                 }
             }
         }
 
-        foreach ($this->fallbackDirs as $dir) {
-            if (file_exists($dir . DIRECTORY_SEPARATOR . $classPath)) {
-                return $dir . DIRECTORY_SEPARATOR . $classPath;
+        // PSR-0 fallback dirs
+        foreach ($this->fallbackDirsPsr0 as $dir) {
+            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+                return $file;
             }
         }
 
-        if ($this->useIncludePath && $file = stream_resolve_include_path($classPath)) {
+        // PSR-0 include paths.
+        if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
             return $file;
         }
 
+        // Remember that this class does not exist.
         return $this->classMap[$class] = false;
     }
 }

+ 4 - 0
src/Composer/Command/ShowCommand.php

@@ -303,6 +303,10 @@ EOT
                     foreach ($autoloads as $name => $path) {
                         $output->writeln(($name ?: '*') . ' => ' . (is_array($path) ? implode(', ', $path) : ($path ?: '.')));
                     }
+                } elseif ($type === 'psr-4') {
+                    foreach ($autoloads as $name => $path) {
+                        $output->writeln(($name ?: '*') . ' => ' . (is_array($path) ? implode(', ', $path) : ($path ?: '.')));
+                    }
                 } elseif ($type === 'classmap') {
                     $output->writeln(implode(', ', $autoloads));
                 }

+ 1 - 0
src/Composer/Compiler.php

@@ -103,6 +103,7 @@ class Compiler
 
         $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/autoload.php'));
         $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/autoload_namespaces.php'));
+        $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/autoload_psr4.php'));
         $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/autoload_classmap.php'));
         $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/autoload_real.php'));
         if (file_exists(__DIR__.'/../../vendor/composer/include_paths.php')) {

+ 8 - 1
src/Composer/Package/Loader/ValidatingArrayLoader.php

@@ -180,7 +180,7 @@ class ValidatingArrayLoader implements LoaderInterface
         }
 
         if ($this->validateArray('autoload') && !empty($this->config['autoload'])) {
-            $types = array('psr-0', 'classmap', 'files');
+            $types = array('psr-0', 'psr-4', 'classmap', 'files');
             foreach ($this->config['autoload'] as $type => $typeConfig) {
                 if (!in_array($type, $types)) {
                     $this->errors[] = 'autoload : invalid value ('.$type.'), must be one of '.implode(', ', $types);
@@ -189,6 +189,13 @@ class ValidatingArrayLoader implements LoaderInterface
             }
         }
 
+        if (!empty($this->config['autoload']['psr-4']) && !empty($this->config['target-dir'])) {
+            $this->errors[] = "The ['autoload']['psr-4'] setting is incompatible with the ['target-dir'] setting.";
+            // Unset the psr-4 setting, since unsetting target-dir might
+            // interfere with other settings.
+            unset($this->config['autoload']['psr-4']);
+        }
+
         // TODO validate dist
         // TODO validate source
 

+ 116 - 3
tests/Composer/Test/Autoload/AutoloadGeneratorTest.php

@@ -95,7 +95,14 @@ class AutoloadGeneratorTest extends TestCase
     {
         $package = new Package('a', '1.0', '1.0');
         $package->setAutoload(array(
-            'psr-0' => array('Main' => 'src/', 'Lala' => array('src/', 'lib/')),
+            '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/'),
         ));
 
@@ -107,11 +114,22 @@ class AutoloadGeneratorTest extends TestCase
         $this->fs->ensureDirectoryExists($this->workingDir.'/src');
         $this->fs->ensureDirectoryExists($this->workingDir.'/lib');
 
+        $this->fs->ensureDirectoryExists($this->workingDir.'/src-fruit');
+        $this->fs->ensureDirectoryExists($this->workingDir.'/src-cake');
+        $this->fs->ensureDirectoryExists($this->workingDir.'/lib-cake');
+
         $this->fs->ensureDirectoryExists($this->workingDir.'/composersrc');
         file_put_contents($this->workingDir.'/composersrc/foo.php', '<?php class ClassMapFoo {}');
 
         $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_1');
+
+        // Assert that autoload_namespaces.php was correctly generated.
         $this->assertAutoloadFiles('main', $this->vendorDir.'/composer');
+
+        // Assert that autoload_psr4.php was correctly generated.
+        $this->assertAutoloadFiles('psr4', $this->vendorDir.'/composer', 'psr4');
+
+        // Assert that autoload_classmap.php was correctly generated.
         $this->assertAutoloadFiles('classmap', $this->vendorDir.'/composer', 'classmap');
     }
 
@@ -122,6 +140,10 @@ class AutoloadGeneratorTest extends TestCase
         $package = new Package('a', '1.0', '1.0');
         $package->setAutoload(array(
             'psr-0' => array('Main' => 'src/', 'Lala' => 'src/'),
+            'psr-4' => array(
+                'Acme\Fruit\\' => 'src-fruit/',
+                'Acme\Cake\\' => array('src-cake/', 'lib-cake/'),
+            ),
             'classmap' => array('composersrc/'),
         ));
 
@@ -138,6 +160,7 @@ class AutoloadGeneratorTest extends TestCase
 
         $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_2');
         $this->assertAutoloadFiles('main3', $this->vendorDir.'/composer');
+        $this->assertAutoloadFiles('psr4_3', $this->vendorDir.'/composer', 'psr4');
         $this->assertAutoloadFiles('classmap3', $this->vendorDir.'/composer', 'classmap');
     }
 
@@ -146,6 +169,10 @@ class AutoloadGeneratorTest extends TestCase
         $package = new Package('a', '1.0', '1.0');
         $package->setAutoload(array(
             'psr-0' => array('Main' => 'src/', 'Lala' => 'src/'),
+            'psr-4' => array(
+                'Acme\Fruit\\' => 'src-fruit/',
+                'Acme\Cake\\' => array('src-cake/', 'lib-cake/'),
+            ),
             'classmap' => array('composersrc/'),
         ));
 
@@ -162,6 +189,7 @@ class AutoloadGeneratorTest extends TestCase
         file_put_contents($this->workingDir.'/composersrc/foo.php', '<?php class ClassMapFoo {}');
         $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_3');
         $this->assertAutoloadFiles('main2', $this->vendorDir.'/composer');
+        $this->assertAutoloadFiles('psr4_2', $this->vendorDir.'/composer', 'psr4');
         $this->assertAutoloadFiles('classmap2', $this->vendorDir.'/composer', 'classmap');
     }
 
@@ -170,6 +198,10 @@ class AutoloadGeneratorTest extends TestCase
         $package = new Package('a', '1.0', '1.0');
         $package->setAutoload(array(
             'psr-0' => array('Main\\Foo' => '', 'Main\\Bar' => ''),
+            'psr-4' => array(
+                'Acme\Fruit\\' => 'src-fruit/',
+                'Acme\Cake\\' => array('src-cake/', 'lib-cake/'),
+            ),
             'classmap' => array('Main/Foo/src', 'lib'),
             'files' => array('foo.php', 'Main/Foo/bar.php'),
         ));
@@ -486,6 +518,20 @@ return array(
     'A' => array(\$vendorDir . '/a/a/src'),
 );
 
+EOF;
+
+        // autoload_psr4.php is expected to be empty in this example.
+        $expectedPsr4 = <<<EOF
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+\$vendorDir = dirname(dirname(__FILE__));
+\$baseDir = dirname(\$vendorDir);
+
+return array(
+);
+
 EOF;
 
         $expectedClassmap = <<<EOF
@@ -505,6 +551,7 @@ EOF;
 
         $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_9');
         $this->assertEquals($expectedNamespace, file_get_contents($this->vendorDir.'/composer/autoload_namespaces.php'));
+        $this->assertEquals($expectedPsr4, file_get_contents($this->vendorDir.'/composer/autoload_psr4.php'));
         $this->assertEquals($expectedClassmap, file_get_contents($this->vendorDir.'/composer/autoload_classmap.php'));
     }
 
@@ -678,6 +725,7 @@ EOF;
         $package = new Package('a', '1.0', '1.0');
         $package->setAutoload(array(
             'psr-0' => array('Foo' => 'src'),
+            'psr-4' => array('Acme\Foo\\' => 'src-psr4'),
             'classmap' => array('classmap'),
             'files' => array('test.php'),
         ));
@@ -685,6 +733,7 @@ EOF;
         $vendorPackage = new Package('b/b', '1.0', '1.0');
         $vendorPackage->setAutoload(array(
             'psr-0' => array('Bar' => 'lib'),
+            'psr-4' => array('Acme\Bar\\' => 'lib-psr4'),
             'classmap' => array('classmaps'),
             'files' => array('bootstrap.php'),
         ));
@@ -734,6 +783,21 @@ return array(
     'Bar' => array($vendorDir . '/b/b/lib'),
 );
 
+EOF;
+
+        $expectedPsr4 = <<<'EOF'
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir).'/working-dir';
+
+return array(
+    'Acme\\Foo\\' => array($baseDir . '/src-psr4'),
+    'Acme\\Bar\\' => array($vendorDir . '/b/b/lib-psr4'),
+);
+
 EOF;
 
         $expectedClassmap = <<<'EOF'
@@ -754,6 +818,7 @@ return array(
 EOF;
 
         $this->assertEquals($expectedNamespace, file_get_contents($vendorDir.'/composer/autoload_namespaces.php'));
+        $this->assertEquals($expectedPsr4, file_get_contents($vendorDir.'/composer/autoload_psr4.php'));
         $this->assertEquals($expectedClassmap, file_get_contents($vendorDir.'/composer/autoload_classmap.php'));
         $this->assertContains("\n    \$vendorDir . '/b/b/bootstrap.php',\n", file_get_contents($vendorDir.'/composer/autoload_files.php'));
         $this->assertContains("\n    \$baseDir . '/test.php',\n", file_get_contents($vendorDir.'/composer/autoload_files.php'));
@@ -768,6 +833,7 @@ EOF;
         $package = new Package('a', '1.0', '1.0');
         $package->setAutoload(array(
             'psr-0' => array('Foo' => '../path/../src'),
+            'psr-4' => array('Acme\Foo\\' => '../path/../src-psr4'),
             'classmap' => array('../classmap'),
             'files' => array('../test.php'),
         ));
@@ -798,7 +864,21 @@ return array(
 
 EOF;
 
-    $expectedClassmap = <<<'EOF'
+        $expectedPsr4 = <<<'EOF'
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir).'/working-dir';
+
+return array(
+    'Acme\\Foo\\' => array($baseDir . '/../src-psr4'),
+);
+
+EOF;
+
+        $expectedClassmap = <<<'EOF'
 <?php
 
 // autoload_classmap.php @generated by Composer
@@ -814,6 +894,7 @@ return array(
 EOF;
 
         $this->assertEquals($expectedNamespace, file_get_contents($this->vendorDir.'/composer/autoload_namespaces.php'));
+        $this->assertEquals($expectedPsr4, file_get_contents($this->vendorDir.'/composer/autoload_psr4.php'));
         $this->assertEquals($expectedClassmap, file_get_contents($this->vendorDir.'/composer/autoload_classmap.php'));
         $this->assertContains("\n    \$baseDir . '/../test.php',\n", file_get_contents($this->vendorDir.'/composer/autoload_files.php'));
     }
@@ -823,6 +904,7 @@ EOF;
         $package = new Package('a', '1.0', '1.0');
         $package->setAutoload(array(
             'psr-0' => array('Foo' => ''),
+            'psr-4' => array('Acme\Foo\\' => ''),
             'classmap' => array(''),
         ));
 
@@ -850,7 +932,21 @@ return array(
 
 EOF;
 
-    $expectedClassmap = <<<'EOF'
+        $expectedPsr4 = <<<'EOF'
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+    'Acme\\Foo\\' => array($baseDir . '/'),
+);
+
+EOF;
+
+        $expectedClassmap = <<<'EOF'
 <?php
 
 // autoload_classmap.php @generated by Composer
@@ -866,6 +962,7 @@ return array(
 EOF;
 
         $this->assertEquals($expectedNamespace, file_get_contents($this->vendorDir.'/composer/autoload_namespaces.php'));
+        $this->assertEquals($expectedPsr4, file_get_contents($this->vendorDir.'/composer/autoload_psr4.php'));
         $this->assertEquals($expectedClassmap, file_get_contents($this->vendorDir.'/composer/autoload_classmap.php'));
     }
 
@@ -874,6 +971,7 @@ EOF;
         $package = new Package('a', '1.0', '1.0');
         $package->setAutoload(array(
             'psr-0' => array('Foo' => 'composer-test-autoload-src/src'),
+            'psr-4' => array('Acme\Foo\\' => 'composer-test-autoload-src/src-psr4'),
         ));
 
         $this->repository->expects($this->once())
@@ -894,10 +992,25 @@ return array(
     'Foo' => array($baseDir . '/composer-test-autoload-src/src'),
 );
 
+EOF;
+
+        $expectedPsr4 = <<<'EOF'
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+    'Acme\\Foo\\' => array($baseDir . '/composer-test-autoload-src/src-psr4'),
+);
+
 EOF;
 
         $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, 'VendorSubstring');
         $this->assertEquals($expectedNamespace, file_get_contents($this->vendorDir.'/composer/autoload_namespaces.php'));
+        $this->assertEquals($expectedPsr4, file_get_contents($this->vendorDir.'/composer/autoload_psr4.php'));
     }
 
     private function assertAutoloadFiles($name, $dir, $type = 'namespaces')

+ 65 - 0
tests/Composer/Test/Autoload/ClassLoaderTest.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace Composer\Test\Autoload;
+
+use Composer\Autoload\ClassLoader;
+
+/**
+ * Tests the Composer\Autoload\ClassLoader class.
+ */
+class ClassLoaderTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * Tests regular PSR-0 and PSR-4 class loading.
+     *
+     * @dataProvider getLoadClassTests
+     *
+     * @param string $class
+     *   The fully-qualified class name to test,
+     *   without preceding namespace separator.
+     * @param bool $prependSeparator
+     *   Whether to call ->loadClass() with a class name with preceding
+     *   namespace separator, as it happens in PHP 5.3.0 - 5.3.2.
+     *   See https://bugs.php.net/50731
+     */
+    public function testLoadClass($class, $prependSeparator = FALSE)
+    {
+        $loader = new ClassLoader();
+        $loader->add('Namespaced\\', __DIR__ . '/Fixtures');
+        $loader->add('Pearlike_', __DIR__ . '/Fixtures');
+        $loader->addPsr4('ShinyVendor\\ShinyPackage\\', __DIR__ . '/Fixtures');
+
+        if ($prependSeparator) {
+          $prepend = '\\';
+          $message = "->loadClass() loads '$class'.";
+        }
+        else {
+          $prepend = '';
+          $message = "->loadClass() loads '\\$class', as required in PHP 5.3.0 - 5.3.2.";
+        }
+
+        $loader->loadClass($prepend . $class);
+        $this->assertTrue(class_exists($class, false), $message);
+    }
+
+    /**
+     * Provides arguments for ->testLoadClass().
+     *
+     * @return array
+     *   Array of parameter sets to test with.
+     */
+    public function getLoadClassTests()
+    {
+        return array(
+            array('Namespaced\\Foo'),
+            array('Pearlike_Foo'),
+            array('ShinyVendor\\ShinyPackage\\SubNamespace\\Foo'),
+            // "Bar" would not work here, since it is defined in a ".inc" file,
+            // instead of a ".php" file. So, use "Baz" instead.
+            array('Namespaced\\Baz', '\\'),
+            array('Pearlike_Bar', '\\'),
+            array('ShinyVendor\\ShinyPackage\\SubNamespace\\Bar', '\\'),
+        );
+    }
+
+}

+ 5 - 0
tests/Composer/Test/Autoload/Fixtures/SubNamespace/Bar.php

@@ -0,0 +1,5 @@
+<?php
+
+namespace ShinyVendor\ShinyPackage\SubNamespace;
+
+class Bar {}

+ 5 - 0
tests/Composer/Test/Autoload/Fixtures/SubNamespace/Foo.php

@@ -0,0 +1,5 @@
+<?php
+
+namespace ShinyVendor\ShinyPackage\SubNamespace;
+
+class Foo {}

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

@@ -0,0 +1,11 @@
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+    'Acme\\Fruit\\' => array($baseDir . '/src-fruit'),
+    'Acme\\Cake\\' => array($baseDir . '/src-cake', $baseDir . '/lib-cake'),
+);

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

@@ -0,0 +1,11 @@
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname(dirname($vendorDir));
+
+return array(
+    'Acme\\Fruit\\' => array($baseDir . '/src-fruit'),
+    'Acme\\Cake\\' => array($baseDir . '/src-cake', $baseDir . '/lib-cake'),
+);

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

@@ -0,0 +1,11 @@
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = $vendorDir;
+
+return array(
+    'Acme\\Fruit\\' => array($vendorDir . '/src-fruit'),
+    'Acme\\Cake\\' => array($vendorDir . '/src-cake', $vendorDir . '/lib-cake'),
+);

+ 5 - 0
tests/Composer/Test/Autoload/Fixtures/autoload_real_files_by_dependency.php

@@ -31,6 +31,11 @@ class ComposerAutoloaderInitFilesAutoloadOrder
             $loader->set($namespace, $path);
         }
 
+        $map = require __DIR__ . '/autoload_psr4.php';
+        foreach ($map as $namespace => $path) {
+            $loader->setPsr4($namespace, $path);
+        }
+
         $classMap = require __DIR__ . '/autoload_classmap.php';
         if ($classMap) {
             $loader->addClassMap($classMap);

+ 5 - 0
tests/Composer/Test/Autoload/Fixtures/autoload_real_functions.php

@@ -31,6 +31,11 @@ class ComposerAutoloaderInitFilesAutoload
             $loader->set($namespace, $path);
         }
 
+        $map = require __DIR__ . '/autoload_psr4.php';
+        foreach ($map as $namespace => $path) {
+            $loader->setPsr4($namespace, $path);
+        }
+
         $classMap = require __DIR__ . '/autoload_classmap.php';
         if ($classMap) {
             $loader->addClassMap($classMap);

+ 5 - 0
tests/Composer/Test/Autoload/Fixtures/autoload_real_include_path.php

@@ -31,6 +31,11 @@ class ComposerAutoloaderInitIncludePath
             $loader->set($namespace, $path);
         }
 
+        $map = require __DIR__ . '/autoload_psr4.php';
+        foreach ($map as $namespace => $path) {
+            $loader->setPsr4($namespace, $path);
+        }
+
         $classMap = require __DIR__ . '/autoload_classmap.php';
         if ($classMap) {
             $loader->addClassMap($classMap);

+ 5 - 0
tests/Composer/Test/Autoload/Fixtures/autoload_real_target_dir.php

@@ -31,6 +31,11 @@ class ComposerAutoloaderInitTargetDir
             $loader->set($namespace, $path);
         }
 
+        $map = require __DIR__ . '/autoload_psr4.php';
+        foreach ($map as $namespace => $path) {
+            $loader->setPsr4($namespace, $path);
+        }
+
         $classMap = require __DIR__ . '/autoload_classmap.php';
         if ($classMap) {
             $loader->addClassMap($classMap);

+ 1 - 1
tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php

@@ -253,7 +253,7 @@ class ValidatingArrayLoaderTest extends \PHPUnit_Framework_TestCase
                     ),
                 ),
                 array(
-                    'autoload : invalid value (psr0), must be one of psr-0, classmap, files'
+                    'autoload : invalid value (psr0), must be one of psr-0, psr-4, classmap, files'
                 )
             ),
         );