Skip to content

Commit

Permalink
merged branch vicb/classcollectionloader (PR #6258)
Browse files Browse the repository at this point in the history
This PR was submitted for the master branch but it was merged into the 2.2 branch instead (closes #6258).

Commits
-------

d4acd6d Classcollectionloader: fix traits + enhancements

Discussion
----------

Classcollectionloader: fix traits + enhancements

Commits:

- some tweaks
- generates smaller cache files (20% decrease for the SE)
- fix traits dependency - thanks php for consistency https://bugs.php.net/bug.php?id=61554

---------------------------------------------------------------------------

by vicb at 2012-12-11T07:56:07Z

@stloyd @fabpot thanks for the useful reviews, everything should be fixed now.

---------------------------------------------------------------------------

by vicb at 2012-12-11T10:03:48Z

Should be ready.

---------------------------------------------------------------------------

by vicb at 2013-01-15T21:05:42Z

@fabpot I initially marked this PR for 2.3 but it contains a fix for traits, should you merge it ?

---------------------------------------------------------------------------

by vicb at 2013-01-23T20:09:32Z

@fabpot could this be in 2.2 ?

---------------------------------------------------------------------------

by vicb at 2013-02-01T14:07:08Z

@fabpot fixed
  • Loading branch information
fabpot committed Feb 1, 2013
2 parents d8bceab + 507d0c4 commit c85ec8b
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 50 deletions.
119 changes: 91 additions & 28 deletions src/Symfony/Component/ClassLoader/ClassCollectionLoader.php
Expand Up @@ -144,45 +144,78 @@ public static function fixNamespaceDeclarations($source)
return $source;
}

$rawChunk = '';
$output = '';
$inNamespace = false;
$tokens = token_get_all($source);

for ($i = 0, $max = count($tokens); $i < $max; $i++) {
$token = $tokens[$i];
for (reset($tokens); false !== $token = current($tokens); next($tokens)) {
if (is_string($token)) {
$output .= $token;
$rawChunk .= $token;
} elseif (in_array($token[0], array(T_COMMENT, T_DOC_COMMENT))) {
// strip comments
continue;
} elseif (T_NAMESPACE === $token[0]) {
if ($inNamespace) {
$output .= "}\n";
$rawChunk .= "}\n";
}
$output .= $token[1];
$rawChunk .= $token[1];

// namespace name and whitespaces
while (($t = $tokens[++$i]) && is_array($t) && in_array($t[0], array(T_WHITESPACE, T_NS_SEPARATOR, T_STRING))) {
$output .= $t[1];
while (($t = next($tokens)) && is_array($t) && in_array($t[0], array(T_WHITESPACE, T_NS_SEPARATOR, T_STRING))) {
$rawChunk .= $t[1];
}
if (is_string($t) && '{' === $t) {
if ('{' === $t) {
$inNamespace = false;
--$i;
prev($tokens);
} else {
$output = rtrim($output);
$output .= "\n{";
$rawChunk = rtrim($rawChunk) . "\n{";
$inNamespace = true;
}
} elseif (T_START_HEREDOC === $token[0]) {
$output .= self::compressCode($rawChunk) . $token[1];
do {
$token = next($tokens);
$output .= $token[1];
} while ($token[0] !== T_END_HEREDOC);
$rawChunk = '';
} elseif (T_CONSTANT_ENCAPSED_STRING === $token[0]) {
$output .= self::compressCode($rawChunk) . $token[1];
$rawChunk = '';
} else {
$output .= $token[1];
$rawChunk .= $token[1];
}
}

if ($inNamespace) {
$output .= "}\n";
$rawChunk .= "}\n";
}

return $output;
return $output . self::compressCode($rawChunk);
}

/**
* This method is only useful for testing.
*/
public static function enableTokenizer($bool)
{
self::$useTokenizer = (Boolean) $bool;
}

/**
* Strips leading & trailing ws, multiple EOL, multiple ws.
*
* @param string $code Original PHP code
*
* @return string compressed code
*/
private static function compressCode($code)
{
return preg_replace(
array('/^\s+/m', '/\s+$/m', '/([\n\r]+ *[\n\r]+)+/', '/[ \t]+/'),
array('', '', "\n", ' '),
$code
);
}

/**
Expand Down Expand Up @@ -247,17 +280,19 @@ private static function getClassHierarchy(\ReflectionClass $class)
array_unshift($classes, $parent);
}

$traits = array();

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);
foreach (self::resolveDependencies(self::computeTraitDeps($c), $c) as $trait) {
if ($trait !== $c) {
$traits[] = $trait;
}
}
}
}

return array_merge(self::getInterfaces($class), $classes);
return array_merge(self::getInterfaces($class), $traits, $classes);
}

private static function getInterfaces(\ReflectionClass $class)
Expand All @@ -277,26 +312,54 @@ private static function getInterfaces(\ReflectionClass $class)
return $classes;
}

private static function getTraits(\ReflectionClass $class)
private static function computeTraitDeps(\ReflectionClass $class)
{
$traits = $class->getTraits();
$classes = array();
$deps = array($class->getName() => $traits);
while ($trait = array_pop($traits)) {
if ($trait->isUserDefined() && !isset(self::$seen[$trait->getName()])) {
$classes[] = $trait;

$traits = array_merge($traits, $trait->getTraits());
self::$seen[$trait->getName()] = true;
$traitDeps = $trait->getTraits();
$deps[$trait->getName()] = $traitDeps;
$traits = array_merge($traits, $traitDeps);
}
}

return $classes;
return $deps;
}

/**
* This method is only useful for testing.
* Dependencies resolution.
*
* This function does not check for circular dependencies as it should never
* occur with PHP traits.
*
* @param array $tree The dependency tree
* @param \ReflectionClass $node The node
* @param \ArrayObject $resolved An array of already resolved dependencies
* @param \ArrayObject $unresolved An array of dependencies to be resolved
*
* @return \ArrayObject The dependencies for the given node
*
* @throws \RuntimeException if a circular dependency is detected
*/
public static function enableTokenizer($bool)
private static function resolveDependencies(array $tree, $node, \ArrayObject $resolved = null, \ArrayObject $unresolved = null)
{
self::$useTokenizer = (Boolean) $bool;
if (null === $resolved) {
$resolved = new \ArrayObject();
}
if (null === $unresolved) {
$unresolved = new \ArrayObject();
}
$nodeName = $node->getName();
$unresolved[$nodeName] = $node;
foreach ($tree[$nodeName] as $dependency) {
if (!$resolved->offsetExists($dependency->getName())) {
self::resolveDependencies($tree, $dependency, $resolved, $unresolved);
}
}
$resolved[$nodeName] = $node;
unset($unresolved[$nodeName]);
return $resolved;
}
}
Expand Up @@ -20,6 +20,35 @@

class ClassCollectionLoaderTest extends \PHPUnit_Framework_TestCase
{
public function testTraitDependencies()
{
if (version_compare(phpversion(), '5.4', '<')) {
$this->markTestSkipped('Requires PHP > 5.4');

return;
}

require_once __DIR__.'/Fixtures/deps/traits.php';

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

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

$this->assertEquals(
array('TD', 'TC', 'TB', 'TA', 'TZ', 'CTFoo'),
array_map(function ($class) { return $class->getName(); }, $ordered)
);

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

$this->assertEquals(
array('TD', 'TZ', 'TC', 'TB', 'TA', 'CTBar'),
array_map(function ($class) { return $class->getName(); }, $ordered)
);
}

/**
* @dataProvider getDifferentOrders
*/
Expand Down Expand Up @@ -71,8 +100,8 @@ public function getDifferentOrders()
*/
public function testClassWithTraitsReordering(array $classes)
{
if (version_compare(phpversion(), '5.4.0', '<')) {
$this->markTestSkipped('Requires PHP > 5.4.0.');
if (version_compare(phpversion(), '5.4', '<')) {
$this->markTestSkipped('Requires PHP > 5.4');

return;
}
Expand All @@ -86,9 +115,9 @@ public function testClassWithTraitsReordering(array $classes)
$expected = array(
'ClassesWithParents\\GInterface',
'ClassesWithParents\\CInterface',
'ClassesWithParents\\CTrait',
'ClassesWithParents\\ATrait',
'ClassesWithParents\\BTrait',
'ClassesWithParents\\CTrait',
'ClassesWithParents\\B',
'ClassesWithParents\\A',
'ClassesWithParents\\D',
Expand Down Expand Up @@ -125,8 +154,20 @@ public function testFixNamespaceDeclarations($source, $expected)
$this->assertEquals('<?php '.$expected, ClassCollectionLoader::fixNamespaceDeclarations('<?php '.$source));
}

public function getFixNamespaceDeclarationsData()
{
return array(
array("namespace;\nclass Foo {}\n", "namespace\n{\nclass Foo {}\n}"),
array("namespace Foo;\nclass Foo {}\n", "namespace Foo\n{\nclass Foo {}\n}"),
array("namespace Bar ;\nclass Foo {}\n", "namespace Bar\n{\nclass Foo {}\n}"),
array("namespace Foo\Bar;\nclass Foo {}\n", "namespace Foo\Bar\n{\nclass Foo {}\n}"),
array("namespace Foo\Bar\Bar\n{\nclass Foo {}\n}\n", "namespace Foo\Bar\Bar\n{\nclass Foo {}\n}"),
array("namespace\n{\nclass Foo {}\n}\n", "namespace\n{\nclass Foo {}\n}"),
);
}

/**
* @dataProvider getFixNamespaceDeclarationsData
* @dataProvider getFixNamespaceDeclarationsDataWithoutTokenizer
*/
public function testFixNamespaceDeclarationsWithoutTokenizer($source, $expected)
{
Expand All @@ -135,7 +176,7 @@ public function testFixNamespaceDeclarationsWithoutTokenizer($source, $expected)
ClassCollectionLoader::enableTokenizer(true);
}

public function getFixNamespaceDeclarationsData()
public function getFixNamespaceDeclarationsDataWithoutTokenizer()
{
return array(
array("namespace;\nclass Foo {}\n", "namespace\n{\nclass Foo {}\n}\n"),
Expand Down Expand Up @@ -168,41 +209,47 @@ public function testCommentStripping()
require_once __DIR__.'/Fixtures/'.str_replace(array('\\', '_'), '/', $class).'.php';
});

ClassCollectionLoader::load(array('Namespaced\\WithComments', 'Pearlike_WithComments'), sys_get_temp_dir(), 'bar', false);
ClassCollectionLoader::load(
array('Namespaced\\WithComments', 'Pearlike_WithComments'),
sys_get_temp_dir(),
'bar',
false
);

spl_autoload_unregister($r);

$this->assertEquals(<<<EOF
<?php
namespace Namespaced
{
class WithComments
{
public static \$loaded = true;
}
public static \$loaded = true;
}
namespace
{
\$string ='string shoult not be modified';
\$heredoc =<<<HD
Heredoc should not be modified
HD;
\$nowdoc =<<<'ND'
Nowdoc should not be modified
ND;
}
namespace
{
class Pearlike_WithComments
{
public static \$loaded = true;
public static \$loaded = true;
}
}
EOF
, file_get_contents($file));
, str_replace("<?php \n", '', file_get_contents($file)));

unlink($file);
}
Expand Down
Expand Up @@ -16,3 +16,22 @@ class WithComments
/** @Boolean */
public static $loaded = true;
}

$string = 'string shoult not be modified';


$heredoc = <<<HD
Heredoc should not be modified
HD;

$nowdoc = <<<'ND'
Nowdoc should not be modified
ND;
36 changes: 36 additions & 0 deletions src/Symfony/Component/ClassLoader/Tests/Fixtures/deps/traits.php
@@ -0,0 +1,36 @@
<?php

trait TD
{}

trait TZ
{
use TD;
}

trait TC
{
use TD;
}

trait TB
{
use TC;
}

trait TA
{
use TB;
}

class CTFoo
{
use TA;
use TZ;
}

class CTBar
{
use TZ;
use TA;
}

0 comments on commit c85ec8b

Please sign in to comment.