Skip to content
This repository has been archived by the owner on Dec 5, 2022. It is now read-only.

Commit

Permalink
[ClassLoader] made ClassCollectionLoader::load() automatically includ…
Browse files Browse the repository at this point in the history
…e class dependencies
  • Loading branch information
fabpot committed Jul 4, 2012
1 parent ddc8887 commit 685dd12
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 62 deletions.
95 changes: 63 additions & 32 deletions ClassCollectionLoader.php
Expand Up @@ -19,7 +19,7 @@
class ClassCollectionLoader class ClassCollectionLoader
{ {
static private $loaded; static private $loaded;
static private $baseClassesCountMap; static private $seen;


/** /**
* Loads a list of classes and caches them in one big file. * Loads a list of classes and caches them in one big file.
Expand Down Expand Up @@ -55,6 +55,8 @@ static public function load($classes, $cacheDir, $name, $autoReload, $adaptive =
$name = $name.'-'.substr(md5(implode('|', $classes)), 0, 5); $name = $name.'-'.substr(md5(implode('|', $classes)), 0, 5);
} }


$classes = array_unique($classes);

$cache = $cacheDir.'/'.$name.$extension; $cache = $cacheDir.'/'.$name.$extension;


// auto-reload // auto-reload
Expand Down Expand Up @@ -90,23 +92,19 @@ static public function load($classes, $cacheDir, $name, $autoReload, $adaptive =
return; return;
} }


// order classes to avoid redeclaration at runtime (class declared before its parent)
self::orderClasses($classes);

$files = array(); $files = array();
$content = ''; $content = '';
foreach ($classes as $class) { foreach (self::getOrderedClasses($classes) as $class) {
if (!class_exists($class) && !interface_exists($class) && (!function_exists('trait_exists') || !trait_exists($class))) { if (in_array($class->getName(), $declared)) {
throw new \InvalidArgumentException(sprintf('Unable to load class "%s"', $class)); continue;
} }


$r = new \ReflectionClass($class); $files[] = $class->getFileName();
$files[] = $r->getFileName();


$c = preg_replace(array('/^\s*<\?php/', '/\?>\s*$/'), '', file_get_contents($r->getFileName())); $c = preg_replace(array('/^\s*<\?php/', '/\?>\s*$/'), '', file_get_contents($class->getFileName()));


// add namespace declaration for global code // add namespace declaration for global code
if (!$r->inNamespace()) { if (!$class->inNamespace()) {
$c = "\nnamespace\n{\n".self::stripComments($c)."\n}\n"; $c = "\nnamespace\n{\n".self::stripComments($c)."\n}\n";
} else { } else {
$c = self::fixNamespaceDeclarations('<?php '.$c); $c = self::fixNamespaceDeclarations('<?php '.$c);
Expand Down Expand Up @@ -234,47 +232,80 @@ static private function stripComments($source)
} }


/** /**
* Orders a set of classes according to their number of parents. * Gets an ordered array of passed classes including all their dependencies.
* *
* @param array $classes * @param array $classes
* *
* @return array An array of sorted \ReflectionClass instances (dependencies added if needed)
*
* @throws \InvalidArgumentException When a class can't be loaded * @throws \InvalidArgumentException When a class can't be loaded
*/ */
static private function orderClasses(array &$classes) static private function getOrderedClasses(array $classes)
{ {
$map = array();
self::$seen = array();
foreach ($classes as $class) { foreach ($classes as $class) {
if (isset(self::$baseClassesCountMap[$class])) {
continue;
}

try { try {
$reflectionClass = new \ReflectionClass($class); $reflectionClass = new \ReflectionClass($class);
} catch (\ReflectionException $e) { } catch (\ReflectionException $e) {
throw new \InvalidArgumentException(sprintf('Unable to load class "%s"', $class)); 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 $map = array_merge($map, self::getClassHierarchy($reflectionClass));
self::$baseClassesCountMap[$class] = self::countParentClasses($reflectionClass) + count($reflectionClass->getInterfaces()); }

return $map;
}

static private function getClassHierarchy(\ReflectionClass $class)
{
if (isset(self::$seen[$class->getName()])) {
return array();
}

self::$seen[$class->getName()] = true;

$classes = array($class);
$parent = $class;
while (($parent = $parent->getParentClass()) && $parent->isUserDefined() && !isset(self::$seen[$parent->getName()])) {
self::$seen[$parent->getName()] = true;

array_unshift($classes, $parent);
}

if (function_exists('get_declared_traits')) {
foreach ($classes as $c) {
foreach (self::getTraits($c) as $trait) {
self::$seen[$trait->getName()] = true;

array_unshift($classes, $trait);
}
}
} }


asort(self::$baseClassesCountMap); foreach ($class->getInterfaces() as $interface) {
if ($interface->isUserDefined() && !isset(self::$seen[$interface->getName()])) {
self::$seen[$interface->getName()] = true;


$classes = array_intersect(array_keys(self::$baseClassesCountMap), $classes); array_unshift($classes, $interface);
}
}

return $classes;
} }


/** static private function getTraits(\ReflectionClass $class)
* 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()) { $traits = $class->getTraits();
$count = self::countParentClasses($parent, ++$count); $classes = array();
while ($trait = array_pop($traits)) {
if ($trait->isUserDefined() && !isset(self::$seen[$trait->getName()])) {
$classes[] = $trait;

$traits = array_merge($traits, $trait->getTraits());
}
} }


return $count; return $classes;
} }
} }
93 changes: 63 additions & 30 deletions Tests/ClassCollectionLoaderTest.php
Expand Up @@ -12,7 +12,6 @@
namespace Symfony\Component\ClassLoader\Tests; namespace Symfony\Component\ClassLoader\Tests;


use Symfony\Component\ClassLoader\ClassCollectionLoader; use Symfony\Component\ClassLoader\ClassCollectionLoader;
use Symfony\Component\ClassLoader\UniversalClassLoader;


require_once __DIR__.'/Fixtures/ClassesWithParents/CInterface.php'; require_once __DIR__.'/Fixtures/ClassesWithParents/CInterface.php';
require_once __DIR__.'/Fixtures/ClassesWithParents/B.php'; require_once __DIR__.'/Fixtures/ClassesWithParents/B.php';
Expand All @@ -25,38 +24,19 @@ class ClassCollectionLoaderTest extends \PHPUnit_Framework_TestCase
*/ */
public function testClassReordering(array $classes) public function testClassReordering(array $classes)
{ {
$expected = <<<EOF $expected = array(
<?php 'ClassesWithParents\\CInterface',
'ClassesWithParents\\B',
namespace ClassesWithParents 'ClassesWithParents\\A',
{ );
interface CInterface {}
}
namespace ClassesWithParents
{
class B implements CInterface {}
}
namespace ClassesWithParents
{
class A extends B {}
}
EOF;


$dir = sys_get_temp_dir(); $r = new \ReflectionClass('Symfony\Component\ClassLoader\ClassCollectionLoader');
$fileName = uniqid('symfony_'); $m = $r->getMethod('getOrderedClasses');
$m->setAccessible(true);


ClassCollectionLoader::load($classes, $dir, $fileName, true); $ordered = $m->invoke('Symfony\Component\ClassLoader\ClassCollectionLoader', $classes);
$cachedContent = @file_get_contents($dir.'/'.$fileName.'.php');


$this->assertEquals($expected, $cachedContent); $this->assertEquals($expected, array_map(function ($class) { return $class->getName(); }, $ordered));
} }


public function getDifferentOrders() public function getDifferentOrders()
Expand All @@ -77,6 +57,59 @@ public function getDifferentOrders()
'ClassesWithParents\\B', 'ClassesWithParents\\B',
'ClassesWithParents\\A', 'ClassesWithParents\\A',
)), )),
array(array(
'ClassesWithParents\\A',
)),
);
}

