LaKrue / TwigstringBundle

This Symfony2 Bundle adds the possibillity to render strings instead of files with the Symfony native Twig templateing engine.
20 stars 18 forks source link

Bundle not loading Twig extensions #5

Open EvanK opened 12 years ago

EvanK commented 12 years ago

I have filters being loaded from both the official Text twig extension and a custom extension I've written, both configured via my config.yml:

services:
    text.twig.extension:
         class: Twig_Extensions_Extension_Text
         tags: [{ name: twig.extension }]
    custom_filter.twig.extension:
        class: Project\Bundle\ExampleBundle\Extension\CustomFilterExtension
        tags: [{ name: twig.extension }]

They are being used in normal twig templates without issue:

{% set myvar = "Lorem ipsum dolor sit amet, consectetur adipiscing elit." %}
{{ myvar|truncate(10) }}
{# prints "Lorem ipsu..." #}

However, trying to use them with the TwigstringBundle throws a Twig_Error_Syntax exception with a message of The filter "truncate" does not exist:

$this->get('twigstring')->render('{{ myvar|truncate(10) }}', array('myvar' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'));
EvanK commented 12 years ago

So it looks like the reason for this is that it doesn't use the existing twig service in symfony, it creates its own service presumably so it can use its own loader class of LK\TwigstringBundle\Loader\String.

While an understandable limitation, is there a way to load extensions specifically for this bundle instead of (or in addition to) the twig engine that symfony uses for template rendering?

jrmyio commented 12 years ago

+1 on this.

The path() extension doesn't seem to work either.

toooni commented 12 years ago

any solutions there? workarounds?

Critical-Impact commented 12 years ago

+1 to this as well, being able to use path and url inside templates would be incredibly useful. For now I'm going to have to generate the route beforehand and pass it in as a variable.

toooni commented 12 years ago

i made a simple but dirty workaround for this. I don't use the TwigstringBundle anymore and use a own twig loader instead. (until there is a symfony solution to use multiple twig loaders in a project). If a template with the ending .twig is submitted, it uses the FilesystemLoader and if not.. it just uses the string.

### loader

namespace Project\Bundle\BackendBundle\Twig\Loader;

use Symfony\Bundle\TwigBundle\Loader\FilesystemLoader;

class Loader extends FilesystemLoader
{
    public function getSource($name){
        $ext = pathinfo($name, PATHINFO_EXTENSION);
        if($ext == 'twig'){
            return parent::getSource($name);
        }else{
            return $name;
        }

    }

    public function getCacheKey($name)
    {
        $ext = pathinfo($name, PATHINFO_EXTENSION);
        if($ext == 'twig'){
            return parent::getCacheKey($name);
        }else{
            return $name;
        }
    }

    public function isFresh($name, $time)
    {
        $ext = pathinfo($name, PATHINFO_EXTENSION);
        if($ext == 'twig'){
            return parent::isFresh($name, $time);
        }else{
            return true;
        }
    }
}

### service
        <service id="twig.loader" class="Project\Bundle\BackendBundle\Twig\Loader\Loader">
            <argument type="service" id="templating.locator" />
            <argument type="service" id="templating.name_parser" />
        </service>

a problem is, that you have to add the path of the symfony brige resources by yourself:

### Weekend4two\Bundle\BackendBundle\DependencyInjection\ProjectBackendExtension.php
$container->getDefinition('twig.loader')->addMethodCall('addPath', array(__DIR__.'/../../../../../vendor/symfony/src/Symfony/Bridge/Twig/Resources/views/Form'));

i know it's not beautiful.. but as i said, when there is a solution in symfony, you can just switch..

hsarwar commented 12 years ago

The bundle does not load any of the twig extensions...simply copying the config's of the default Symfony twig bundle doesn't work as the bundle does not support the adding of extensions via tags (which is what the default Symfony twig bundle is using).

So first we need to make the bundle support tags; to do that add the following class to the bundle

\LK\TwigstringBundle\DependencyInjection\Compiler\TwigEnvironmentPass.php:

<?php
namespace LK\TwigstringBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;

class TwigEnvironmentPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if (false === $container->hasDefinition('twig2')) {
            return;
        }

        $definition = $container->getDefinition('twig2');

        // Extensions must always be registered before everything else.
        // For instance, global variable definitions must be registered
        // afterward. If not, the globals from the extensions will never
        // be registered.
        $calls = $definition->getMethodCalls();
        $definition->setMethodCalls(array());
        foreach ($container->findTaggedServiceIds('twig2.extension') as $id => $attributes) {
            $definition->addMethodCall('addExtension', array(new Reference($id)));
        }
        $definition->setMethodCalls(array_merge($definition->getMethodCalls(), $calls));
    }
}

This class is a copy of the symfony class at https://github.com/symfony/TwigBundle/blob/master/DependencyInjection/Compiler/TwigEnvironmentPass.php, with just the tags renamed to 'twig2.extension' instead.

Now we need to add this class as a compiler pass:

\LK\TwigstringBundle\LKTwigstringBundle.php:

<?php
namespace LK\TwigstringBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class LKTwigstringBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        parent::build($container);

        $container->addCompilerPass(new DependencyInjection\Compiler\TwigEnvironmentPass());
    }
}

Once this is done just add the extensions you need at \LK\TwigstringBundle\Resources\config\services.xml.

eg. I needed the routing extension myself so here is my current services.xml:

<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <parameters>
        <parameter key="twig2.class">Twig_Environment</parameter>
        <parameter key="twig2.loader.class">LK\TwigstringBundle\Loader\String</parameter>
        <parameter key="templating.engine.twig2.class">Symfony\Bundle\TwigBundle\TwigEngine</parameter>
        <parameter key="twig2.extension.routing.class">Symfony\Bridge\Twig\Extension\RoutingExtension</parameter>
    </parameters>

    <services>
        <service id="twig2" class="%twig2.class%" public="true">
            <argument type="service" id="twig2.loader" />
            <argument>%twig.options%</argument>
        </service>

        <service id="twig2.loader" class="%twig2.loader.class%" public="false">
            <argument type="service" id="templating.locator" />
            <argument type="service" id="templating.name_parser" />
        </service>

        <service id="templating.engine.twig2" class="%templating.engine.twig2.class%" public="false">
            <argument type="service" id="twig2" />
            <argument type="service" id="templating.name_parser" />
            <argument type="service" id="templating.globals" />
        </service>

        <service id="twig2.extension.routing" class="%twig2.extension.routing.class%" public="false">
            <tag name="twig2.extension" />
            <argument type="service" id="router" />
        </service>

    </services>

</container>

After the doing the above I can successfully call the 'path' function in my 'string' twig templates. (More about tagging here: http://symfony.com/doc/current/cookbook/service_container/tags.html)

You can find the other extensions that come with the default symfony install here https://github.com/symfony/TwigBundle/blob/master/Resources/config/twig.xml

While the above worked for me, after looking at the default twig bundle which lots of stuff like a cache, cache warmer already built in (and I'm not sure if those would work with this bundle without major additions) I'm thinking it may be a better just to write the 'string' twig content to a temporary file in the cache, and just the use the default twig service on that file...

vincentpazeller commented 11 years ago

Thank you very much hsarwar, this worked fine for me :). However, the best would be that Twigstring bundle offers some more functionalities related to filters, functions, etc., e.g.:

For instance, I use twigstring to handle emails. Each email has a template and several models (one for each language). I want to protect models from any filters/function misusage...

Would be nice ^^