[TwigBundle] Refactored TwigExtension class and implemented configuration tree

Added config fixtures in each format to demonstrate the possible styles of all of the extension options. These should all be covered by the updated tests. Made XSD slightly more restrictive, with regards to the "type" attribute on globals. This is coupled with validation in the configuration class.
This commit is contained in:
Jeremy Mikola 2011-02-17 19:19:32 -05:00
parent 9b602626eb
commit f0d2ce7f32
8 changed files with 347 additions and 144 deletions

View File

@ -0,0 +1,122 @@
<?php
namespace Symfony\Bundle\TwigBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\NodeBuilder;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
/**
* TwigExtension configuration structure.
*
* @author Jeremy Mikola <jmikola@gmail.com>
*/
class Configuration
{
/**
* Generates the configuration tree.
*
* @return \Symfony\Component\Config\Definition\NodeInterface
*/
public function getConfigTree()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('twig', 'array');
$rootNode
->scalarNode('cache_warmer')->end()
;
$this->addExtensionsSection($rootNode);
$this->addFormSection($rootNode);
$this->addGlobalsSection($rootNode);
$this->addTwigOptions($rootNode);
return $treeBuilder->buildTree();
}
private function addExtensionsSection(NodeBuilder $rootNode)
{
$rootNode
->fixXmlConfig('extension')
->arrayNode('extensions')
->prototype('scalar')
->beforeNormalization()
->ifTrue(function($v) { return is_array($v) && isset($v['id']); })
->then(function($v){ return $v['id']; })
->end()
->end()
->end()
;
}
private function addFormSection(NodeBuilder $rootNode)
{
$rootNode
->arrayNode('form')
->addDefaultsIfNotSet()
->fixXmlConfig('resource')
->arrayNode('resources')
->addDefaultsIfNotSet()
->defaultValue(array('TwigBundle::form.html.twig'))
->validate()
->always()
->then(function($v){
return array_merge(array('TwigBundle::form.html.twig'), $v);
})
->end()
->prototype('scalar')->end()
->end()
->end()
;
}
private function addGlobalsSection(NodeBuilder $rootNode)
{
$rootNode
->fixXmlConfig('global')
->arrayNode('globals')
->useAttributeAsKey('key')
->prototype('array')
->beforeNormalization()
->ifTrue(function($v){ return is_scalar($v); })
->then(function($v){
return ('@' === substr($v, 0, 1))
? array('id' => substr($v, 1), 'type' => 'service')
: array('value' => $v);
})
->end()
->scalarNode('id')->end()
->scalarNode('type')
->validate()
->ifNotInArray(array('service'))
->thenInvalid('The %s type is not supported')
->end()
->end()
->scalarNode('value')->end()
->end()
->end()
;
}
private function addTwigOptions(NodeBuilder $rootNode)
{
$rootNode
->scalarNode('autoescape')->end()
->scalarNode('base_template_class')->end()
->scalarNode('cache')
->addDefaultsIfNotSet()
->defaultValue('%kernel.cache_dir%/twig')
->end()
->scalarNode('charset')
->addDefaultsIfNotSet()
->defaultValue('%kernel.charset%')
->end()
->scalarNode('debug')
->addDefaultsIfNotSet()
->defaultValue('%kernel.debug%')
->end()
->scalarNode('strict_variables')->end()
->scalarNode('auto_reload')->end()
;
}
}

View File

