symfony / webpack-encore-bundle

Symfony integration with Webpack Encore!
https://symfony.com/webpack-encore
MIT License
938 stars 83 forks source link

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

Open andyexeter opened 5 years ago

andyexeter commented 5 years ago

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:

https://github.com/symfony/webpack-encore-bundle/blob/5e1cab3d223f65933d59a5a95ea01a6ed2833db4/src/Asset/EntrypointLookup.php#L84-L89

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 commented 5 years ago

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 https://github.com/symfony/webpack-encore-bundle/pull/21)

weaverryan commented 5 years ago

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 commented 5 years ago

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',
        ];
    }
}
sandermarechal commented 4 years ago

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 commented 3 years ago

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 commented 2 years ago

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 %}