Skip to content

Commit

Permalink
[TwigBundle] added support for Twig namespaced paths (Twig 1.10)
Browse files Browse the repository at this point in the history
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
fabpot committed Oct 3, 2012
1 parent 0bfa86c commit 5c809d8
Show file tree
Hide file tree
Showing 12 changed files with 178 additions and 52 deletions.
6 changes: 6 additions & 0 deletions src/Symfony/Bundle/TwigBundle/CHANGELOG.md
@@ -1,6 +1,12 @@
CHANGELOG
=========

2.2.0
-----

* added automatic registration of namespaced paths for registered bundles
* added support for namespaced paths

2.1.0
-----

Expand Down
Expand Up @@ -126,6 +126,29 @@ private function addTwigOptions(ArrayNodeDefinition $rootNode)
->scalarNode('auto_reload')->end()
->scalarNode('optimizations')->end()
->arrayNode('paths')
->beforeNormalization()
->always()
->then(function ($paths) {
$normalized = array();
foreach ($paths as $path => $namespace) {
if (is_array($namespace)) {
// xml
$path = $namespace['value'];
$namespace = $namespace['namespace'];
}

// path within the default namespace
if (ctype_digit((string) $path)) {
$path = $namespace;
$namespace = null;
}

$normalized[$path] = $namespace;
}

return $normalized;
})
->end()
->prototype('variable')->end()
->end()
->end()
Expand Down
Expand Up @@ -60,12 +60,33 @@ public function load(array $configs, ContainerBuilder $container)
$reflClass = new \ReflectionClass('Symfony\Bridge\Twig\Extension\FormExtension');
$container->getDefinition('twig.loader')->addMethodCall('addPath', array(dirname(dirname($reflClass->getFileName())).'/Resources/views/Form'));

