EasyCorp / EasyAdminBundle

EasyAdmin is a fast, beautiful and modern admin generator for Symfony applications.
MIT License
4.09k stars 1.02k forks source link

[EA - 3.1.3] - Unable to add global "ea" as the runtime or the extensions have already been initialized. #3715

Closed fjugalde closed 2 years ago

fjugalde commented 4 years ago

Describe the bug I'm getting this error when trying to exec $this-renderView(...) in the EntityCrudController. The errors it seems to be in the line 77 of AdminContextListener.php

$this->twig->addGlobal('ea', $adminContext);

This method is from the Environment.php file of twig. And inside this method (Line 747) there is a validation that is throwing this error in line 750:

if ($this->extensionSet->isInitialized() && !\array_key_exists($name, $this->getGlobals())) {
            throw new \LogicException(sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name));
        }

So it seems like the global variable already exists in twig globals.

Have anyone getting this error when executes the $this-renderView(...) method?

Thanks so much in advance.

Francisco Ugalde

To Reproduce In a single controller extending from AbstractController, add a method with a Route annotation, Add a return like:

return new Response($this->renderView('my_template.html.twig', []);

Additional Context I'm trying to generate a html content from a twig template on the crud method configureAssets(...) and to pass as param in the ->addHtmlContentToBody() method.

oskar-gp commented 4 years ago

Same issue here. When I render view from twig then I can dump string, but when I put this string in configureAssets->addHtmlContentToBody()

fjugalde commented 4 years ago

Same issue here. When I render view from twig then I can dump string, but when I put this string in configureAssets->addHtmlContentToBody()

Correct!.. i can dump the render view content as string too but after that, an exception is thrown with the above message details.

javiereguiluz commented 4 years ago

Please, try to debug this error around line 77 of AdminContextListener.php. See if there's a method to check if a Twig global exists, check if it already exists and check if adding the global conditionally solves your issue. Thanks!

fjugalde commented 4 years ago

Hi @javiereguiluz

Sorry for the delay in replying..

I made a debug just like you mention it above and inside the method ->addGlobal(...) there is a validation that checks if the extension is initialized and if the global value to being added didn't exist.

When i run my controller, it seems that executes two times the same addGlobal(...) method. I printed a dump() in the addGlobal(...) method inside the Environment.php file:

 public function addGlobal(string $name, $value)
    {
        dump((new \DateTime())->format('H:i:s'));
        dump('initialized:'.($this->extensionSet->isInitialized()?'true':'false'), 'array_key_exists:'.(!\array_key_exists($name, $this->getGlobals())?'true':'false'));
        if ($this->extensionSet->isInitialized() && !\array_key_exists($name, $this->getGlobals())) {
            throw new \LogicException(sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name));
        }
        sleep(2);

        if (null !== $this->resolvedGlobals) {
            $this->resolvedGlobals[$name] = $value;
        } else {
            $this->globals[$name] = $value;
        }
    }

NOTE: I added a sleep(2) in order to stop execution for 2 sec and be able to differentiate the dump outputs.

And just before adding global in the AdminContextListener:

    dump($this->twig->getGlobals());
    $this->twig->addGlobal('ea', $adminContext);

And the output is image

It's seems like before AdminContextListener is executed, the Context is added before in another file, and then, when the listener is executed, tries to add it again.

But the thing is that if we check the stack trace or the execution order, when the listener is executed, the twig environment seems to be different from the original because seems that does not contains the ea context added before his execution?

It's very strange.

alumnihabbim commented 3 years ago

This bug also happens using RoadRunner:

With the extension https://github.com/baldinof/roadrunner-bundle the first entry always makes the bug, just by reloading the page it lets continue.

With the extension https://github.com/php-runtime/roadrunner-symfony-nyholm the bug is persistent, it cannot be used with easyadmin.

maxkain commented 2 years ago

UPD: there is no longer works, see https://github.com/EasyCorp/EasyAdminBundle/issues/3715#issuecomment-1828630373

This bug also happens using RoadRunner:

With the extension https://github.com/baldinof/roadrunner-bundle the first entry always makes the bug, just by reloading the page it lets continue.

With the extension https://github.com/php-runtime/roadrunner-symfony-nyholm the bug is persistent, it cannot be used with easyadmin.

I use swoole webserver and k911/swoole-bundle. Have the same issue. This helped me:

services.yaml:

    App\Service\Twig\Environment:
        decorates: twig
        parent: twig

App\Service\Twig\Environment:

namespace App\Service\Twig;

use Twig\Environment as BaseEnvironment;

class Environment extends BaseEnvironment
{
    private $presetGlobals = ['ea'];

    public function setPresetGlobals(array $names)
    {
        $this->presetGlobals = $names;
    }

