Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The $context arg in render don't preserve the "internal context" in a double call of render #128

Open
ivan-redooc opened this issue Jan 29, 2021 · 6 comments

Comments

@ivan-redooc
Copy link

ivan-redooc commented Jan 29, 2021

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
@ivan-redooc
Copy link
Author

@samdark I think I can help.
Already have an idea, I can share if you like.

@samdark
Copy link
Member

samdark commented Feb 3, 2021

Yes, please.

@ivan-redooc
Copy link
Author

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);
    }

@developedsoftware
Copy link

I am having the exact same issue. Did you ever find a solution?

@developedsoftware
Copy link

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?

@developedsoftware
Copy link

$this->env->getLoader()->addPath(dirname($this->getSourceContext()->getPath()));

This one line inside loadTemplate() seems to do the trick ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants