How to render Twig template from database in symfony2
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' }
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")
);
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'));
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
.