1
votes

I have problems with adding Twig extensions.

I have Bundle controllers extending custom BaseController class:

class DefaultController extends BaseController

And there's my BaseController class (only part of it).

class BaseController extends Controller {

public function setContainer(\Symfony\Component\DependencyInjection\ContainerInterface $container = null)
{
    parent::setContainer($container);
    $this->onContainerSet();
}

public function onContainerSet()
{
  // many other tasks

    $this->get('twig')->addExtension(new \Twig_Extension_StringLoader());

    $this->get('twig.loader')->addPath('../app');

    $function = new \Twig_SimpleFunction('stars', function ($number, $maximum_stars = 5) {
        $this->get('twig')->addGlobal('star_number',sprintf("%.1f",$number));
        $this->get('twig')->addGlobal('star_max',$maximum_stars);

        $full_stars = floor($number);
        $half_stars = ($number - $full_stars) * 2;
        $empty_stars = $maximum_stars - $full_stars - $half_stars;

        $this->get('twig')->addGlobal('full_stars_number',$full_stars);
        $this->get('twig')->addGlobal('half_stars_number',$half_stars);
        $this->get('twig')->addGlobal('empty_stars_number',$empty_stars);


        echo $this->renderView(
            'views/stars.html.twig'
        );;
    });


    $function2 = new \Twig_SimpleFunction('inurl', function ($anchor, $code) {
        echo '<a href="'.$this->internalLinks[$code].'">'.$anchor."</a>";

    });


    $this->get('twig')->addFunction($function);
    $this->get('twig')->addFunction($function2);

   }
}

The problem:

When I clear cache directory I have first message:

CRITICAL - Uncaught PHP Exception LogicException: "Unable to register extension "string_loader" as extensions have already been initialized." at ...\vendor\twig\twig\lib\Twig\Environment.php line 660 Context: {"exception":"Object(LogicException)"}

But when I reload page (cache folder is already created) it works fine (no exception).

However if I comment line:

// $this->get('twig')->addExtension(new \Twig_Extension_StringLoader());

and clear cache directory I have exception:

CRITICAL - Uncaught PHP Exception LogicException: "Unable to add function "stars" as extensions have already been initialized." at ...\vendor\twig\twig\lib\Twig\Environment.php line 946 Context: {"exception":"Object(LogicException)"}

So it seems that when cache directory doesn't exist from some reason adding any Twig extensions doesn't work (extensions have already been initialized) as I would like but when cache directory is already created everything works fine.

Question - how to solve it in the simplest way?

2
Why won't you load extension via service?Bartek
I could try but I don't know how to do this. I tried with Twig_Extension_StringLoader a moment ago but it didn't workMarcin Nabiałek
Why would you register twig extension in controller? Docs: symfony.com/doc/current/cookbook/templating/twig_extension.htmlIgor Pantović
First I don't know how to enable Twig_Extension_StringLoader that way. And another issue I don't know how to pass extra data to Estension as I have $this->internalLinks in $function2Marcin Nabiałek

2 Answers

4
votes

Create your class in YourBundle\Twig

class YourExtension extends \Twig_Extension
{

    /**
     * @var Router
     */
    protected $router;

    function __construct(Router $router)
    {
        $this->router = $router;
    }

    /**
     * @return array
     */
    public function getFilters()
    {
        return [
            new \Twig_SimpleFilter('my_filter', [$this, 'myFilter'], ['is_safe' => ['html']]),
        ];
    }

    /**
     * @return string
     */
    public function myFilter(User $user)
    {
        return 'FILTERED: ' . $user->getName();
    }

    /**
     * @return string
     */
    public function getName()
    {
        return 'my_filter_extension';
    }

}

Then, register your extension as a service: ( in this case I inject router as an argument )

yourbundle.twig.my_filter_extension:
    class: Acme\YourBundle\Twig\YourExtension
    arguments: [@router]
    tags:
        - { name: twig.extension }

If you want to enable Twig_Extension_StringLoader, add to your services:

yourbundle.twig.extension.loader:
    class: Twig_Extension_StringLoader
    tags:
        - { name: 'twig.extension' }

Twig_Extension_StringLoader is not loaded by default.

2
votes

What I finally did to achieve result (maybe someone will have similar problem in the future):

In config.yml I've added:

services:
    yourbundle.twig.extension.loader:
        class: Twig_Extension_StringLoader
        tags:
            - { name: 'twig.extension' }

    yourbundle.twig.stars_extension:
        class: Mnab\Twig\Stars
        tags:
            - { name: 'twig.extension' }

    yourbundle.twig.inurl_extension:
        class: Mnab\Twig\InternalUrl
        tags:
            - { name: 'twig.extension' }

in my BaseController I only left from question code:

    $this->get('twig.loader')->addPath('../app');

but also added:

    $this->get('twig')->addGlobal('internal_links',$this->internalLinks);

to use it in Twig extension

And I've create 2 classes:

<?php
//InternalUrl.php
namespace Mnab\Twig;

use Symfony\Component\DependencyInjection\ContainerInterface;

class InternalUrl extends \Twig_Extension {


    public function getFunctions()
    {
        return array(
            new \Twig_SimpleFunction('inurl', array($this, 'inUrlFunction'), array('needs_environment' => true, 'is_safe' => array('html'))),
        );
    }


    public function inUrlFunction(\Twig_Environment $env, $anchor, $code)
    {
        return '<a href="'.$env->getGlobals()['internal_links'][$code].'">'.$anchor."</a>";
    }


    public function getName()
    {
        return 'inurl_extension';
    }
} 

and

<?php
// Stars.php

namespace Mnab\Twig;


class Stars extends \Twig_Extension
{

    public function getFunctions()
    {
        return array(
            new \Twig_SimpleFunction('stars', array($this, 'starsFunction'), array('needs_environment' => true, 'is_safe' => array('html'))),
        );
    }

    public function starsFunction(\Twig_Environment $env, $number, $maximum_stars = 5)
    {


        $env->addGlobal('star_number',sprintf("%.1f",$number));
        $env->addGlobal('star_max',$maximum_stars);

        $full_stars = floor($number);
        $half_stars = ($number - $full_stars) * 2;
        $empty_stars = $maximum_stars - $full_stars - $half_stars;

        $env->addGlobal('full_stars_number',$full_stars);
        $env->addGlobal('half_stars_number',$half_stars);
        $env->addGlobal('empty_stars_number',$empty_stars);


        return $env->render(
            'views/stars.html.twig'
        );

    }


    public function getName()
    {
        return 'stars_extension';
    }
} 

Now it seems to work regardless of cache is created or not. So it seems to better register services when you want to use Twig Extensions than registering Extensions in Controller.