From 3d576f2cbfd0e48503c6dd4cf8367a8f4e7ddf07 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Mon, 10 Dec 2012 13:27:35 +0100 Subject: [PATCH 1/7] [ClassCollectionLoader] make faster (traits don't support if) --- src/Symfony/Component/ClassLoader/ClassCollectionLoader.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php b/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php index b0e7618418006..48d1974d424a3 100644 --- a/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php +++ b/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php @@ -247,17 +247,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); + array_unshift($traits, $trait); } } } - return array_merge(self::getInterfaces($class), $classes); + return array_merge(self::getInterfaces($class), $traits, $classes); } private static function getInterfaces(\ReflectionClass $class) From afea24455df5a1d6cc0d9b71dab54897a454be20 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Mon, 10 Dec 2012 16:30:40 +0100 Subject: [PATCH 2/7] [ClassCollectionLoader] Decrease generated cache file size 20% reduction for the Sf2 class cache --- .../ClassLoader/ClassCollectionLoader.php | 6 ++++++ .../Tests/ClassCollectionLoaderTest.php | 19 +++---------------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php b/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php index 48d1974d424a3..3f51545c8a949 100644 --- a/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php +++ b/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php @@ -196,6 +196,12 @@ public static function fixNamespaceDeclarations($source) private static function writeCacheFile($file, $content) { $tmpFile = tempnam(dirname($file), basename($file)); + // Strip leading & trailing ws, multiple EOL, multiple ws + $content = preg_replace( + array('/^\s+/m', '/\s+$/m', '/([\n\r]+ *[\n\r]+)+/', '/[ \t]+/'), + array('', '', "\n", ' '), + $content + ); if (false !== @file_put_contents($tmpFile, $content) && @rename($tmpFile, $file)) { @chmod($file, 0666 & ~umask()); diff --git a/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php b/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php index 95fafeae82a65..7f4b761aaaaef 100644 --- a/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php +++ b/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php @@ -173,34 +173,21 @@ public function testCommentStripping() spl_autoload_unregister($r); $this->assertEquals(<< Date: Mon, 10 Dec 2012 18:31:16 +0100 Subject: [PATCH 3/7] [ClassCollectionLoader] Implement dep resolution for traits --- .../ClassLoader/ClassCollectionLoader.php | 66 +++++++++++++++---- .../Tests/ClassCollectionLoaderTest.php | 38 ++++++++++- .../Tests/Fixtures/deps/traits.php | 36 ++++++++++ 3 files changed, 124 insertions(+), 16 deletions(-) create mode 100644 src/Symfony/Component/ClassLoader/Tests/Fixtures/deps/traits.php diff --git a/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php b/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php index 3f51545c8a949..e454dfc85547e 100644 --- a/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php +++ b/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php @@ -185,6 +185,14 @@ public static function fixNamespaceDeclarations($source) return $output; } + /** + * This method is only useful for testing. + */ + public static function enableTokenizer($bool) + { + self::$useTokenizer = (Boolean) $bool; + } + /** * Writes a cache file. * @@ -257,10 +265,10 @@ private static function getClassHierarchy(\ReflectionClass $class) if (function_exists('get_declared_traits')) { foreach ($classes as $c) { - foreach (self::getTraits($c) as $trait) { - self::$seen[$trait->getName()] = true; - - array_unshift($traits, $trait); + foreach (self::resolveDependencies(self::computeTraitDeps($c), $c) as $trait) { + if ($trait !== $c) { + $traits[] = $trait; + } } } } @@ -285,26 +293,58 @@ 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 + * + * @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())) { + if ($unresolved->offsetExists($dependency->getName())) { + throw new \RuntimeException(sprintf( + 'Circular dependency "%s" - "%s"', + $node->getName(), + $dependency->getName() + )); + } + self::resolveDependencies($tree, $dependency, $resolved, $unresolved); + } + } + $resolved[$nodeName] = $node; + unset($unresolved[$nodeName]); + return $resolved; } } diff --git a/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php b/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php index 7f4b761aaaaef..37bf262ab55b6 100644 --- a/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php +++ b/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php @@ -20,6 +20,38 @@ 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) + ); + } + array_map(function ($class) { return $class->getName(); }, $ordered) + ); + } + /** * @dataProvider getDifferentOrders */ @@ -71,8 +103,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; } @@ -86,9 +118,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', diff --git a/src/Symfony/Component/ClassLoader/Tests/Fixtures/deps/traits.php b/src/Symfony/Component/ClassLoader/Tests/Fixtures/deps/traits.php new file mode 100644 index 0000000000000..a5537ac92fa82 --- /dev/null +++ b/src/Symfony/Component/ClassLoader/Tests/Fixtures/deps/traits.php @@ -0,0 +1,36 @@ + Date: Tue, 11 Dec 2012 08:52:30 +0100 Subject: [PATCH 4/7] [ClassCollectionLoader] Implement feedback --- .../ClassLoader/ClassCollectionLoader.php | 62 +++++++++++++------ .../Tests/ClassCollectionLoaderTest.php | 52 ++++++++++++---- .../Fixtures/Namespaced/WithComments.php | 19 ++++++ 3 files changed, 101 insertions(+), 32 deletions(-) diff --git a/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php b/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php index e454dfc85547e..5b03dee979ddb 100644 --- a/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php +++ b/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php @@ -144,45 +144,57 @@ 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); } /** @@ -193,6 +205,22 @@ 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 + ); + } + /** * Writes a cache file. * @@ -204,12 +232,6 @@ public static function enableTokenizer($bool) private static function writeCacheFile($file, $content) { $tmpFile = tempnam(dirname($file), basename($file)); - // Strip leading & trailing ws, multiple EOL, multiple ws - $content = preg_replace( - array('/^\s+/m', '/\s+$/m', '/([\n\r]+ *[\n\r]+)+/', '/[ \t]+/'), - array('', '', "\n", ' '), - $content - ); if (false !== @file_put_contents($tmpFile, $content) && @rename($tmpFile, $file)) { @chmod($file, 0666 & ~umask()); diff --git a/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php b/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php index 37bf262ab55b6..1975334b427ff 100644 --- a/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php +++ b/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php @@ -48,9 +48,6 @@ public function testTraitDependencies() array_map(function ($class) { return $class->getName(); }, $ordered) ); } - array_map(function ($class) { return $class->getName(); }, $ordered) - ); - } /** * @dataProvider getDifferentOrders @@ -157,8 +154,20 @@ public function testFixNamespaceDeclarations($source, $expected) $this->assertEquals('assertEquals(<< Date: Tue, 11 Dec 2012 10:08:46 +0100 Subject: [PATCH 5/7] [ClassCollectionLoader] CS --- src/Symfony/Component/ClassLoader/ClassCollectionLoader.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php b/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php index 5b03dee979ddb..d20e357046737 100644 --- a/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php +++ b/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php @@ -150,7 +150,6 @@ public static function fixNamespaceDeclarations($source) $tokens = token_get_all($source); for (reset($tokens); false !== $token = current($tokens); next($tokens)) { - if (is_string($token)) { $rawChunk .= $token; } elseif (in_array($token[0], array(T_COMMENT, T_DOC_COMMENT))) { @@ -163,9 +162,7 @@ public static function fixNamespaceDeclarations($source) $rawChunk .= $token[1]; // namespace name and whitespaces - while (($t = next($tokens)) && - is_array($t) - && in_array($t[0], array(T_WHITESPACE, T_NS_SEPARATOR, T_STRING))) { + while (($t = next($tokens)) && is_array($t) && in_array($t[0], array(T_WHITESPACE, T_NS_SEPARATOR, T_STRING))) { $rawChunk .= $t[1]; } if ('{' === $t) { From 804b7d1ba4a8ae98629148f686a019ef136591be Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Tue, 11 Dec 2012 11:02:43 +0100 Subject: [PATCH 6/7] [ClassCollectionLoader] Remove CD check --- .../Component/ClassLoader/ClassCollectionLoader.php | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php b/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php index d20e357046737..1de202a5beab4 100644 --- a/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php +++ b/src/Symfony/Component/ClassLoader/ClassCollectionLoader.php @@ -329,7 +329,10 @@ private static function computeTraitDeps(\ReflectionClass $class) } /** - * Dependencies resolution + * 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 @@ -352,13 +355,6 @@ private static function resolveDependencies(array $tree, $node, \ArrayObject $re $unresolved[$nodeName] = $node; foreach ($tree[$nodeName] as $dependency) { if (!$resolved->offsetExists($dependency->getName())) { - if ($unresolved->offsetExists($dependency->getName())) { - throw new \RuntimeException(sprintf( - 'Circular dependency "%s" - "%s"', - $node->getName(), - $dependency->getName() - )); - } self::resolveDependencies($tree, $dependency, $resolved, $unresolved); } } From e450726f6a5f1d16f571ffb1601573b13709bccf Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 1 Feb 2013 15:06:19 +0100 Subject: [PATCH 7/7] implement feedback --- .../ClassLoader/Tests/ClassCollectionLoaderTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php b/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php index 1975334b427ff..73bdc46d700d5 100644 --- a/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php +++ b/src/Symfony/Component/ClassLoader/Tests/ClassCollectionLoaderTest.php @@ -176,17 +176,17 @@ public function testFixNamespaceDeclarationsWithoutTokenizer($source, $expected) ClassCollectionLoader::enableTokenizer(true); } - public function getFixNamespaceDeclarationsDataWithoutTokenizer() - { - return array( + public function getFixNamespaceDeclarationsDataWithoutTokenizer() + { + return array( array("namespace;\nclass Foo {}\n", "namespace\n{\nclass Foo {}\n}\n"), array("namespace Foo;\nclass Foo {}\n", "namespace Foo\n{\nclass Foo {}\n}\n"), array("namespace Bar ;\nclass Foo {}\n", "namespace Bar\n{\nclass Foo {}\n}\n"), array("namespace Foo\Bar;\nclass Foo {}\n", "namespace Foo\Bar\n{\nclass Foo {}\n}\n"), array("namespace Foo\Bar\Bar\n{\nclass Foo {}\n}\n", "namespace Foo\Bar\Bar\n{\nclass Foo {}\n}\n"), array("namespace\n{\nclass Foo {}\n}\n", "namespace\n{\nclass Foo {}\n}\n"), - ); - } + ); + } /** * @expectedException InvalidArgumentException