diff --git a/src/Symfony/Bundle/TwigBundle/ContainerAwareRuntimeLoader.php b/src/Symfony/Bundle/TwigBundle/ContainerAwareRuntimeLoader.php new file mode 100644 index 0000000000000..89607a55912f9 --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/ContainerAwareRuntimeLoader.php @@ -0,0 +1,43 @@ + + * + * 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 + */ +class ContainerAwareRuntimeLoader implements \Twig_RuntimeLoaderInterface +{ + private $container; + private $mapping; + + public function __construct(ContainerInterface $container, array $mapping) + { + $this->container = $container; + $this->mapping = $mapping; + } + + /** + * {@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)); + } + + return $this->container->get($this->mapping[$class]); + } +} diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/RuntimeLoaderPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/RuntimeLoaderPass.php new file mode 100644 index 0000000000000..4cbc00c788d4a --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/RuntimeLoaderPass.php @@ -0,0 +1,36 @@ + + * + * 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; + } + + $definition->replaceArgument(1, $mapping); + } +} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml index d7b3ebfb390ae..f11445a535ea5 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml @@ -12,6 +12,9 @@ app + + + @@ -128,6 +131,7 @@ + @@ -160,5 +164,10 @@ + + + + + diff --git a/src/Symfony/Bundle/TwigBundle/Tests/ContainerAwareRuntimeLoaderTest.php b/src/Symfony/Bundle/TwigBundle/Tests/ContainerAwareRuntimeLoaderTest.php new file mode 100644 index 0000000000000..82862289624d6 --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Tests/ContainerAwareRuntimeLoaderTest.php @@ -0,0 +1,39 @@ + + * + * 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'); + } +} diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php index a99125a21c149..e5d16c6ced246 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php @@ -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; @@ -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 @@ -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]); @@ -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( diff --git a/src/Symfony/Bundle/TwigBundle/TwigBundle.php b/src/Symfony/Bundle/TwigBundle/TwigBundle.php index 7b4a2053f0f16..76d0676409b88 100644 --- a/src/Symfony/Bundle/TwigBundle/TwigBundle.php +++ b/src/Symfony/Bundle/TwigBundle/TwigBundle.php @@ -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. @@ -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); } }