Skip to content

Commit

Permalink
[HttpKernel] Allow usage of patterns in classes and annotations to cache
Browse files Browse the repository at this point in the history
  • Loading branch information
tgalopin committed Jul 28, 2016
1 parent 194dcf3 commit 1be7424
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 4 deletions.
Expand Up @@ -169,6 +169,14 @@ public function load(array $configs, ContainerBuilder $container)
$definition->replaceArgument(1, null);
}

$this->addAnnotatedClassesToCompile(array(
'**Bundle\\Controller\\',
'**Bundle\\Entity\\',

// Added explicitly so that we don't rely on the class map being dumped to make it work
'Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller',
));

$this->addClassesToCompile(array(
'Symfony\\Component\\Config\\ConfigCache',
'Symfony\\Component\\Config\\FileLocator',
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/FrameworkBundle/composer.json
Expand Up @@ -24,7 +24,7 @@
"symfony/config": "~2.8|~3.0",
"symfony/event-dispatcher": "~2.8|~3.0",
"symfony/http-foundation": "~3.1",
"symfony/http-kernel": "~3.1.2|~3.2",
"symfony/http-kernel": "~3.2",
"symfony/polyfill-mbstring": "~1.0",
"symfony/filesystem": "~2.8|~3.0",
"symfony/finder": "~2.8|~3.0",
Expand Down
Expand Up @@ -11,6 +11,8 @@

namespace Symfony\Component\HttpKernel\DependencyInjection;

use Composer\Autoload\ClassLoader;
use Symfony\Component\Debug\DebugClassLoader;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\HttpKernel\Kernel;
Expand All @@ -35,12 +37,113 @@ public function __construct(Kernel $kernel)
public function process(ContainerBuilder $container)
{
$classes = array();
$annotatedClasses = array();
foreach ($container->getExtensions() as $extension) {
if ($extension instanceof Extension) {
$classes = array_merge($classes, $extension->getClassesToCompile());
$annotatedClasses = array_merge($annotatedClasses, $extension->getAnnotatedClassesToCompile());
}
}

$this->kernel->setClassCache(array_unique($container->getParameterBag()->resolveValue($classes)));
$classes = $container->getParameterBag()->resolveValue($classes);
$annotatedClasses = $container->getParameterBag()->resolveValue($annotatedClasses);
$existingClasses = $this->getClassesInComposerClassMaps();

$this->kernel->setClassCache($this->expandClasses($classes, $existingClasses));
$this->kernel->setAnnotatedClassCache($this->expandClasses($annotatedClasses, $existingClasses));
}

/**
* Expands the given class patterns using a list of existing classes.
*
* @param array $patterns The class patterns to expand
* @param array $classes The existing classes to match against the patterns
*
* @return array A list of classes derivated from the patterns
*/
private function expandClasses(array $patterns, array $classes)
{
$expanded = array();

// Explicit classes declared in the patterns are returned directly
foreach ($patterns as $key => $pattern) {
if (substr($pattern, -1) !== '\\' && false === strpos($pattern, '*')) {
unset($patterns[$key]);
$expanded[] = ltrim($pattern, '\\');
}
}

// Match patterns with the classes list
$regexps = $this->patternsToRegexps($patterns);

foreach ($classes as $class) {
$class = ltrim($class, '\\');

if ($this->matchAnyRegexps($class, $regexps)) {
$expanded[] = $class;
}
}

return array_unique($expanded);
}

private function getClassesInComposerClassMaps()
{
$classes = array();

foreach (spl_autoload_functions() as $function) {
if (!is_array($function)) {
continue;
}

if ($function[0] instanceof DebugClassLoader) {
$function = $function[0]->getClassLoader();
}

if (is_array($function) && $function[0] instanceof ClassLoader) {
$classes += $function[0]->getClassMap();
}
}

return array_keys($classes);
}

private function patternsToRegexps($patterns)
{
$regexps = array();

foreach ($patterns as $pattern) {
// Escape user input
$regex = preg_quote(ltrim($pattern, '\\'));

// Wildcards * and **
$regex = strtr($regex, array('\\*\\*' => '.*?', '\\*' => '[^\\\\]*?'));

// If this class does not end by a slash, anchor the end
if (substr($regex, -1) !== '\\') {
$regex .= '$';
}

$regexps[] = '{^\\\\'.$regex.'}';
}

return $regexps;
}

private function matchAnyRegexps($class, $regexps)
{
$blacklisted = false !== strpos($class, 'Test');

foreach ($regexps as $regex) {
if ($blacklisted && false === strpos($regex, 'Test')) {
continue;
}

if (preg_match($regex, '\\'.$class)) {
return true;
}
}

return false;
}
}
Expand Up @@ -21,6 +21,7 @@
abstract class Extension extends BaseExtension
{
private $classes = array();
private $annotatedClasses = array();

/**
* Gets the classes to cache.
Expand All @@ -32,13 +33,33 @@ public function getClassesToCompile()
return $this->classes;
}

/**
* Gets the annotated classes to cache.
*
* @return array An array of classes
*/
public function getAnnotatedClassesToCompile()
{
return $this->annotatedClasses;
}

/**
* Adds classes to the class cache.
*
* @param array $classes An array of classes
* @param array $classes An array of class patterns
*/
public function addClassesToCompile(array $classes)
{
$this->classes = array_merge($this->classes, $classes);
}

/**
* Adds annotated classes to the class cache.
*
* @param array $annotatedClasses An array of class patterns
*/
public function addAnnotatedClassesToCompile(array $annotatedClasses)
{
$this->annotatedClasses = array_merge($this->annotatedClasses, $annotatedClasses);
}
}
10 changes: 9 additions & 1 deletion src/Symfony/Component/HttpKernel/Kernel.php
Expand Up @@ -329,13 +329,21 @@ public function loadClassCache($name = 'classes', $extension = '.php')
}