if (!empty($config['paths'])) {
foreach ($config['paths'] as $path) {
$container->getDefinition('twig.loader')->addMethodCall('addPath', array($path));
$twigLoaderDefinition = $container->getDefinition('twig.loader');

// register user-configured paths
foreach ($config['paths'] as $path => $namespace) {
if (!$namespace) {
$twigLoaderDefinition->addMethodCall('addPath', array($path));
} else {
$twigLoaderDefinition->addMethodCall('addPath', array($path, $namespace));
}
}

// register bundles as Twig namespaces
foreach ($container->getParameter('kernel.bundles') as $bundle => $class) {
if (is_dir($dir = $container->getParameter('kernel.root_dir').'/Resources/'.$bundle.'/views')) {
$this->addTwigPath($twigLoaderDefinition, $dir, $bundle);
}

$reflection = new \ReflectionClass($class);
if (is_dir($dir = dirname($reflection->getFilename()).'/Resources/views')) {
$this->addTwigPath($twigLoaderDefinition, $dir, $bundle);
}
}

if (is_dir($dir = $container->getParameter('kernel.root_dir').'/Resources/views')) {
$twigLoaderDefinition->addMethodCall('addPath', array($dir));
}

if (!empty($config['globals'])) {
$def = $container->getDefinition('twig');
foreach ($config['globals'] as $key => $global) {
Expand Down Expand Up @@ -108,6 +129,15 @@ public function load(array $configs, ContainerBuilder $container)
));
}

private function addTwigPath($twigLoaderDefinition, $dir, $bundle)
{
$name = $bundle;
if ('Bundle' === substr($name, -6)) {
$name = substr($name, 0, -6);
}
$twigLoaderDefinition->addMethodCall('addPath', array($dir, $name));
}

/**
* Returns the base path for the XSD files.
*
Expand Down
21 changes: 12 additions & 9 deletions src/Symfony/Bundle/TwigBundle/Loader/FilesystemLoader.php
Expand Up @@ -64,16 +64,19 @@ protected function findTemplate($template)
$file = null;
$previous = null;
try {
$template = $this->parser->parse($template);
try {
$file = $this->locator->locate($template);
} catch (\InvalidArgumentException $e) {
$previous = $e;
}
} catch (\Exception $e) {
$file = parent::findTemplate($template);
} catch (\Twig_Error_Loader $e) {
$previous = $e;

// for BC
try {
$file = parent::findTemplate($template);
} catch (\Twig_Error_Loader $e) {
$template = $this->parser->parse($template);
try {
$file = $this->locator->locate($template);
} catch (\InvalidArgumentException $e) {
$previous = $e;
}
} catch (\Exception $e) {
$previous = $e;
}
}
Expand Down
Expand Up @@ -11,7 +11,7 @@
<xsd:sequence>
<xsd:element name="form" type="form" minOccurs="0" maxOccurs="1" />
<xsd:element name="global" type="global" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="path" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="path" type="path" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>

<xsd:attribute name="auto-reload" type="xsd:string" />
Expand All @@ -30,6 +30,10 @@
</xsd:choice>
</xsd:complexType>

<xsd:complexType name="path" mixed="true">
<xsd:attribute name="namespace" type="xsd:string" />
</xsd:complexType>

<xsd:complexType name="global" mixed="true">
<xsd:attribute name="key" type="xsd:string" use="required" />
<xsd:attribute name="type" type="global_type" />
Expand Down
@@ -0,0 +1 @@
This is a layout
@@ -0,0 +1 @@
This is a layout
Expand Up @@ -18,5 +18,10 @@
'charset' => 'ISO-8859-1',
'debug' => true,
'strict_variables' => true,
'paths' => array('path1', 'path2'),
'paths' => array(
'path1',
'path2',
'namespaced_path1' => 'namespace',
'namespaced_path2' => 'namespace',
),
));
Expand Up @@ -14,5 +14,7 @@
<twig:global key="pi">3.14</twig:global>
<twig:path>path1</twig:path>
<twig:path>path2</twig:path>
<twig:path namespace="namespace">namespaced_path1</twig:path>
<twig:path namespace="namespace">namespaced_path2</twig:path>
</twig:config>
</container>
Expand Up @@ -13,4 +13,8 @@ twig:
charset: ISO-8859-1
debug: true
strict_variables: true
paths: [path1, path2]
paths:
path1: ''
path2: ''
namespaced_path1: namespace
namespaced_path2: namespace
Expand Up @@ -108,6 +108,37 @@ public function testGlobalsWithDifferentTypesAndValues()
}
}

/**
* @dataProvider getFormats
*/
public function testTwigLoaderPaths($format)
{
$container = $this->createContainer();
$container->registerExtension(new TwigExtension());
$this->loadFromFile($container, 'full', $format);
$this->compileContainer($container);

$def = $container->getDefinition('twig.loader');
$paths = array();
foreach ($def->getMethodCalls() as $call) {
if ('addPath' === $call[0]) {
if (false === strpos($call[1][0], 'Form')) {
$paths[] = $call[1];
}
}
}

$this->assertEquals(array(
array('path1'),
array('path2'),
array('namespaced_path1', 'namespace'),
array('namespaced_path2', 'namespace'),
array(__DIR__.'/Fixtures/Resources/TwigBundle/views', 'Twig'),
array(realpath(__DIR__.'/../../Resources/views'), 'Twig'),
array(__DIR__.'/Fixtures/Resources/views'),
), $paths);
}

public function getFormats()
{
return array(
Expand All @@ -121,8 +152,10 @@ private function createContainer()
{
$container = new ContainerBuilder(new ParameterBag(array(
'kernel.cache_dir' => __DIR__,
'kernel.root_dir' => __DIR__.'/Fixtures',
'kernel.charset' => 'UTF-8',
'kernel.debug' => false,
'kernel.bundles' => array('TwigBundle' => 'Symfony\\Bundle\\TwigBundle\\TwigBundle'),
)));

return $container;
Expand Down
88 changes: 51 additions & 37 deletions src/Symfony/Bundle/TwigBundle/Tests/Loader/FilesystemLoaderTest.php
Expand Up @@ -16,59 +16,73 @@
use Symfony\Component\Config\FileLocatorInterface;
use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference;
use Symfony\Component\Templating\TemplateNameParserInterface;
use InvalidArgumentException;

class FilesystemLoaderTest extends TestCase
{
/** @var FileLocatorInterface */
private $locator;
/** @var TemplateNameParserInterface */
private $parser;
/** @var FilesystemLoader */
private $loader;

protected function setUp()
public function testGetSource()
{
parent::setUp();

$this->locator = $this->getMock('Symfony\Component\Config\FileLocatorInterface');
$this->parser = $this->getMock('Symfony\Component\Templating\TemplateNameParserInterface');
$this->loader = new FilesystemLoader($this->locator, $this->parser);

$this->parser->expects($this->once())
->method('parse')
->with('name.format.engine')
->will($this->returnValue(new TemplateReference('', '', 'name', 'format', 'engine')))
$parser = $this->getMock('Symfony\Component\Templating\TemplateNameParserInterface');
$locator = $this->getMock('Symfony\Component\Config\FileLocatorInterface');
$locator
->expects($this->once())
->method('locate')
->will($this->returnValue(__DIR__.'/../DependencyInjection/Fixtures/Resources/views/layout.html.twig'))
;
}
$loader = new FilesystemLoader($locator, $parser);
$loader->addPath(__DIR__.'/../DependencyInjection/Fixtures/Resources/views', 'namespace');

protected function tearDown()
{
parent::tearDown();
// Twig-style
$this->assertEquals("This is a layout\n", $loader->getSource('@namespace/layout.html.twig'));

$this->locator = null;
$this->parser = null;
$this->loader = null;
// Symfony-style
$this->assertEquals("This is a layout\n", $loader->getSource('TwigBundle::layout.html.twig'));
}

/**
* @expectedException Twig_Error_Loader
*/
public function testTwigErrorIfLocatorThrowsInvalid()
{
$this->setExpectedException('Twig_Error_Loader');
$invalidException = new InvalidArgumentException('Unable to find template "NonExistent".');
$this->locator->expects($this->once())
->method('locate')
->will($this->throwException($invalidException));
$parser = $this->getMock('Symfony\Component\Templating\TemplateNameParserInterface');
$parser
->expects($this->once())
->method('parse')
->with('name.format.engine')
->will($this->returnValue(new TemplateReference('', '', 'name', 'format', 'engine')))
;

$this->loader->getCacheKey('name.format.engine');
$locator = $this->getMock('Symfony\Component\Config\FileLocatorInterface');
$locator
->expects($this->once())
->method('locate')
->will($this->throwException(new \InvalidArgumentException('Unable to find template "NonExistent".')))
;

$loader = new FilesystemLoader($locator, $parser);
$loader->getCacheKey('name.format.engine');
}

/**
* @expectedException Twig_Error_Loader
*/
public function testTwigErrorIfLocatorReturnsFalse()
{
$this->setExpectedException('Twig_Error_Loader');
$this->locator->expects($this->once())
->method('locate')
->will($this->returnValue(false));
$parser = $this->getMock('Symfony\Component\Templating\TemplateNameParserInterface');
$parser
->expects($this->once())
->method('parse')
->with('name.format.engine')
->will($this->returnValue(new TemplateReference('', '', 'name', 'format', 'engine')))
;

$locator = $this->getMock('Symfony\Component\Config\FileLocatorInterface');
$locator
->expects($this->once())
->method('locate')
->will($this->returnValue(false))
;

$this->loader->getCacheKey('name.format.engine');
$loader = new FilesystemLoader($locator, $parser);
$loader->getCacheKey('name.format.engine');
}
}

0 comments on commit 5c809d8

Please sign in to comment.