Skip to content
Advertisement

Is it possible to make global macros in Twig 3?

I want to upgrade my Twig from very old version (2.x or even 1.x) to 3.3. In old version macros imported in top level template are available in all extened and included templates. And I have a bunch of templates where I need my macros (100+ and many embed blocks). I don’t want to import macros manually in evety template.

So, according to similar question Twig auto import macros by environment I tried to implement suggested solution, but it doesn’t work. Actually I tried this:

$tpl = $this->Twig->load('inc/function.twig');
$this->Twig->addGlobal('fnc', $tpl);

I also tried this:

$tpl = $this->Twig->load('inc/function.twig');
$this->Twig->addGlobal('fnc', $tpl->unwrap());

but I have same result. In templates fnc is defined but it’s a Template object and I can not access macros. I get a fatal error when I try to do it:

Fatal error: Uncaught TwigErrorRuntimeError: Accessing TwigTemplate attributes is forbidden

As I understand, in Twig 3 you can not just include macros using addGlobal.

Old Twig was added to our repository (was not ignored) and we probably will add to repository new Twig too, so it’s possible to modify Twig’s source code.

UPD: When I try just to addGlobal my template with macros I get

Fatal error: Uncaught LogicException: Unable to add global "fnc" as the runtime or the extensions have already been initialized.

I’ve solved this problem using this solution (I’ve extended Environment class).

Advertisement

Answer

During some testing I found out you can still call the “functions” defined inside a macro with pure PHP

<?php
    $wrapper = $twig->load('macro.html');
    $template = $wrapper->unwrap();
    echo $template->macro_Foo().''; //return a TwigMarkup

With this in place you could write a wrapper around the macro and try to auto load them in a container.


First off we need an extension to enable and access the container

<?php
    class MacroWrapperExtension extends TwigExtensionAbstractExtension
    {
        public function getFunctions()
        {
            return [
                new TwigTwigFunction('macro', [$this, 'macro'], ['needs_environment' => true,]),
            ];
        }

        protected $container = null;

        public function macro(TwigEnvironment $twig, $template) {
            return $this->getContainer($twig)->get($template);
        }

        private function getContainer(TwigEnvironment $twig) {
            if ($this->container === null) $this->container = new MacroWrapperContainer($twig);
            return $this->container;
        }

    }

Next the container itself. The container is responsible to load and store/save the (auto loaded) macros in the memory. The code will try to locate and load any file in the map macros in your view folder.

template
|--- index.html
|--- macros
|------- test.html
|
<?php
    class MacroWrapperContainer {
        const FOLDER = 'macros';

        protected $twig = null;
        protected $macros = [];

        public function __construct(TwigEnvironment $twig) {
            $this->setTwig($twig)
                 ->load();
        }

        public function get($macro) {
            return $this->macros[$macro] ?? null;
        }

        protected function load() {
            foreach($this->getTwig()->getLoader()->getPaths() as $path) {
                if (!is_dir($path.'/'.self::FOLDER))  continue;
                $this->loadMacros($path.'/'.self::FOLDER);
            }
        }

        protected function loadMacros($path) {
            $files = scandir($path);
            foreach($files as $file) if ($this->isTemplate($file)) $this->loadMacro($file);
        }

        protected function loadMacro($file) {
            $name = pathinfo($file, PATHINFO_FILENAME);
            if (!isset($this->macros[$name])) $this->macros[$name] = new MacroWrapper($this->getTwig()->load(self::FOLDER.'/'.$file));
        }

        protected function isTemplate($file) {
            return in_array(pathinfo($file, PATHINFO_EXTENSION), [ 'html', 'twig', ]);
        }

        protected function setTwig(TwigEnvironment $twig) {
            $this->twig = $twig;
            return $this;
        }

        protected function getTwig() {
            return $this->twig;
        }

        public function __call($method_name, $args) {
            return $this->get($method_name);
        }
    }

Last off we need to mimic the behavior I’ve posted in the beginning of the question. So lets create a wrapper around the macro template which will be responsible to call the actual functions inside the macro.

As seen the functions inside a macro get prefixed with macro_, so just let auto-prefix every call made to the macro wrapper with macro_

<?php
    class MacroWrapper {
        protected $template = null;

        public function __construct(TwigTemplateWrapper $template_wrapper) {
            $this->template = $template_wrapper->unwrap();
        }

        public function __call($method_name, $args){
            return $this->template->{'macro_'.$method_name}(...$args);
        }
    }

Now inject the extension into twig

$twig->addExtension(new MacroWrapperExtension());

This will enable the function macro inside every template, which lets us access any macro file inside the macros folder

{{ macro('test').hello('foo') }}

{{ macro('test').bar('foo', 'bar', 'foobar') }}
User contributions licensed under: CC BY-SA
9 People found this is helpful
Advertisement