/**
* Used internally.
* @internal
*/
public function setClassCache(array $classes)
{
file_put_contents($this->getCacheDir().'/classes.map', sprintf('<?php return %s;', var_export($classes, true)));
}

/**
* @internal
*/
public function setAnnotatedClassCache(array $annotatedClasses)
{
file_put_contents($this->getCacheDir().'/annotations.map', sprintf('<?php return %s;', var_export($annotatedClasses, true)));
}

/**
* {@inheritdoc}
*/
Expand Down
@@ -0,0 +1,97 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\HttpKernel\Tests\DependencyInjection;

use Symfony\Component\HttpKernel\DependencyInjection\AddClassesToCachePass;

class AddClassesToCachePassTest extends \PHPUnit_Framework_TestCase
{
public function testExpandClasses()
{
$r = new \ReflectionClass(AddClassesToCachePass::class);
$pass = $r->newInstanceWithoutConstructor();
$r = new \ReflectionMethod(AddClassesToCachePass::class, 'expandClasses');
$expand = $r->getClosure($pass);

$this->assertSame('Foo', $expand(array('Foo'), array())[0]);
$this->assertSame('Foo', $expand(array('\\Foo'), array())[0]);
$this->assertSame('Foo', $expand(array('Foo'), array('\\Foo'))[0]);
$this->assertSame('Foo', $expand(array('Foo'), array('Foo'))[0]);
$this->assertSame('Foo', $expand(array('\\Foo'), array('\\Foo\\Bar'))[0]);
$this->assertSame('Foo', $expand(array('Foo'), array('\\Foo\\Bar'))[0]);
$this->assertSame('Foo', $expand(array('\\Foo'), array('\\Foo\\Bar\\Acme'))[0]);

$this->assertSame('Foo\\Bar', $expand(array('Foo\\'), array('\\Foo\\Bar'))[0]);
$this->assertSame('Foo\\Bar\\Acme', $expand(array('Foo\\'), array('\\Foo\\Bar\\Acme'))[0]);
$this->assertEmpty($expand(array('Foo\\'), array('\\Foo')));

$this->assertSame('Acme\\Foo\\Bar', $expand(array('**\\Foo\\'), array('\\Acme\\Foo\\Bar'))[0]);
$this->assertEmpty($expand(array('**\\Foo\\'), array('\\Foo\\Bar')));
$this->assertEmpty($expand(array('**\\Foo\\'), array('\\Acme\\Foo')));
$this->assertEmpty($expand(array('**\\Foo\\'), array('\\Foo')));

$this->assertSame('Acme\\Foo', $expand(array('**\\Foo'), array('\\Acme\\Foo'))[0]);
$this->assertEmpty($expand(array('**\\Foo'), array('\\Acme\\Foo\\AcmeBundle')));
$this->assertEmpty($expand(array('**\\Foo'), array('\\Acme\\FooBar\\AcmeBundle')));

$this->assertSame('Foo\\Acme\\Bar', $expand(array('Foo\\*\\Bar'), array('\\Foo\\Acme\\Bar'))[0]);
$this->assertEmpty($expand(array('Foo\\*\\Bar'), array('\\Foo\\Acme\\Bundle\\Bar')));

$this->assertSame('Foo\\Acme\\Bar', $expand(array('Foo\\**\\Bar'), array('\\Foo\\Acme\\Bar'))[0]);
$this->assertSame('Foo\\Acme\\Bundle\\Bar', $expand(array('Foo\\**\\Bar'), array('\\Foo\\Acme\\Bundle\\Bar'))[0]);

$this->assertSame('Acme\\Bar', $expand(array('*\\Bar'), array('\\Acme\\Bar'))[0]);
$this->assertEmpty($expand(array('*\\Bar'), array('\\Bar')));
$this->assertEmpty($expand(array('*\\Bar'), array('\\Foo\\Acme\\Bar')));

$this->assertSame('Foo\\Acme\\Bar', $expand(array('**\\Bar'), array('\\Foo\\Acme\\Bar'))[0]);
$this->assertSame('Foo\\Acme\\Bundle\\Bar', $expand(array('**\\Bar'), array('\\Foo\\Acme\\Bundle\\Bar'))[0]);
$this->assertEmpty($expand(array('**\\Bar'), array('\\Bar')));

$this->assertSame('Foo\\Bar', $expand(array('Foo\\*'), array('\\Foo\\Bar'))[0]);
$this->assertEmpty($expand(array('Foo\\*'), array('\\Foo\\Acme\\Bar')));

$this->assertSame('Foo\\Bar', $expand(array('Foo\\**'), array('\\Foo\\Bar'))[0]);
$this->assertSame('Foo\\Acme\\Bar', $expand(array('Foo\\**'), array('\\Foo\\Acme\\Bar'))[0]);

$this->assertSame(array('Foo\\Bar'), $expand(array('Foo\\*'), array('Foo\\Bar', 'Foo\\BarTest')));
$this->assertSame(array('Foo\\Bar', 'Foo\\BarTest'), $expand(array('Foo\\*', 'Foo\\*Test'), array('Foo\\Bar', 'Foo\\BarTest')));

$this->assertSame(
'Acme\\FooBundle\\Controller\\DefaultController',
$expand(array('**Bundle\\Controller\\'), array('\\Acme\\FooBundle\\Controller\\DefaultController'))[0]
);

$this->assertSame(
'FooBundle\\Controller\\DefaultController',
$expand(array('**Bundle\\Controller\\'), array('\\FooBundle\\Controller\\DefaultController'))[0]
);

$this->assertSame(
'Acme\\FooBundle\\Controller\\Bar\\DefaultController',
$expand(array('**Bundle\\Controller\\'), array('\\Acme\\FooBundle\\Controller\\Bar\\DefaultController'))[0]
);

$this->assertSame(
'Bundle\\Controller\\Bar\\DefaultController',
$expand(array('**Bundle\\Controller\\'), array('\\Bundle\\Controller\\Bar\\DefaultController'))[0]
);

$this->assertSame(
'Acme\\Bundle\\Controller\\Bar\\DefaultController',
$expand(array('**Bundle\\Controller\\'), array('\\Acme\\Bundle\\Controller\\Bar\\DefaultController'))[0]
);

$this->assertSame('Foo\\Bar', $expand(array('Foo\\Bar'), array())[0]);
$this->assertSame('Foo\\Acme\\Bar', $expand(array('Foo\\**'), array('\\Foo\\Acme\\Bar'))[0]);
}
}

0 comments on commit 1be7424

Please sign in to comment.