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 Twig\Error\RuntimeError: Accessing \Twig\Template 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).
CodePudding user response:
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 \Twig\Markup
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 \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;
}
}
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(\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);
}
}
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(\Twig\TemplateWrapper $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') }}