How to render Twig template from database in symfony2

35,263

Solution 1

Twig_Loader_String is deprecated and was always designed for internal use anyway. The usage of this loader is strongly discouraged.

From the API doc:

This loader should NEVER be used. It only exists for Twig internal purposes. When using this loader with a cache mechanism, you should know that a new cache key is generated each time a template content "changes" (the cache key being the source code of the template). If you don't want to see your cache grows out of control, you need to take care of clearing the old cache file by yourself.

Also check out this issue: https://github.com/symfony/symfony/issues/10865


The best way I know to load a template from a String source are:

From a controller:

$template = $this->get('twig')->createTemplate('Hello {{ name }}');
$template->render(array('name'=>'World'));

as described here: http://twig.sensiolabs.org/doc/recipes.html#loading-a-template-from-a-string

From a twig template:

{{ include(template_from_string("Hello {{ name }}", {'name' : 'Peter'})) }}

as described here: http://twig.sensiolabs.org/doc/functions/template_from_string.html

Note, that the 'template_from_string' - function is not available by default and needs to be loaded. In symfony you would do this by adding a new service:

# services.yml
services:
    appbundle.twig.extension.string:
        class: Twig_Extension_StringLoader
        tags:
            - { name: 'twig.extension' }

Solution 2

This should work. Replace "Hello {{ name }}" with your template text, and fill the array that is passed into the render function with any variables that you need.

$env = new \Twig_Environment(new \Twig_Loader_String());
echo $env->render(
  "Hello {{ name }}",
  array("name" => "World")
);

Solution 3

Here's a solution that works with Symfony 4 (and possibly older versions as well, although I haven't tested it) and allows you to work with templates stored in the database the same way you would work with templates in the filesystem.

This answer assumes you're using Doctrine, but is relatively easy to adapt if you're using another database library.

Create the Template entity

This is an example class that uses annotations, but you can use whatever configuration method you're already using.

src/Entity/Template.php

<?php
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Table(name="templates")
 * @ORM\Entity
 */
class Template
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(type="string", nullable=false)
     */
    private $filename;

    /**
     * @var string
     *
     * @ORM\Column(type="text", nullable=false)
     */
    private $source;

    /**
     * @var \DateTime
     *
     * @ORM\Column(type="datetime", nullable=false)
     */
    private $last_updated;
}

The bare minimum fields are filename and source, but it's a very good idea to include last_updated or you'll lose the benefits of caching.

Create a DatabaseLoader class

src/Twig/Loader/DatabaseLoader.php

<?php
namespace App\Twig\Loader;

use App\Entity\Template;
use Doctrine\ORM\EntityManagerInterface;
use Twig_Error_Loader;
use Twig_LoaderInterface;
use Twig_Source;

class DatabaseLoader implements Twig_LoaderInterface
{
    protected $repo;

    public function __construct(EntityManagerInterface $em)
    {
        $this->repo = $em->getRepository(Template::class);
    }

    public function getSourceContext($name)
    {
        if (false === $template = $this->getTemplate($name)) {
            throw new Twig_Error_Loader(sprintf('Template "%s" does not exist.', $name));
        }

        return new Twig_Source($template->getSource(), $name);
    }

    public function exists($name)
    {
        return (bool)$this->getTemplate($name);
    }

    public function getCacheKey($name)
    {
        return $name;
    }

    public function isFresh($name, $time)
    {
        if (false === $template = $this->getTemplate($name)) {
            return false;
        }

        return $template->getLastUpdated()->getTimestamp() <= $time;
    }

    /**
     * @param $name
     * @return Template|null
     */
    protected function getTemplate($name)
    {
        return $this->repo->findOneBy(['filename' => $name]);  
    }
}

The class is relatively simple. getTemplate looks up the template filename from the database, and the rest of the methods use getTemplate to implement the interface that Twig needs.

Add the DatabaseLoader to your service config

config/services.yaml

services:
    App\Twig\Loader\DatabaseLoader:
        tags:
        - { name: twig.loader }

Now you can use your database templates in the same way as filesystem templates.

Rendering from a controller:

return $this->render('home.html.twig');