/**
* @dataProvider getDifferentOrdersForTraits
*/
public function testClassWithTraitsReordering(array $classes)
{
if (version_compare(phpversion(), '5.4.0', '<')) {
$this->markTestSkipped('Requires PHP > 5.4.0.');

return;
}

require_once __DIR__.'/Fixtures/ClassesWithParents/ATrait.php';
require_once __DIR__.'/Fixtures/ClassesWithParents/BTrait.php';
require_once __DIR__.'/Fixtures/ClassesWithParents/CTrait.php';
require_once __DIR__.'/Fixtures/ClassesWithParents/D.php';
require_once __DIR__.'/Fixtures/ClassesWithParents/E.php';

$expected = array(
'ClassesWithParents\\CInterface',
'ClassesWithParents\\CTrait',
'ClassesWithParents\\ATrait',
'ClassesWithParents\\BTrait',
'ClassesWithParents\\B',
'ClassesWithParents\\A',
'ClassesWithParents\\D',
'ClassesWithParents\\E',
);

$r = new \ReflectionClass('Symfony\Component\ClassLoader\ClassCollectionLoader');
$m = $r->getMethod('getOrderedClasses');
$m->setAccessible(true);

$ordered = $m->invoke('Symfony\Component\ClassLoader\ClassCollectionLoader', $classes);

$this->assertEquals($expected, array_map(function ($class) { return $class->getName(); }, $ordered));
}

public function getDifferentOrdersForTraits()
{
return array(
array(array(
'ClassesWithParents\\E',
'ClassesWithParents\\ATrait',
)),
array(array(
'ClassesWithParents\\E',
)),
); );
} }


Expand Down
7 changes: 7 additions & 0 deletions Tests/Fixtures/ClassesWithParents/ATrait.php
@@ -0,0 +1,7 @@
<?php

namespace ClassesWithParents;

trait ATrait
{
}
8 changes: 8 additions & 0 deletions Tests/Fixtures/ClassesWithParents/BTrait.php
@@ -0,0 +1,8 @@
<?php

namespace ClassesWithParents;

trait BTrait
{
use ATrait;
}
7 changes: 7 additions & 0 deletions Tests/Fixtures/ClassesWithParents/CTrait.php
@@ -0,0 +1,7 @@
<?php

namespace ClassesWithParents;

trait CTrait
{
}
8 changes: 8 additions & 0 deletions Tests/Fixtures/ClassesWithParents/D.php
@@ -0,0 +1,8 @@
<?php

namespace ClassesWithParents;

class D extends A
{
use BTrait;
}
8 changes: 8 additions & 0 deletions Tests/Fixtures/ClassesWithParents/E.php
@@ -0,0 +1,8 @@
<?php

namespace ClassesWithParents;

class E extends D
{
use CTrait;
}

0 comments on commit 685dd12

Please sign in to comment.