diff --git a/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php b/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php index 47588f76c4..7435e3fdf4 100644 --- a/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php +++ b/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php @@ -19,6 +19,7 @@ namespace Symfony\Component\ClassLoader; class ClassCollectionLoader { static private $loaded; + static private $baseClassesCountMap; /** * Loads a list of classes and caches them in one big file. @@ -61,6 +62,9 @@ class ClassCollectionLoader $time = filemtime($cache); $meta = unserialize(file_get_contents($metadata)); + sort($meta[1]); + sort($classes); + if ($meta[1] != $classes) { $reload = true; } else { @@ -81,6 +85,9 @@ class ClassCollectionLoader return; } + // order classes to avoid redeclaration at runtime (class declared before its parent) + self::orderClasses($classes); + $files = array(); $content = ''; foreach ($classes as $class) { @@ -220,4 +227,49 @@ class ClassCollectionLoader return $output; } + + /** + * Orders a set of classes according to their number of parents. + * + * @param array $classes + * + * @throws \InvalidArgumentException When a class can't be loaded + */ + static private function orderClasses(array &$classes) + { + foreach ($classes as $class) { + if (isset(self::$baseClassesCountMap[$class])) { + continue; + } + + try { + $reflectionClass = new \ReflectionClass($class); + } catch (\ReflectionException $e) { + throw new \InvalidArgumentException(sprintf('Unable to load class "%s"', $class)); + } + + // The counter is cached to avoid reflection if the same class is asked again later + self::$baseClassesCountMap[$class] = self::countParentClasses($reflectionClass) + count($reflectionClass->getInterfaces()); + } + + asort(self::$baseClassesCountMap); + + $classes = array_intersect(array_keys(self::$baseClassesCountMap), $classes); + } + + /** + * Counts the number of parent classes in userland. + * + * @param \ReflectionClass $class + * @param integer $count If exists, the current counter + * @return integer + */ + static private function countParentClasses(\ReflectionClass $class, $count = 0) + { + if (($parent = $class->getParentClass()) && $parent->isUserDefined()) { + $count = self::countParentClasses($parent, ++$count); + } + + return $count; + } } diff --git a/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php b/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php index 5f4db1f06e..fc76747177 100644 --- a/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php +++ b/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php @@ -12,9 +12,74 @@ namespace Symfony\Component\ClassLoader\Tests; use Symfony\Component\ClassLoader\ClassCollectionLoader; +use Symfony\Component\ClassLoader\UniversalClassLoader; class ClassCollectionLoaderTest extends \PHPUnit_Framework_TestCase { + /** + * @dataProvider getDifferentOrders + */ + public function testClassReordering(array $classes) + { + $loader = new UniversalClassLoader(); + $loader->registerNamespace('ClassesWithParents', __DIR__.'/Fixtures'); + $loader->register(); + + $expected = <<assertEquals($expected, $cachedContent); + } + + public function getDifferentOrders() + { + return array( + array(array( + 'ClassesWithParents\\A', + 'ClassesWithParents\\CInterface', + 'ClassesWithParents\\B', + )), + array(array( + 'ClassesWithParents\\B', + 'ClassesWithParents\\A', + 'ClassesWithParents\\CInterface', + )), + array(array( + 'ClassesWithParents\\CInterface', + 'ClassesWithParents\\B', + 'ClassesWithParents\\A', + )), + ); + } + public function testFixNamespaceDeclarations() { $source = <<