From f0d2ce7f322d47ccb98d7f9b1f1b66a9d8877d5b Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Thu, 17 Feb 2011 19:19:32 -0500 Subject: [PATCH] [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. --- .../DependencyInjection/Configuration.php | 122 ++++++++++++++ .../DependencyInjection/TwigExtension.php | 128 ++++++--------- .../Resources/config/schema/twig-1.0.xsd | 20 ++- .../TwigBundle/Resources/config/twig.xml | 8 - .../DependencyInjection/Fixtures/php/full.php | 25 +++ .../DependencyInjection/Fixtures/xml/full.xml | 18 +++ .../DependencyInjection/Fixtures/yml/full.yml | 18 +++ .../DependencyInjection/TwigExtensionTest.php | 152 ++++++++++++------ 8 files changed, 347 insertions(+), 144 deletions(-) create mode 100644 src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php create mode 100644 src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/full.php create mode 100644 src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/full.xml create mode 100644 src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/full.yml diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php new file mode 100644 index 0000000000..07f2a12046 --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php @@ -0,0 +1,122 @@ + + */ +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() + ; + } +} diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php index 29bb03e0e5..d530d1b2f6 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php @@ -11,24 +11,69 @@ namespace Symfony\Bundle\TwigBundle\DependencyInjection; -use Symfony\Component\HttpKernel\DependencyInjection\Extension; -use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\Config\Definition\Processor; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\HttpKernel\DependencyInjection\Extension; /** * TwigExtension. * * @author Fabien Potencier + * @author Jeremy Mikola */ class TwigExtension extends Extension { + /** + * Responds to the twig configuration parameter. + * + * @param array $configs + * @param ContainerBuilder $container + */ public function load(array $configs, ContainerBuilder $container) { $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $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( 'Twig_Environment', 'Twig_ExtensionInterface', @@ -41,83 +86,6 @@ class TwigExtension extends Extension 'Twig_TemplateInterface', '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)); } /** diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/schema/twig-1.0.xsd b/src/Symfony/Bundle/TwigBundle/Resources/config/schema/twig-1.0.xsd index 932983c7df..fd98eb90d7 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/schema/twig-1.0.xsd +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/schema/twig-1.0.xsd @@ -14,14 +14,14 @@ - - - - - + + - + + + + @@ -32,7 +32,7 @@ - + @@ -47,4 +47,10 @@ + + + + + + diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml index 5067d1896d..251e31487b 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml @@ -6,16 +6,8 @@ Twig_Environment - - %kernel.charset% - %kernel.debug% - %kernel.cache_dir%/twig - Symfony\Bundle\TwigBundle\Loader\FilesystemLoader Symfony\Bundle\TwigBundle\GlobalVariables - - TwigBundle::form.html.twig - Symfony\Bundle\TwigBundle\TwigEngine Symfony\Bundle\TwigBundle\CacheWarmer\TemplateCacheCacheWarmer diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/full.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/full.php new file mode 100644 index 0000000000..840e393aa1 --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/full.php @@ -0,0 +1,25 @@ +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, +)); diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/full.xml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/full.xml new file mode 100644 index 0000000000..07677caca0 --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/full.xml @@ -0,0 +1,18 @@ + + + + + + + MyBundle::form.html.twig + + + 3.14 + + + + diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/full.yml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/full.yml new file mode 100644 index 0000000000..55378d6acc --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/full.yml @@ -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 diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php index 9e1394f438..9abe53ebfa 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php @@ -11,72 +11,126 @@ namespace Symfony\Bundle\TwigBundle\Tests\DependencyInjection; -use Symfony\Bundle\TwigBundle\Tests\TestCase; 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\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 { - public function testLoad() + /** + * @dataProvider getFormats + */ + public function testLoadEmptyConfiguration($format) { - $container = new ContainerBuilder(); - $loader = new TwigExtension(); + $container = $this->createContainer(); + $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 if not already loaded'); + $this->assertEquals('Twig_Environment', $container->getParameter('twig.class'), '->load() loads the twig.xml file'); + $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'); - $this->assertEquals('ISO-8859-1', $options['charset'], '->load() overrides existing configuration options'); - $this->assertEquals('%kernel.debug%', $options['debug'], '->load() merges the new values with the old ones'); + $this->assertEquals(__DIR__.'/twig', $options['cache'], '->load() sets default value for cache option'); + $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 = new ContainerBuilder(); - $loader = new TwigExtension(); - $loader->load(array(array('global' => array( - 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]); + $container = $this->createContainer(); + $container->registerExtension(new TwigExtension()); + $this->loadFromFile($container, 'full', $format); + $this->compileContainer($container); - // YAML, PHP - $container = new ContainerBuilder(); - $loader = new TwigExtension(); - $loader->load(array(array('globals' => array( - 'foo' => '@bar', - 'pi' => 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]); + $this->assertEquals('Twig_Environment', $container->getParameter('twig.class'), '->load() loads the twig.xml file'); + $this->assertTrue($container->getDefinition('templating.cache_warmer.templates_cache')->hasTag('kernel.cache_warmer'), '->load() enables cache warming'); + + // Extensions + foreach (array('twig.extension.debug', 'twig.extension.text') as $id) { + $config = $container->getDefinition($id); + $this->assertEquals(array('twig.extension'), array_keys($config->getTags()), '->load() adds tags to extension definitions'); + } + + // Form resources + $resources = $container->getParameter('twig.form.resources'); + $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 - $container = new ContainerBuilder(); - $container->register('foo', 'stdClass'); - $loader = new TwigExtension(); - $loader->load(array(array('extensions' => array(array('id' => 'foo')))), $container); - $config = $container->getDefinition('foo'); - $this->assertEquals(array('twig.extension'), array_keys($config->getTags())); + return array( + array('php'), + array('yml'), + array('xml'), + ); + } - // YAML, PHP - $container = new ContainerBuilder(); - $container->register('foo', 'stdClass'); - $loader = new TwigExtension(); - $loader->load(array(array('extensions' => array('foo'))), $container); - $config = $container->getDefinition('foo'); - $this->assertEquals(array('twig.extension'), array_keys($config->getTags())); + private function createContainer() + { + $container = new ContainerBuilder(new ParameterBag(array( + 'kernel.cache_dir' => __DIR__, + 'kernel.charset' => 'UTF-8', + 'kernel.debug' => false, + ))); + + 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); } }