@ -11,24 +11,69 @@
namespace Symfony\Bundle\TwigBundle\DependencyInjection; namespace Symfony\Bundle\TwigBundle\DependencyInjection;
use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
/** /**
* TwigExtension. * TwigExtension.
* *
* @author Fabien Potencier <fabien.potencier@symfony-project.com> * @author Fabien Potencier <fabien.potencier@symfony-project.com>
* @author Jeremy Mikola <jmikola@gmail.com>
*/ */
class TwigExtension extends Extension class TwigExtension extends Extension
{ {
/**
* Responds to the twig configuration parameter.
*
* @param array $configs
* @param ContainerBuilder $container
*/
public function load(array $configs, ContainerBuilder $container) public function load(array $configs, ContainerBuilder $container)
{ {
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('twig.xml'); $loader->load('twig.xml');
$processor = new Processor();
$configuration = new Configuration();
$config = $processor->process($configuration->getConfigTree(), $configs);
$container->setParameter('twig.form.resources', $config['form']['resources']);
if (!empty($config['globals'])) {
$def = $container->getDefinition('twig');
foreach ($config['globals'] as $key => $global) {
if (isset($global['type']) && 'service' === $global['type']) {
$def->addMethodCall('addGlobal', array($key, new Reference($global['id'])));
} else {
$def->addMethodCall('addGlobal', array($key, $global['value']));
}
}
}
if (!empty($config['extensions'])) {
foreach ($config['extensions'] as $id) {
$container->getDefinition($id)->addTag('twig.extension');
}
}
if (!empty($config['cache_warmer'])) {
$container->getDefinition('templating.cache_warmer.templates_cache')->addTag('kernel.cache_warmer');
}
unset(
$config['form'],
$config['globals'],
$config['extensions'],
$config['cache_warmer']
);
$container->setParameter('twig.options', $config);
$this->addClassesToCompile(array( $this->addClassesToCompile(array(
'Twig_Environment', 'Twig_Environment',
'Twig_ExtensionInterface', 'Twig_ExtensionInterface',
@ -41,83 +86,6 @@ class TwigExtension extends Extension
'Twig_TemplateInterface', 'Twig_TemplateInterface',
'Twig_Template', 'Twig_Template',
)); ));
foreach ($configs as $config) {
$this->doConfigLoad($config, $container);
}
}
/**
* Loads the Twig configuration.
*
* @param array $config An array of configuration settings
* @param ContainerBuilder $container A ContainerBuilder instance
*/
protected function doConfigLoad(array $config, ContainerBuilder $container)
{
// form resources
foreach (array('resources', 'resource') as $key) {
if (isset($config['form'][$key])) {
$resources = (array) $config['form'][$key];
$container->setParameter('twig.form.resources', array_merge($container->getParameter('twig.form.resources'), $resources));
unset($config['form'][$key]);
}
}
// globals
$def = $container->getDefinition('twig');
$globals = $this->normalizeConfig($config, 'global');
if (isset($globals[0])) {
foreach ($globals as $global) {
if (isset($global['type']) && 'service' === $global['type']) {
$def->addMethodCall('addGlobal', array($global['key'], new Reference($global['id'])));
} elseif (isset($global['value'])) {
$def->addMethodCall('addGlobal', array($global['key'], $global['value']));
} else {
throw new \InvalidArgumentException(sprintf('Unable to understand global configuration (%s).', var_export($global, true)));
}
}
} else {
foreach ($globals as $key => $value) {
if (is_string($value) && '@' === substr($value, 0, 1)) {
$def->addMethodCall('addGlobal', array($key, new Reference(substr($value, 1))));
} else {
$def->addMethodCall('addGlobal', array($key, $value));
}
}
}
unset($config['globals'], $config['global']);
// extensions
$extensions = $this->normalizeConfig($config, 'extension');
if (isset($extensions[0]) && is_array($extensions[0])) {
foreach ($extensions as $extension) {
$container->getDefinition($extension['id'])->addTag('twig.extension');
}
} else {
foreach ($extensions as $id) {
$container->getDefinition($id)->addTag('twig.extension');
}
}
unset($config['extensions'], $config['extension']);
// convert - to _
foreach ($config as $key => $value) {
if (false !== strpos($key, '-')) {
unset($config[$key]);
$config[str_replace('-', '_', $key)] = $value;
}
}
if (isset($config['cache-warmer'])) {
$config['cache_warmer'] = $config['cache-warmer'];
}
if (isset($config['cache_warmer']) && $config['cache_warmer']) {
$container->getDefinition('templating.cache_warmer.templates_cache')->addTag('kernel.cache_warmer');
}
$container->setParameter('twig.options', array_replace($container->getParameter('twig.options'), $config));
} }
/** /**

View File

@ -14,14 +14,14 @@
<xsd:element name="extension" type="extension" minOccurs="0" maxOccurs="unbounded" /> <xsd:element name="extension" type="extension" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence> </xsd:sequence>
<xsd:attribute name="charset" type="xsd:string" /> <xsd:attribute name="auto-reload" type="xsd:boolean" />
<xsd:attribute name="debug" type="xsd:string" /> <xsd:attribute name="autoescape" type="xsd:boolean" />
<xsd:attribute name="cache" type="xsd:string" />
<xsd:attribute name="strict-variables" type="xsd:string" />
<xsd:attribute name="auto-reload" type="xsd:string" />
<xsd:attribute name="base-template-class" type="xsd:string" /> <xsd:attribute name="base-template-class" type="xsd:string" />
<xsd:attribute name="autoescape" type="xsd:string" /> <xsd:attribute name="cache" type="xsd:string" />
<xsd:attribute name="cache-warmer" type="cache_warmer" /> <xsd:attribute name="cache-warmer" type="cache_warmer" />
<xsd:attribute name="charset" type="xsd:string" />
<xsd:attribute name="debug" type="xsd:boolean" />
<xsd:attribute name="strict-variables" type="xsd:boolean" />
</xsd:complexType> </xsd:complexType>
<xsd:complexType name="form"> <xsd:complexType name="form">
@ -32,7 +32,7 @@
<xsd:complexType name="global" mixed="true"> <xsd:complexType name="global" mixed="true">
<xsd:attribute name="key" type="xsd:string" use="required" /> <xsd:attribute name="key" type="xsd:string" use="required" />
<xsd:attribute name="type" type="xsd:string" /> <xsd:attribute name="type" type="global_type" />
<xsd:attribute name="id" type="xsd:string" /> <xsd:attribute name="id" type="xsd:string" />
</xsd:complexType> </xsd:complexType>
@ -47,4 +47,10 @@
<xsd:enumeration value="full" /> <xsd:enumeration value="full" />
</xsd:restriction> </xsd:restriction>
</xsd:simpleType> </xsd:simpleType>
<xsd:simpleType name="global_type">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="service" />
</xsd:restriction>
</xsd:simpleType>
</xsd:schema> </xsd:schema>

View File

@ -6,16 +6,8 @@
<parameters> <parameters>
<parameter key="twig.class">Twig_Environment</parameter> <parameter key="twig.class">Twig_Environment</parameter>
<parameter key="twig.options" type="collection">
<parameter key="charset">%kernel.charset%</parameter>
<parameter key="debug">%kernel.debug%</parameter>
<parameter key="cache">%kernel.cache_dir%/twig</parameter>
</parameter>
<parameter key="twig.loader.class">Symfony\Bundle\TwigBundle\Loader\FilesystemLoader</parameter> <parameter key="twig.loader.class">Symfony\Bundle\TwigBundle\Loader\FilesystemLoader</parameter>
<parameter key="twig.globals.class">Symfony\Bundle\TwigBundle\GlobalVariables</parameter> <parameter key="twig.globals.class">Symfony\Bundle\TwigBundle\GlobalVariables</parameter>
<parameter key="twig.form.resources" type="collection">
<parameter>TwigBundle::form.html.twig</parameter>
</parameter>
<parameter key="templating.engine.twig.class">Symfony\Bundle\TwigBundle\TwigEngine</parameter> <parameter key="templating.engine.twig.class">Symfony\Bundle\TwigBundle\TwigEngine</parameter>
<parameter key="templating.cache_warmer.templates_cache.class">Symfony\Bundle\TwigBundle\CacheWarmer\TemplateCacheCacheWarmer</parameter> <parameter key="templating.cache_warmer.templates_cache.class">Symfony\Bundle\TwigBundle\CacheWarmer\TemplateCacheCacheWarmer</parameter>
</parameters> </parameters>

View File

@ -0,0 +1,25 @@
<?php
$container->loadFromExtension('twig', array(
'form' => array(
'resources' => array(
'MyBundle::form.html.twig',
)
),
'extensions' => array(
'twig.extension.debug',
'twig.extension.text',
),
'globals' => array(
'foo' => '@bar',
'pi' => 3.14,
),
'auto_reload' => true,
'autoescape' => true,
'base_template_class' => 'stdClass',
'cache' => '/tmp',
'cache_warmer' => true,
'charset' => 'ISO-8859-1',
'debug' => true,
'strict_variables' => true,
));

View File

@ -0,0 +1,18 @@
<?xml version="1.0" ?>
<container xmlns="http://www.symfony-project.org/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:twig="http://www.symfony-project.org/schema/dic/twig"
xsi:schemaLocation="http://www.symfony-project.org/schema/dic/services http://www.symfony-project.org/schema/dic/services/services-1.0.xsd
http://www.symfony-project.org/schema/dic/twig http://www.symfony-project.org/schema/dic/twig/twig-1.0.xsd">
<twig:config auto-reload="true" autoescape="true" base-template-class="stdClass" cache="/tmp" cache-warmer="true" charset="ISO-8859-1" debug="true" strict-variables="true">
<twig:form>
<twig:resource>MyBundle::form.html.twig</twig:resource>
</twig:form>
<twig:global key="foo" id="bar" type="service" />
<twig:global key="pi">3.14</twig:global>
<twig:extension id="twig.extension.debug" />
<twig:extension id="twig.extension.text" />
</twig:config>
</container>

View File

@ -0,0 +1,18 @@
twig:
form:
resources:
- MyBundle::form.html.twig
extensions:
- twig.extension.debug
- twig.extension.text
globals:
foo: @bar
pi: 3.14
auto_reload: true
autoescape: true
base_template_class: stdClass
cache: /tmp
cache_warmer: true
charset: ISO-8859-1
debug: true
strict_variables: true

View File

@ -11,72 +11,126 @@
namespace Symfony\Bundle\TwigBundle\Tests\DependencyInjection; namespace Symfony\Bundle\TwigBundle\Tests\DependencyInjection;
use Symfony\Bundle\TwigBundle\Tests\TestCase;
use Symfony\Bundle\TwigBundle\DependencyInjection\TwigExtension; use Symfony\Bundle\TwigBundle\DependencyInjection\TwigExtension;
use Symfony\Bundle\TwigBundle\Tests\TestCase;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
class TwigExtensionTest extends TestCase class TwigExtensionTest extends TestCase
{ {
public function testLoad() /**
* @dataProvider getFormats
*/
public function testLoadEmptyConfiguration($format)
{ {
$container = new ContainerBuilder(); $container = $this->createContainer();
$loader = new TwigExtension(); $container->registerExtension(new TwigExtension());
$container->loadFromExtension('twig', array());
$this->compileContainer($container);
$loader->load(array(array()), $container); $this->assertEquals('Twig_Environment', $container->getParameter('twig.class'), '->load() loads the twig.xml file');
$this->assertEquals('Twig_Environment', $container->getParameter('twig.class'), '->load() loads the twig.xml file if not already loaded'); $this->assertFalse($container->getDefinition('templating.cache_warmer.templates_cache')->hasTag('kernel.cache_warmer'), '->load() does not enable cache warming by default');
$this->assertContains('TwigBundle::form.html.twig', $container->getParameter('twig.form.resources'), '->load() includes default template for form resources');
$loader->load(array(array('charset' => 'ISO-8859-1')), $container); // Twig options
$options = $container->getParameter('twig.options'); $options = $container->getParameter('twig.options');
$this->assertEquals('ISO-8859-1', $options['charset'], '->load() overrides existing configuration options'); $this->assertEquals(__DIR__.'/twig', $options['cache'], '->load() sets default value for cache option');
$this->assertEquals('%kernel.debug%', $options['debug'], '->load() merges the new values with the old ones'); $this->assertEquals('UTF-8', $options['charset'], '->load() sets default value for charset option');
$this->assertEquals(false, $options['debug'], '->load() sets default value for debug option');
} }
public function testConfigGlobals() /**
* @dataProvider getFormats
*/
public function testLoadFullConfiguration($format)
{ {
// XML $container = $this->createContainer();
$container = new ContainerBuilder(); $container->registerExtension(new TwigExtension());
$loader = new TwigExtension(); $this->loadFromFile($container, 'full', $format);
$loader->load(array(array('global' => array( $this->compileContainer($container);
array('key' => 'foo', 'type' => 'service', 'id' => 'bar'),
array('key' => 'pi', 'value' => 3.14),
))), $container);
$config = $container->getDefinition('twig')->getMethodCalls();
$this->assertEquals('foo', $config[0][1][0]);
$this->assertEquals(new Reference('bar'), $config[0][1][1]);
$this->assertEquals('pi', $config[1][1][0]);
$this->assertEquals(3.14, $config[1][1][1]);
// YAML, PHP $this->assertEquals('Twig_Environment', $container->getParameter('twig.class'), '->load() loads the twig.xml file');
$container = new ContainerBuilder(); $this->assertTrue($container->getDefinition('templating.cache_warmer.templates_cache')->hasTag('kernel.cache_warmer'), '->load() enables cache warming');
$loader = new TwigExtension();
$loader->load(array(array('globals' => array( // Extensions
'foo' => '@bar', foreach (array('twig.extension.debug', 'twig.extension.text') as $id) {
'pi' => 3.14, $config = $container->getDefinition($id);
))), $container); $this->assertEquals(array('twig.extension'), array_keys($config->getTags()), '->load() adds tags to extension definitions');
$config = $container->getDefinition('twig')->getMethodCalls(); }
$this->assertEquals('foo', $config[0][1][0]);
$this->assertEquals(new Reference('bar'), $config[0][1][1]); // Form resources
$this->assertEquals('pi', $config[1][1][0]); $resources = $container->getParameter('twig.form.resources');
$this->assertEquals(3.14, $config[1][1][1]); $this->assertContains('TwigBundle::form.html.twig', $resources, '->load() includes default template for form resources');
$this->assertContains('MyBundle::form.html.twig', $resources, '->load() merges new templates into form resources');
// Globals
$calls = $container->getDefinition('twig')->getMethodCalls();
$this->assertEquals('foo', $calls[0][1][0], '->load() registers services as Twig globals');
$this->assertEquals(new Reference('bar'), $calls[0][1][1], '->load() registers services as Twig globals');
$this->assertEquals('pi', $calls[1][1][0], '->load() registers variables as Twig globals');
$this->assertEquals(3.14, $calls[1][1][1], '->load() registers variables as Twig globals');
// Twig options
$options = $container->getParameter('twig.options');
$this->assertTrue($options['auto_reload'], '->load() sets the auto_reload option');
$this->assertTrue($options['autoescape'], '->load() sets the autoescape option');
$this->assertEquals('stdClass', $options['base_template_class'], '->load() sets the base_template_class option');
$this->assertEquals('/tmp', $options['cache'], '->load() sets the cache option');
$this->assertEquals('ISO-8859-1', $options['charset'], '->load() sets the charset option');
$this->assertTrue($options['debug'], '->load() sets the debug option');
$this->assertTrue($options['strict_variables'], '->load() sets the strict_variables option');
} }
public function testConfigExtensions() public function getFormats()
{ {
// XML return array(
$container = new ContainerBuilder(); array('php'),
$container->register('foo', 'stdClass'); array('yml'),
$loader = new TwigExtension(); array('xml'),
$loader->load(array(array('extensions' => array(array('id' => 'foo')))), $container); );
$config = $container->getDefinition('foo'); }
$this->assertEquals(array('twig.extension'), array_keys($config->getTags()));
// YAML, PHP private function createContainer()
$container = new ContainerBuilder(); {
$container->register('foo', 'stdClass'); $container = new ContainerBuilder(new ParameterBag(array(
$loader = new TwigExtension(); 'kernel.cache_dir' => __DIR__,
$loader->load(array(array('extensions' => array('foo'))), $container); 'kernel.charset' => 'UTF-8',
$config = $container->getDefinition('foo'); 'kernel.debug' => false,
$this->assertEquals(array('twig.extension'), array_keys($config->getTags())); )));
return $container;
}
private function compileContainer(ContainerBuilder $container)
{
$container->getCompilerPassConfig()->setOptimizationPasses(array());
$container->getCompilerPassConfig()->setRemovingPasses(array());
$container->compile();
}
private function loadFromFile(ContainerBuilder $container, $file, $format)
{
$locator = new FileLocator(__DIR__.'/Fixtures/'.$format);
switch ($format) {
case 'php':
$loader = new PhpFileLoader($container, $locator);
break;
case 'xml':
$loader = new XmlFileLoader($container, $locator);
break;
case 'yml':
$loader = new YamlFileLoader($container, $locator);
break;
default:
throw new \InvalidArgumentException('Unsupported format: '.$format);
}
$loader->load($file.'.'.$format);
} }
} }