Skip to content

added a Twig runtime loader #20092

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
Sep 29, 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
43 changes: 43 additions & 0 deletions src/Symfony/Bundle/TwigBundle/ContainerAwareRuntimeLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?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\Bundle\TwigBundle;

use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Loads Twig extension runtimes via the service container.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ContainerAwareRuntimeLoader implements \Twig_RuntimeLoaderInterface
{
private $container;
private $mapping;

public function __construct(ContainerInterface $container, array $mapping)
{
$this->container = $container;
$this->mapping = $mapping;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Injecting a container but not actually assigning it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed, added some tests as well :)


/**
* {@inheritdoc}
*/
public function load($class)
{
if (!isset($this->mapping[$class])) {
throw new \LogicException(sprintf('Class "%s" is not configured as a Twig runtime. Add the "twig.runtime" tag to the related service in the container.', $class));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't it return null instead, to play well with multiple runtime loaders, as supported by Twig ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that but I don't think people will register some other runtime loaders when using Symfony. And returning null here means that debugging an issue would be very difficult.

The other possibility would be for Twig to catch exceptions and keep it for when no loaders work.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other possibility is to inject a logger and log such errors. WDYT?

I don't like catching the exceptions in Twig as we try to avoid that as much as possible (mainly for performance reasons).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with logging

}

return $this->container->get($this->mapping[$class]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?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\Bundle\TwigBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;

/**
* Registers Twig runtime services.
*/
class RuntimeLoaderPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('twig.runtime_loader')) {
return;
}

$definition = $container->getDefinition('twig.runtime_loader');
$mapping = array();
foreach ($container->findTaggedServiceIds('twig.runtime') as $id => $attributes) {
$mapping[$container->getDefinition($id)->getClass()] = $id;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should validate that services are public (and not abstract), to provide an early error message for such mistake

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see #20112

}

$definition->replaceArgument(1, $mapping);
}
}
9 changes: 9 additions & 0 deletions src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
<argument>app</argument>
<argument type="service" id="twig.app_variable" />
</call>
<call method="addRuntimeLoader">
<argument type="service" id="twig.runtime_loader" />
</call>
<configurator service="twig.configurator.environment" method="configure" />
</service>

Expand Down Expand Up @@ -128,6 +131,7 @@
<service id="twig.form.renderer" class="Symfony\Bridge\Twig\Form\TwigRenderer" public="false">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

must be public to be lazy-loadable

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed, fixed

<argument type="service" id="twig.form.engine" />
<argument type="service" id="security.csrf.token_manager" on-invalid="null" />
<tag name="twig.runtime" />
</service>

<service id="twig.translation.extractor" class="Symfony\Bridge\Twig\Translation\TwigExtractor">
Expand Down Expand Up @@ -160,5 +164,10 @@
<argument /> <!-- decimal point, set in TwigExtension -->
<argument /> <!-- thousands separator, set in TwigExtension -->
</service>

<service id="twig.runtime_loader" class="Symfony\Bundle\TwigBundle\ContainerAwareRuntimeLoader" public="false">
<argument type="service" id="service_container" />
<argument type="collection" /> <!-- the mapping between class names and service names -->
</service>
</services>
</container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?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\Bundle\TwigBundle\Tests;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Bundle\TwigBundle\ContainerAwareRuntimeLoader;

