Skip to content
This repository
Browse code

[TwigBundle] added support for Twig namespaced paths (Twig 1.10)

In a template, you can now use native Twig template names, instead of
the Symfony ones:

Before (still works):

    {% extends "AcmeDemoBundle::layout.html.twig" %}
    {% include "AcmeDemoBundle:Foo:bar.html.twig" %}

After:

    {% extends "@AcmeDemo/layout.html.twig" %}
    {% include "@AcmeDemo/Foo/bar.html.twig" %}

Using native template names is also faster.

The only drawback is that the new notation looks similar to the way we
locate resources in Symfony, which would be
@AcmeDemoBundle/Resources/views/Foo/bar.html.twig. We could have used
the same notation, but it is rather verbose (and by the way, using this
notation did not work anyway in templates).
  • Loading branch information...
commit 5c809d8ffb5e66f532cca34597c3f513ff017691 1 parent 0bfa86c
Fabien Potencier authored October 01, 2012
6  src/Symfony/Bundle/TwigBundle/CHANGELOG.md
Source Rendered
... ...
@@ -1,6 +1,12 @@
1 1
 CHANGELOG
2 2
 =========
3 3
 
  4
+2.2.0
  5
+-----
  6
+
  7
+ * added automatic registration of namespaced paths for registered bundles
  8
+ * added support for namespaced paths
  9
+
4 10
 2.1.0
5 11
 -----
6 12
 
23  src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php
@@ -126,6 +126,29 @@ private function addTwigOptions(ArrayNodeDefinition $rootNode)
126 126
                 ->scalarNode('auto_reload')->end()
127 127
                 ->scalarNode('optimizations')->end()
128 128
                 ->arrayNode('paths')
  129
+                    ->beforeNormalization()
  130
+                        ->always()
  131
+                        ->then(function ($paths) {
  132
+                            $normalized = array();
  133
+                            foreach ($paths as $path => $namespace) {
  134
+                                if (is_array($namespace)) {
  135
+                                    // xml
  136
+                                    $path = $namespace['value'];
  137
+                                    $namespace = $namespace['namespace'];
  138
+                                }
  139
+
  140
+                                // path within the default namespace
  141
+                                if (ctype_digit((string) $path)) {
  142
+                                    $path = $namespace;
  143
+                                    $namespace = null;
  144
+                                }
  145
+
  146
+                                $normalized[$path] = $namespace;
  147
+                            }
  148
+
  149
+                            return $normalized;
  150
+                        })
  151
+                    ->end()
129 152
                     ->prototype('variable')->end()
130 153
                 ->end()
131 154
             ->end()
36  src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php
@@ -60,12 +60,33 @@ public function load(array $configs, ContainerBuilder $container)
60 60
         $reflClass = new \ReflectionClass('Symfony\Bridge\Twig\Extension\FormExtension');
61 61
         $container->getDefinition('twig.loader')->addMethodCall('addPath', array(dirname(dirname($reflClass->getFileName())).'/Resources/views/Form'));
62 62
 