    public function addGlobal(string $name, $value)
    {
        $presetGlobals = $this->presetGlobals;

        if (in_array($name, $presetGlobals)) {
            $ref = (new \ReflectionObject($this))->getParentClass();

            $globalsProperty = $ref->getProperty('globals');
            $globalsProperty->setAccessible(true);

            $resolvedGlobalsProperty  = $ref->getProperty('resolvedGlobals');
            $resolvedGlobalsProperty->setAccessible(true);

            $globals = $globalsProperty->getValue($this);
            $resolvedGlobals = $resolvedGlobalsProperty->getValue($this);

            foreach ($presetGlobals as $global) {
                if ($name == $global) {
                    if ($resolvedGlobals !== null) {
                        $resolvedGlobals[$global] = null;
                    } else {
                        $globals[$global] = null;
                    }
                }
            }

            $globalsProperty->setValue($this, $globals);
            $resolvedGlobalsProperty->setValue($this, $resolvedGlobals);
        }

        parent::addGlobal($name, $value);
    }
}
javiereguiluz commented 2 years ago

@maxkain thanks for showing that code as an example of how to fix this.

Sadly, I can't merge any of that because I don't use this kind of systems, so I will never be able to debug issues or fix/maintain this.

Also, this looks like a generic problem of Twig globals when using this kind of long-running systems. Maybe it's something that Twig could fix/improve in their core?

In any case, closing for the reasons mentioned earlier. Thanks for understanding!

allan-simon commented 2 years ago

@maxkain shouldn't be the issue created on Twig itself to have a mechanism to help twig being compatible with roadrunner/bref/swoole ?

WebDaMa commented 1 year ago

For anyone having this issue on EA4

Somehow >addHtmlContentToBody() just breaks.

Instead create templates/bundles/EasyAdminBundle/layout.html.twig

And include your content like this:

{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #}
{% extends '@!EasyAdmin/layout.html.twig' %}

{% block configured_body_contents %}
    {{ parent() }}
    # Add custom HTML to body here
    {% include 'components/logout/_logout.html.twig' %}
    {% include 'components/logout/_session-expired.html.twig' %}
{% endblock %}
maxkain commented 11 months ago

Last solution no longer works. There is new. But as I understand, the right solution is to put a service into globals instead of 'ea', which will fetch the data.

    App\Service\Twig\Environment:
        parent: twig
        decorates: twig
namespace App\Service\Twig;

use EasyCorp\Bundle\EasyAdminBundle\Twig\EasyAdminTwigExtension;
use Twig\Environment as BaseEnvironment;
use Twig\Extension\GlobalsInterface;

class Environment extends BaseEnvironment
{
    private $extensionsToRefreshGlobals = [EasyAdminTwigExtension::class];

    public function mergeGlobals(array $context): array
    {
        foreach ($this->getExtensions() as $class => $extension) {
            if ($extension instanceof GlobalsInterface && in_array($class, $this->extensionsToRefreshGlobals)) {
                $this->refreshGlobals($extension->getGlobals());
            }
        }

        return parent::mergeGlobals($context);
    }

    private function refreshGlobals(array $freshGlobals): void
    {
        $ref = (new \ReflectionObject($this))->getParentClass();

        $globalsProperty = $ref->getProperty('globals');
        $globalsProperty->setAccessible(true);

        $resolvedGlobalsProperty  = $ref->getProperty('resolvedGlobals');
        $resolvedGlobalsProperty->setAccessible(true);

        $extensionSetProperty  = $ref->getProperty('extensionSet');
        $extensionSetProperty->setAccessible(true);
        $extensionSet = $extensionSetProperty->getValue($this);

        $refExtensionSet = (new \ReflectionObject($extensionSet));
        $extensionSetGlobalsProperty = $refExtensionSet->getProperty('globals');
        $extensionSetGlobalsProperty->setAccessible(true);

        $globals = $globalsProperty->getValue($this);
        $resolvedGlobals = $resolvedGlobalsProperty->getValue($this);
        $extensionSetGlobals = $extensionSetGlobalsProperty->getValue($extensionSet);

        $globals = array_merge($globals, $freshGlobals);
        if ($resolvedGlobals !== null) {
            $resolvedGlobals = array_merge($resolvedGlobals, $freshGlobals);
        }
        if ($extensionSetGlobals !== null) {
            $extensionSetGlobals = array_merge($extensionSetGlobals, $freshGlobals);
        }

        $globalsProperty->setValue($this, $globals);
        $resolvedGlobalsProperty->setValue($this, $resolvedGlobals);
        $extensionSetGlobalsProperty->setValue($this, $extensionSetGlobals);
    }
}
misterx commented 11 months ago

UPD: I 've created fix for admin context but it is related to EA 4. So i posted it there, as more more suitable issue for it https://github.com/EasyCorp/EasyAdminBundle/issues/5986#issuecomment-1857725801

maxkain commented 11 months ago

Yet another way to fix this issue: use twig extension for "ea" instead of globals, and set it in layout template like this: {% set ea = ea() %}

misterx commented 11 months ago

@maxkain I'm not sure if this will work in nested blocks and macros because you are redefining a variable in the template scope, not globally. However, I could be wrong.

maxkain commented 8 months ago

UPD: I 've created fix for admin context but it is related to EA 4. So i posted it there, as more more suitable issue for it #5986 (comment)

@misterx, I have tried it. Sometimes it works, sometimes there is error: request.CRITICAL: Uncaught PHP Exception Twig\Error\RuntimeError: "An exception has been thrown during the rendering of a template ("Twig\Environment::getTemplateClass(): Argument #1 ($name) must be of type string, null given, called in ...vendor/twig/twig/src/Template.php on line 319")." at index.html.twig line 4