Including from another Twig template (which can be in the database or filesystem):

{{ include('welcome.html.twig') }}

Rendering to a string (where $twig is an instance of Twig\Environment)

$html = $twig->render('email.html.twig')

In each of these cases, Twig will check the database first. If getTemplate in your DatabaseLoader returns null, Twig will then check the filesystem. If the template isn't available in the database or the filesystem, Twig will throw a Twig_Error_Loader.

Solution 4

Clone the native twig service and replace the filesystem loader with the native twig string loader:

<service id="my.twigstring" class="%twig.class%">
    <argument type="service" id="my.twigstring.loader" />
    <argument>%twig.options%</argument>
</service>        
<service id="my.twigstring.loader" class="Twig_Loader_String"></service>

Usage example from within a controller:

$this->get('my.twigstring')->render('Hello {{ name }}', array('name' => 'Fabien'));

Solution 5

As of Twig 1.10, the Twig Engine doesn't support rendering strings. But there is a bundle available which adds this behavior called TwigstringBundle.

It adds the $this->get('twigstring') service wich you can use to render your strings.

(As September '19, the current version of Twig is 2.X, and version 3 is around the corner; so this is only applies to very old versions of Twig).

Share:
35,263
Karol F
Author by

Karol F

Updated on February 07, 2022

Comments

  • Karol F
    Karol F over 2 years

    I'm working on application written in symfony2 and I want to send email after some action/event... the problem is, that the users can define something like "email templates" which are stores in db like simple string, for example: "This is some email from {{ user }}" and I need to render body for that email which should use that template...

    In symfony documentation from this link: https://symfony.com/doc/2.0/cookbook/email/email.html#sending-emails the method for render view is $this->renderView and it expects the path to file such as "bundle:controller:file.html.twig", but my template is simple string from database...

    How can I render it?

  • Grizerg
    Grizerg over 11 years
    I agree, a concise solution without using a side bundle.
  • Florent
    Florent over 11 years
    That's not related to the question. Please include an example using Twig.
  • Gigala
    Gigala about 11 years
    I think this is the best solution and the best answer :)
  • ncatnow
    ncatnow about 11 years
    This works. I've noticed however that custom twig extensions are not enabled by default. SO question here: stackoverflow.com/questions/16383522/…
  • ftassi
    ftassi almost 11 years
    As from Symfony2.2 you can simply add a new loader defining the service and tagging it with twig.loader tag. This way twig will use the Twig_Loader_Chain that will try to load template using each defined loader. Have a look at symfony.com/doc/current/reference/dic_tags.html#twig-loader If you need to define priority for twig loaders have a look at DifaneTwigDatabaseBundle, I've made some changes in our fork at github.com/webgriffe/DifaneTwigDatabaseBundle (not pushed back to original repo because I still need to review the code)
  • Quentin
    Quentin almost 10 years
    This solution is not suitable for everyone: it doesn't load twig extensions, doesn't cache compiled templates (slower), and doesn't allow any kind of template reference: {% extends %} or {% include %}.
  • Parag Tyagi
    Parag Tyagi over 9 years
    Sweet and simple. Time saver. Life saver. +1
  • Henry
    Henry almost 9 years
    I've read in the comments for Twig_Loader_String (deprecated) that it will fill the cache up...
  • Andrew Zhilin
    Andrew Zhilin over 7 years
    In my case I wasn't able to send variables in template_from_string (probably, it have the same scope?), so I used {% set name = 'Peter' %} before inlude
  • PhoneixS
    PhoneixS about 5 years
    For the same but with PDO, you could see twig.symfony.com/doc/2.x/…
  • yivi
    yivi over 4 years
    For those wanting to implement this, be forewarned that will make doing cache:clear and cache:warmup DB dependant... Which may break your image building processes if you are deploying through docker or the like...
  • yivi
    yivi over 4 years
    You can always change your post-install script to be cache:clear --no-warmup, but this is something to be aware of...
  • Nico Haase
    Nico Haase about 4 years
    Please add all information to your answer instead of linking to external sites only
  • Rene Arias
    Rene Arias almost 3 years
    you must be now to control cache
  • jirarium
    jirarium over 2 years