Skip to content

Rendering template twice causes encore_entry_link_tags() to output nothing second time #73

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

Open
andyexeter opened this issue Jul 9, 2019 · 8 comments

Comments

@andyexeter
Copy link

andyexeter commented Jul 9, 2019

When calling encore_entry_link_tags() in a Twig template, the underlying PHP code has some safe-guarding in place to ensure the same link tag isn't outputted twice:

// make sure to not return the same file multiple times
$entryFiles = $entryData[$key];
$newFiles = array_values(array_diff($entryFiles, $this->returnedFiles));
$this->returnedFiles = array_merge($this->returnedFiles, $newFiles);
return $newFiles;

I'm not entirely sure of the rationale behind this, but it causes an issue when encore_entry_link_tags() is called twice in the same request.

This issue came to light when investigating why an order confirmation page of a website wasn't outputting a stylesheet's link tag. After some debugging I figured out it was because the controller for this page sends a confirmation email with an attached PDF which is rendered via HTML/Twig.

The Twig template for the PDF called encore_entry_link_tags(), so when the confirmation page was loaded and encore_entry_link_tags() was called a second time, nothing was returned.

I have worked around this by calling the reset() method of the EntrypointLookup object right after the confirmation email is sent but it feels like a bit of a hack.

Why does the "make sure to not return the same file multiple times" code need to be there? Can it be removed, or can the issue it solves be solved in a different way?

Relevant Slack discussion: https://symfony-devs.slack.com/archives/C5VHNHY11/p1562584991057800

@Lyrkan
Copy link
Contributor

Lyrkan commented Jul 12, 2019

Hey @andyexeter,

Why does the "make sure to not return the same file multiple times" code need to be there?

That's because entrypoints can share some chunks.

For instance if you have app.css, admin.css and app~admin.css (common chunk) you may also want to call {{ encore_entry_link_tags('app') }} and {{ encore_entry_link_tags('admin') }} on the same page.
If that check didn't exist you'd then end-up with the link tag for app~admin.css being added multiple times to your page (the same thing applies to encore_entry_script_tags).

Can it be removed, or can the issue it solves be solved in a different way?

It's a tricky topic... that behavior also causes issues in some cases such as error pages generation since that can happen after you already started processing your main template (see #21)

@weaverryan
Copy link
Member

What we “kinda” want to do is making this “memory” this has almost tied to each Twig “rendering cycle”. Like, each time you render Twig, you get a fresh “memory/cache” that’s shared by all rendered sub-templates. But I’m not sure if knowing such a thing is even possible.

@andyexeter
Copy link
Author

I just ran into this issue again, with error pages this time. It happened because a RuntimeError exception was thrown within a template after the stylesheets were rendered.

FWIW I have implemented a fix for this particular scenario by adding a kernel.exception event listener and resetting within that:

namespace App\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupCollectionInterface;

class ExceptionListener implements EventSubscriberInterface
{
    private $entrypointLookupCollection;

    public function __construct(EntrypointLookupCollectionInterface $entrypointLookupCollection)
    {
        $this->entrypointLookupCollection = $entrypointLookupCollection;
    }

    public function onKernelException(ExceptionEvent $event)
    {
        if ($event->getException() instanceof \Twig\Error\RuntimeError) {
            $this->entrypointLookupCollection->getEntrypointLookup()->reset();
        }
    }

    public static function getSubscribedEvents()
    {
        return [
            KernelEvents::EXCEPTION => 'onKernelException',
        ];
    }
}

weaverryan added a commit that referenced this issue Jan 31, 2020
…(tbmatuka)

This PR was squashed before being merged into the master branch (closes #74).

Discussion
----------

Reset default EntrypointLookup on exception to fix #21 and #73

Since @ckrack seems to be unavailable to continue working on #21, I figured this would be faster :)

I don't like having `_default` hardcoded in the listener, but I see no other options right now.
I thought about adding a `resetAll()` method to EntrypointLookupCollection, but there were a couple of issues with that:
1. It would either be a BC break if I added it to the interface, or an important method that isn't interfaced and I don't really like either of those.
2. I couldn't even implement it because the container that we get in the collection only has `has()` and `get()` methods, so I couldn't go through it. This would also have to be replaced (and break BC) to implement `resetAll()`.

Fixes symfony/demo#910

Commits
-------

da36629 Reset default EntrypointLookup on exception to fix #21 and #73
@sandermarechal
Copy link

This same problem occurs when you send more than one HTML e-mail. I use Encore for the CSS in my emails. When sending multiple emails, only the first email will have a stylesheet. All subsequent emails will not have a stylesheet.

This problem is compounded when using the messenger component to handle things in a background consumer. The first email that the consumer sends will be fine, but any subsequent emails will not be.

There should be a straightforward way to reset the cache. Does Twig some sort of events that we can hook into? Something that we can use to clear the cache for each rendering cycle?

@devsigner-xyz
Copy link

I personally think this would be a very interesting improvement.

In my case I am using asynchronous loading of stylesheets to avoid rendering blocking caused by stylesheets to improve SEO, for that I have to use this trick (see code below). The problem with this is that if the user has Javascript disabled in his browser, the stylesheets never get loaded because onload never gets executed, this causes all the styles to be lost.

To avoid this situation I am trying to use a second time encore_entry_link_tags() inside <noscript> and this is when I have encountered the same problem as @andyexeter, as the second time we use encore_entry_link_tags() it returns nothing.

{% block head_stylesheets %}
    {{ critical_path_css('critical', 'critical_frontend_homepage') }}

    {{ encore_entry_link_tags('frontend_homepage', null, 'frontend', attributes={
        media: "none",
        onload: "this.media='all'"
    }) }}

    <noscript>
        {{ encore_entry_link_tags('frontend_homepage', null, 'frontend') }}
    </noscript>
{% endblock %}

Is there a possibility to implement an enhancement to be able to call the same stylesheets twice?

@pouetman87
Copy link

pouetman87 commented Jun 9, 2022

here a dirty workaround
1- store the tags in a global variable (for example $_POST) the first time
2- use the global variable in place of the encore_entry_link_tags() or encore_entry_script_tags() method

To do that, make a twig extension (for exemple in src/Twig/GeneralExtension.php )

<?php
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

class GeneralExtension extends AbstractExtension
{
    public function getFunctions()
    {
        return [
            new TwigFunction('setPostVar', [$this, 'setPostVar']),
            new TwigFunction('getPostVar', [$this, 'getPostVar']),
        ];
    }
    public function setPostVar(string $key, mixed $value)
    {
        $_POST[$key]=$value;
        return true;
    }
    public function getPostVar(string $key)
    {
        if(!isset($_POST[$key]))return null;
        return $_POST[$key];
    }
}

in your template use it like this :

{% block styles %}
{% if getPostVar('encoreCss') is null %}
{% do setPostVar('encoreCss',encore.encore_absolute_link_tags('pdf')) %}
{% endif %}
{{ getPostVar('encoreCss')|raw }}
{% endblock %}

@carsonbot
Copy link
Collaborator

Thank you for this issue.
There has not been a lot of activity here for a while. Has this been resolved?

@andyexeter
Copy link
Author

@carsonbot no, this is not resolved.

@carsonbot carsonbot removed the Stalled label Mar 27, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants