是否可以在 Twig 3 中制作全局宏?

Is it possible to make global macros in Twig 3?

我想将我的 Twig 从非常旧的版本(2.x 甚至 1.x)升级到3.3。在旧版本中,在顶级模板中导入的宏在所有扩展和包含的模板中都可用。我有一堆模板需要我的宏(100 多个和许多 embed 块)。我不想在每个模板中手动导入宏。

所以,根据类似的问题,我尝试实施建议的解决方案,但它不起作用。 其实我试过这个:

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

我也试过这个:

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

但我得到了相同的结果。在模板中定义了 fnc 但它是一个模板对象,我无法访问宏。当我尝试这样做时出现致命错误:

Fatal error: Uncaught Twig\Error\RuntimeError: Accessing \Twig\Template attributes is forbidden

据我了解,在 Twig 3 中,您不能仅使用 addGlobal.

包含宏

旧的 Twig 已添加到我们的存储库(未被忽略),我们可能也会将新的 Twig 添加到存储库,因此可以修改 Twig 的源代码。

更新: 当我尝试使用宏 addGlobal 我的模板时,我得到

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

我已经使用 this solution 解决了这个问题(我扩展了 Environment class)。

在一些测试中,我发现您仍然可以使用纯 PHP

调用宏中定义的“函数”
<?php
    $wrapper = $twig->load('macro.html');
    $template = $wrapper->unwrap();
    echo $template->macro_Foo().''; //return a \Twig\Markup

有了这个,您可以围绕宏编写一个包装器,并尝试将它们自动加载到容器中。


首先我们需要一个扩展来启用和访问容器

<?php
    class MacroWrapperExtension extends \Twig\Extension\AbstractExtension
    {
        public function getFunctions()
        {
            return [
                new \Twig\TwigFunction('macro', [$this, 'macro'], ['needs_environment' => true,]),
            ];
        }

        protected $container = null;

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

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

    }

接下来是容器本身。容器负责加载和 store/save 内存中的(自动加载)宏。该代码将尝试在您的视图文件夹中定位并加载地图宏中的任何文件。

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

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

        public function __construct(\Twig\Environment $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(\Twig\Environment $twig) {
            $this->twig = $twig;
            return $this;
        }

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

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

最后我们需要模仿我在问题开头发布的行为。因此,让我们围绕宏模板创建一个包装器,它将负责调用宏内的实际函数。

正如所见,宏中的函数以 macro_ 为前缀,因此只需让 auto-prefix 对宏包装器的每次调用都使用 macro_

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

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

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

现在将扩展注入 twig

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

这将在每个模板中启用功能 macro,这让我们可以访问宏文件夹中的任何宏文件

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

{{ macro('test').bar('foo', 'bar', 'foobar') }}