63  
-        if (!empty($config['paths'])) {
64  
-            foreach ($config['paths'] as $path) {
65  
-                $container->getDefinition('twig.loader')->addMethodCall('addPath', array($path));
  63
+        $twigLoaderDefinition = $container->getDefinition('twig.loader');
  64
+
  65
+        // register user-configured paths
  66
+        foreach ($config['paths'] as $path => $namespace) {
  67
+            if (!$namespace) {
  68
+                $twigLoaderDefinition->addMethodCall('addPath', array($path));
  69
+            } else {
  70
+                $twigLoaderDefinition->addMethodCall('addPath', array($path, $namespace));
66 71
             }
67 72
         }
68 73
 
  74
+        // register bundles as Twig namespaces
  75
+        foreach ($container->getParameter('kernel.bundles') as $bundle => $class) {
  76
+            if (is_dir($dir = $container->getParameter('kernel.root_dir').'/Resources/'.$bundle.'/views')) {
  77
+                $this->addTwigPath($twigLoaderDefinition, $dir, $bundle);
  78
+            }
  79
+
  80
+            $reflection = new \ReflectionClass($class);
  81
+            if (is_dir($dir = dirname($reflection->getFilename()).'/Resources/views')) {
  82
+                $this->addTwigPath($twigLoaderDefinition, $dir, $bundle);
  83
+            }
  84
+        }
  85
+
  86
+        if (is_dir($dir = $container->getParameter('kernel.root_dir').'/Resources/views')) {
  87
+            $twigLoaderDefinition->addMethodCall('addPath', array($dir));
  88
+        }
  89
+
69 90
         if (!empty($config['globals'])) {
70 91
             $def = $container->getDefinition('twig');
71 92
             foreach ($config['globals'] as $key => $global) {
@@ -108,6 +129,15 @@ public function load(array $configs, ContainerBuilder $container)
108 129
         ));
109 130
     }
110 131
 
  132
+    private function addTwigPath($twigLoaderDefinition, $dir, $bundle)
  133
+    {
  134
+        $name = $bundle;
  135
+        if ('Bundle' === substr($name, -6)) {
  136
+            $name = substr($name, 0, -6);
  137
+        }
  138
+        $twigLoaderDefinition->addMethodCall('addPath', array($dir, $name));
  139
+    }
  140
+
111 141
     /**
112 142
      * Returns the base path for the XSD files.
113 143
      *
21  src/Symfony/Bundle/TwigBundle/Loader/FilesystemLoader.php
@@ -64,16 +64,19 @@ protected function findTemplate($template)
64 64
         $file = null;
65 65
         $previous = null;
66 66
         try {
67  
-            $template = $this->parser->parse($template);
68  
-            try {
69  
-                $file = $this->locator->locate($template);
70  
-            } catch (\InvalidArgumentException $e) {
71  
-                $previous = $e;
72  
-            }
73  
-        } catch (\Exception $e) {
  67
+            $file = parent::findTemplate($template);
  68
+        } catch (\Twig_Error_Loader $e) {
  69
+            $previous = $e;
  70
+
  71
+            // for BC
74 72
             try {
75  
-                $file = parent::findTemplate($template);
76  
-            } catch (\Twig_Error_Loader $e) {
  73
+                $template = $this->parser->parse($template);
  74
+                try {
  75
+                    $file = $this->locator->locate($template);
  76
+                } catch (\InvalidArgumentException $e) {
  77
+                    $previous = $e;
  78
+                }
  79
+            } catch (\Exception $e) {
77 80
                 $previous = $e;
78 81
             }
79 82
         }
6  src/Symfony/Bundle/TwigBundle/Resources/config/schema/twig-1.0.xsd
@@ -11,7 +11,7 @@
11 11
         <xsd:sequence>
12 12
             <xsd:element name="form" type="form" minOccurs="0" maxOccurs="1" />
13 13
             <xsd:element name="global" type="global" minOccurs="0" maxOccurs="unbounded" />
14  
-            <xsd:element name="path" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
  14
+            <xsd:element name="path" type="path" minOccurs="0" maxOccurs="unbounded" />
15 15
         </xsd:sequence>
16 16
 
17 17
         <xsd:attribute name="auto-reload" type="xsd:string" />
@@ -30,6 +30,10 @@
30 30
         </xsd:choice>
31 31
     </xsd:complexType>
32 32
 
  33
+    <xsd:complexType name="path" mixed="true">
  34
+        <xsd:attribute name="namespace" type="xsd:string" />
  35
+    </xsd:complexType>
  36
+
33 37
     <xsd:complexType name="global" mixed="true">
34 38
         <xsd:attribute name="key" type="xsd:string" use="required" />
35 39
         <xsd:attribute name="type" type="global_type" />
1  ...Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/Resources/TwigBundle/views/layout.html.twig
... ...
@@ -0,0 +1 @@
  1
+This is a layout
1  src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/Resources/views/layout.html.twig
... ...
@@ -0,0 +1 @@
  1
+This is a layout
7  src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/full.php
@@ -18,5 +18,10 @@
18 18
      'charset'             => 'ISO-8859-1',
19 19
      'debug'               => true,
20 20
      'strict_variables'    => true,
21  
-     'paths'               => array('path1', 'path2'),
  21
+     'paths'               => array(
  22
+         'path1',
  23
+         'path2',
  24
+         'namespaced_path1' => 'namespace',
  25
+         'namespaced_path2' => 'namespace',
  26
+      ),
22 27
 ));
2  src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/full.xml
@@ -14,5 +14,7 @@
14 14
         <twig:global key="pi">3.14</twig:global>
15 15
         <twig:path>path1</twig:path>
16 16
         <twig:path>path2</twig:path>
  17
+        <twig:path namespace="namespace">namespaced_path1</twig:path>
  18
+        <twig:path namespace="namespace">namespaced_path2</twig:path>
17 19
     </twig:config>
18 20
 </container>
6  src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/full.yml
@@ -13,4 +13,8 @@ twig:
13 13
     charset:             ISO-8859-1
14 14
     debug:               true
15 15
     strict_variables:    true
16  
-    paths:               [path1, path2]
  16
+    paths:
  17
+        path1: ''
  18
+        path2: ''
  19
+        namespaced_path1: namespace
  20
+        namespaced_path2: namespace
33  src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php
@@ -108,6 +108,37 @@ public function testGlobalsWithDifferentTypesAndValues()
108 108
         }
109 109
     }
110 110
 
  111
+    /**
  112
+     * @dataProvider getFormats
  113
+     */
  114
+    public function testTwigLoaderPaths($format)
  115
+    {
  116
+        $container = $this->createContainer();
  117
+        $container->registerExtension(new TwigExtension());
  118
+        $this->loadFromFile($container, 'full', $format);
  119
+        $this->compileContainer($container);
  120
+
  121
+        $def = $container->getDefinition('twig.loader');
  122
+        $paths = array();
  123
+        foreach ($def->getMethodCalls() as $call) {
  124
+            if ('addPath' === $call[0]) {
  125
+                if (false === strpos($call[1][0], 'Form')) {
  126
+                    $paths[] = $call[1];
  127
+                }
  128
+            }
  129
+        }
  130
+
  131
+        $this->assertEquals(array(
  132
+            array('path1'),
  133
+            array('path2'),
  134
+            array('namespaced_path1', 'namespace'),
  135
+            array('namespaced_path2', 'namespace'),
  136
+            array(__DIR__.'/Fixtures/Resources/TwigBundle/views', 'Twig'),
  137
+            array(realpath(__DIR__.'/../../Resources/views'), 'Twig'),
  138
+            array(__DIR__.'/Fixtures/Resources/views'),
  139
+        ), $paths);
  140
+    }
  141
+
111 142
     public function getFormats()
112 143
     {
113 144
         return array(
@@ -121,8 +152,10 @@ private function createContainer()
121 152
     {
122 153
         $container = new ContainerBuilder(new ParameterBag(array(
123 154
             'kernel.cache_dir' => __DIR__,
  155
+            'kernel.root_dir'  => __DIR__.'/Fixtures',
124 156
             'kernel.charset'   => 'UTF-8',
125 157
             'kernel.debug'     => false,
  158
+            'kernel.bundles'   => array('TwigBundle' => 'Symfony\\Bundle\\TwigBundle\\TwigBundle'),
126 159
         )));
127 160
 
128 161
         return $container;
88  src/Symfony/Bundle/TwigBundle/Tests/Loader/FilesystemLoaderTest.php
@@ -16,59 +16,73 @@
16 16
 use Symfony\Component\Config\FileLocatorInterface;
17 17
 use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference;
18 18
 use Symfony\Component\Templating\TemplateNameParserInterface;
19  
-use InvalidArgumentException;
20 19
 
21 20
 class FilesystemLoaderTest extends TestCase
22 21
 {
23  
-    /** @var FileLocatorInterface */
24  
-    private $locator;
25  
-    /** @var TemplateNameParserInterface */
26  
-    private $parser;
27  
-    /** @var FilesystemLoader */
28  
-    private $loader;
29  
-
30  
-    protected function setUp()
  22
+    public function testGetSource()
31 23
     {
32  
-        parent::setUp();
33  
-
34  
-        $this->locator = $this->getMock('Symfony\Component\Config\FileLocatorInterface');
35  
-        $this->parser = $this->getMock('Symfony\Component\Templating\TemplateNameParserInterface');
36  
-        $this->loader = new FilesystemLoader($this->locator, $this->parser);
37  
-
38  
-        $this->parser->expects($this->once())
39  
-                ->method('parse')
40  
-                ->with('name.format.engine')
41  
-                ->will($this->returnValue(new TemplateReference('', '', 'name', 'format', 'engine')))
  24
+        $parser = $this->getMock('Symfony\Component\Templating\TemplateNameParserInterface');
  25
+        $locator = $this->getMock('Symfony\Component\Config\FileLocatorInterface');
  26
+        $locator
  27
+            ->expects($this->once())
  28
+            ->method('locate')
  29
+            ->will($this->returnValue(__DIR__.'/../DependencyInjection/Fixtures/Resources/views/layout.html.twig'))
42 30
         ;
43  
-    }
  31
+        $loader = new FilesystemLoader($locator, $parser);
  32
+        $loader->addPath(__DIR__.'/../DependencyInjection/Fixtures/Resources/views', 'namespace');
44 33
 
45  
-    protected function tearDown()
46  
-    {
47  
-        parent::tearDown();
  34
+        // Twig-style
  35
+        $this->assertEquals("This is a layout\n", $loader->getSource('@namespace/layout.html.twig'));
48 36
 
49  
-        $this->locator = null;
50  
-        $this->parser = null;
51  
-        $this->loader = null;
  37
+        // Symfony-style
  38
+        $this->assertEquals("This is a layout\n", $loader->getSource('TwigBundle::layout.html.twig'));
52 39
     }
53 40
 
  41
+    /**
  42
+     * @expectedException Twig_Error_Loader
  43
+     */
54 44
     public function testTwigErrorIfLocatorThrowsInvalid()
55 45
     {
56  
-        $this->setExpectedException('Twig_Error_Loader');
57  
-        $invalidException = new InvalidArgumentException('Unable to find template "NonExistent".');
58  
-        $this->locator->expects($this->once())
59  
-                      ->method('locate')
60  
-                      ->will($this->throwException($invalidException));
  46
+        $parser = $this->getMock('Symfony\Component\Templating\TemplateNameParserInterface');
  47
+        $parser
  48
+            ->expects($this->once())
  49
+            ->method('parse')
  50
+            ->with('name.format.engine')
  51
+            ->will($this->returnValue(new TemplateReference('', '', 'name', 'format', 'engine')))
  52
+        ;
61 53
 
62  
-        $this->loader->getCacheKey('name.format.engine');
  54
+        $locator = $this->getMock('Symfony\Component\Config\FileLocatorInterface');
  55
+        $locator
  56
+            ->expects($this->once())
  57
+            ->method('locate')
  58
+            ->will($this->throwException(new \InvalidArgumentException('Unable to find template "NonExistent".')))
  59
+        ;
  60
+
  61
+        $loader = new FilesystemLoader($locator, $parser);
  62
+        $loader->getCacheKey('name.format.engine');
63 63
     }
64 64
 
  65
+    /**
  66
+     * @expectedException Twig_Error_Loader
  67
+     */
65 68
     public function testTwigErrorIfLocatorReturnsFalse()
66 69
     {
67  
-        $this->setExpectedException('Twig_Error_Loader');
68  
-        $this->locator->expects($this->once())
69  
-                      ->method('locate')
70  
-                      ->will($this->returnValue(false));
  70
+        $parser = $this->getMock('Symfony\Component\Templating\TemplateNameParserInterface');
  71
+        $parser
  72
+            ->expects($this->once())
  73
+            ->method('parse')
  74
+            ->with('name.format.engine')
  75
+            ->will($this->returnValue(new TemplateReference('', '', 'name', 'format', 'engine')))
  76
+        ;
  77
+
  78
+        $locator = $this->getMock('Symfony\Component\Config\FileLocatorInterface');
  79
+        $locator
  80
+            ->expects($this->once())
  81
+            ->method('locate')
  82
+            ->will($this->returnValue(false))
  83
+        ;
71 84
 
72  
-        $this->loader->getCacheKey('name.format.engine');
  85
+        $loader = new FilesystemLoader($locator, $parser);
  86
+        $loader->getCacheKey('name.format.engine');
73 87
     }
74 88
 }

0 notes on commit 5c809d8

Please sign in to comment.
Something went wrong with that request. Please try again.