The $context arg in render don't preserve the "internal context" in a double call of render
ivan-redooc opened this issue · 6 comments
Using a "render" inside a model break the ability of Twig to include afterwards includes
What steps will reproduce the problem?
File structure
models
User.php
ViewContext.php
controllers
AlfaController.php
views
alfa
index.twig
footer.twig
templates
profile.twig
Files
AlfaController.php
class AlfaController{
funtion action index(){
$user=new User();
return $this->render("index.twig",['user'=>$user])
}
}
user.php
class User{
public function getProfile(){
$context = new ViewContext([
'viewPath' => "@app/views/templates",
]);
return \Yii::$app->getView()->render("profile.twig",[],$context);
}
}
ViewContext.php
class ViewContext extends BaseObject implements ViewContextInterface
{
public $viewPath;
public function getViewPath ()
{
return \Yii::getAlias($this->viewPath);
}
}
index.twig
<html>
<body>
{# here the second render #}
{{user.profile}}
{# the include will fail #}
{{include "footer.twig"}}
</body>
</html>
profile.twig
{# nothing special here #}
<div>
I'm a user
</div>
footer.twig
{# nothing special here #}
<div>
<hr>
</div>
What's expected?
A render like this
<html>
<body>
<div>
I'm a user
</div>
<div>
<hr>
</div>
</body>
</html>
What do you get instead?
Error:
\Twig\Error\LoaderError
Message:
Unable to find template "footer.twig" (looked into: frontend/views/templates, frontend/views).
Throwing point:
file: vendor/twig/twig/src/Loader/FilesystemLoader.php
line: 227
Additional info
In funtion render
of vendor/yiisoft/yii2-twig/src/ViewRenderer.php
a new FilesystemLoader was set inside the $this->twig object, so the next call (by the include) can't find the TWIG in a different path ( in this case frontend/views/templates
):
public function render($view, $file, $params)
{
$this->twig->addGlobal('this', $view);
$loader = new FilesystemLoader(dirname($file));
if ($view instanceof View) {
$this->addFallbackPaths($loader, $view->theme);
}
$this->addAliases($loader, Yii::$aliases);
$this->twig->setLoader($loader);
// Change lexer syntax (must be set after other settings)
if (!empty($this->lexerOptions)) {
$this->setLexerOptions($this->lexerOptions);
}
return $this->twig->render(pathinfo($file, PATHINFO_BASENAME), $params);
}
The $context is not involved in this business-logic.
A possible solution (I'm adopting) is to define in main.php a second view with twig render in $app
Q | A |
---|---|
Yii version | 2.0.38 |
Yii Twig version | 2.4.0 |
Twig version | v3.0.5 |
PHP version | 7.3 |
Operating system | Ubuntu |
@samdark I think I can help.
Already have an idea, I can share if you like.
Yes, please.
Because the core of ViewContextInterface is the function getViewPath()
we can use it as index of array of FilesystemLoader
.
So my idea (still to test) is:
/**
* @var FilesystemLoader[]
* @since
*/
protected $loaders=[];
//....
public function render($view, $file, $params)
{
$this->twig->addGlobal('this', $view);
if(isset($this->loaders[$view->context->getViewPath()])) {
// I reuse if already created
$loader = $this->loaders[$view->context->getViewPath()];
} else {
// just one time
$loader = new FilesystemLoader(dirname($file));
if ($view instanceof View) {
$this->addFallbackPaths($loader, $view->theme);
}
$this->addAliases($loader, Yii::$aliases);
}
$this->twig->setLoader($loader);
// Change lexer syntax (must be set after other settings)
if (!empty($this->lexerOptions)) {
$this->setLexerOptions($this->lexerOptions);
}
return $this->twig->render(pathinfo($file, PATHINFO_BASENAME), $params);
}
I am having the exact same issue. Did you ever find a solution?
I think I may have solved this by altering https://github.com/twigphp/Twig/tree/3.x/src/Template.php
Whenever we call loadTemplate() I am adding the path of the currently loading template to the list of paths to check
Before
protected function loadTemplate($template, $templateName = null, $line = null, $index = null)
{
try {
if (\is_array($template)) {
return $this->env->resolveTemplate($template);
}
if ($template instanceof self || $template instanceof TemplateWrapper) {
return $template;
}
if ($template === $this->getTemplateName()) {
$class = static::class;
if (false !== $pos = strrpos($class, '___', -1)) {
$class = substr($class, 0, $pos);
}
} else {
$class = $this->env->getTemplateClass($template);
}
return $this->env->loadTemplate($class, $template, $index);
} catch (Error $e) {
if (!$e->getSourceContext()) {
$e->setSourceContext($templateName ? new Source('', $templateName) : $this->getSourceContext());
}
if ($e->getTemplateLine() > 0) {
throw $e;
}
if (!$line) {
$e->guess();
} else {
$e->setTemplateLine($line);
}
throw $e;
}
}
After (added 2 lines of code after the try block)
protected function loadTemplate($template, $templateName = null, $line = null, $index = null)
{
try {
$source = $this->getSourceContext();
$this->env->getLoader()->addPath(dirname($source->getPath()));
if (\is_array($template)) {
return $this->env->resolveTemplate($template);
}
if ($template instanceof self || $template instanceof TemplateWrapper) {
return $template;
}
if ($template === $this->getTemplateName()) {
$class = static::class;
if (false !== $pos = strrpos($class, '___', -1)) {
$class = substr($class, 0, $pos);
}
} else {
$class = $this->env->getTemplateClass($template);
}
return $this->env->loadTemplate($class, $template, $index);
} catch (Error $e) {
if (!$e->getSourceContext()) {
$e->setSourceContext($templateName ? new Source('', $templateName) : $this->getSourceContext());
}
if ($e->getTemplateLine() > 0) {
throw $e;
}
if (!$line) {
$e->guess();
} else {
$e->setTemplateLine($line);
}
throw $e;
}
}
Should I open an issue upstream? Or can we override that functionality from within the yii2-twig implementation of twig?
$this->env->getLoader()->addPath(dirname($this->getSourceContext()->getPath()));
This one line inside loadTemplate() seems to do the trick ;)