Skip to content

[HttpKernel] Allow bundles to declare classes and annotated classes to compile using patterns #19205

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 30, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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;
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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]);
}
}