class ContainerAwareRuntimeLoaderTest extends TestCase
{
public function testLoad()
{
$container = $this->getMockBuilder(ContainerInterface::class)->getMock();
$container->expects($this->once())->method('get')->with('foo');

$loader = new ContainerAwareRuntimeLoader($container, array(
'FooClass' => 'foo',
));
$loader->load('FooClass');
}

/**
* @expectedException \LogicException
* @expectedExceptionMessage Class "BarClass" is not configured as a Twig runtime.
*/
public function testLoadWithoutAMatch()
{
$loader = new ContainerAwareRuntimeLoader($this->getMockBuilder(ContainerInterface::class)->getMock(), array());
$loader->load('BarClass');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@

namespace Symfony\Bundle\TwigBundle\Tests\DependencyInjection;

use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\RuntimeLoaderPass;
use Symfony\Bundle\TwigBundle\DependencyInjection\TwigExtension;
use Symfony\Bundle\TwigBundle\Tests\TestCase;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
Expand Down Expand Up @@ -62,17 +64,17 @@ public function testLoadFullConfiguration($format)
$calls = $container->getDefinition('twig')->getMethodCalls();
$this->assertEquals('app', $calls[0][1][0], '->load() registers services as Twig globals');
$this->assertEquals(new Reference('twig.app_variable'), $calls[0][1][1]);
$this->assertEquals('foo', $calls[1][1][0], '->load() registers services as Twig globals');
$this->assertEquals(new Reference('bar'), $calls[1][1][1], '->load() registers services as Twig globals');
$this->assertEquals('baz', $calls[2][1][0], '->load() registers variables as Twig globals');
$this->assertEquals('@qux', $calls[2][1][1], '->load() allows escaping of service identifiers');
$this->assertEquals('pi', $calls[3][1][0], '->load() registers variables as Twig globals');
$this->assertEquals(3.14, $calls[3][1][1], '->load() registers variables as Twig globals');
$this->assertEquals('foo', $calls[2][1][0], '->load() registers services as Twig globals');
$this->assertEquals(new Reference('bar'), $calls[2][1][1], '->load() registers services as Twig globals');
$this->assertEquals('baz', $calls[3][1][0], '->load() registers variables as Twig globals');
$this->assertEquals('@qux', $calls[3][1][1], '->load() allows escaping of service identifiers');
$this->assertEquals('pi', $calls[4][1][0], '->load() registers variables as Twig globals');
$this->assertEquals(3.14, $calls[4][1][1], '->load() registers variables as Twig globals');

// Yaml and Php specific configs
if (in_array($format, array('yml', 'php'))) {
$this->assertEquals('bad', $calls[4][1][0], '->load() registers variables as Twig globals');
$this->assertEquals(array('key' => 'foo'), $calls[4][1][1], '->load() registers variables as Twig globals');
$this->assertEquals('bad', $calls[5][1][0], '->load() registers variables as Twig globals');
$this->assertEquals(array('key' => 'foo'), $calls[5][1][1], '->load() registers variables as Twig globals');
}

// Twig options
Expand Down Expand Up @@ -133,7 +135,7 @@ public function testGlobalsWithDifferentTypesAndValues()
$this->compileContainer($container);

$calls = $container->getDefinition('twig')->getMethodCalls();
foreach (array_slice($calls, 1) as $call) {
foreach (array_slice($calls, 2) as $call) {
list($name, $value) = each($globals);
$this->assertEquals($name, $call[1][0]);
$this->assertSame($value, $call[1][1]);
Expand Down Expand Up @@ -211,6 +213,29 @@ public function stopwatchExtensionAvailabilityProvider()
);
}

public function testRuntimeLoader()
{
$container = $this->createContainer();
$container->registerExtension(new TwigExtension());
$container->loadFromExtension('twig', array());
$container->setParameter('kernel.environment', 'test');
$container->setParameter('debug.file_link_format', 'test');
$container->setParameter('foo', 'FooClass');
$container->register('http_kernel', 'FooClass');
$container->register('templating.locator', 'FooClass');
$container->register('templating.name_parser', 'FooClass');
$container->register('foo', '%foo%')->addTag('twig.runtime');
$container->addCompilerPass(new RuntimeLoaderPass(), PassConfig::TYPE_BEFORE_REMOVING);
$container->getCompilerPassConfig()->setRemovingPasses(array());
$container->compile();

$loader = $container->getDefinition('twig.runtime_loader');
$this->assertEquals(array(
'Symfony\Bridge\Twig\Form\TwigRenderer' => 'twig.form.renderer',
'FooClass' => 'foo',
), $loader->getArgument(1));
}

private function createContainer()
{
$container = new ContainerBuilder(new ParameterBag(array(
Expand Down
3 changes: 3 additions & 0 deletions src/Symfony/Bundle/TwigBundle/TwigBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
namespace Symfony\Bundle\TwigBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\TwigEnvironmentPass;
use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\TwigLoaderPass;
use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\ExceptionListenerPass;
use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\ExtensionPass;
use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\RuntimeLoaderPass;

/**
* Bundle.
Expand All @@ -33,5 +35,6 @@ public function build(ContainerBuilder $container)
$container->addCompilerPass(new TwigEnvironmentPass());
$container->addCompilerPass(new TwigLoaderPass());
$container->addCompilerPass(new ExceptionListenerPass());
$container->addCompilerPass(new RuntimeLoaderPass(), PassConfig::TYPE_BEFORE_REMOVING);
}
}