EasyAdminBundle icon indicating copy to clipboard operation
EasyAdminBundle copied to clipboard

Template twig error in production mode

Open zpi12lmm opened this issue 1 year ago • 25 comments

Describe the bug I have a Symfony project where I use EasyAdmin based on docker compose. The error occurs only in the dashboard, and only in production mode. I tried testing in a clean install with no extra dependencies and configuration to make sure it wasn't my settings that were causing the problem. However, the error still doesn't go anywhere, could you also check and maybe find the problem.

To Reproduce

  1. Clone the setup repository.
  2. Run in developer mode: docker compose build --no-cache and docker compose up --pull -d --wait
  3. In php containers, install the following composer dependencies: easycorp/easyadmin-bundle and symfony/orm-pack
  4. Add a Dashboard controller, as well as any CRUD controller (if not already there), for example for the User entity
  5. Restart docker compose in production mode: docker compose down --remove-orphans, CADDY_MERCURE_JWT_SECRET=secret docker compose -f compose.yaml -f compose.prod.yaml build --no-cache, and CADDY_MERCURE_JWT_SECRET=secret docker compose -f compose.yaml -f compose.prod.yaml up --pull -d --wait
  6. Go to the /admin page, and go through the CRUD pages several times, if necessary, perform actions to trigger an error.

(OPTIONAL) Additional context This is the error in the logs: { "message": "Uncaught PHP Exception TypeError: \"Twig\\Environment::getTemplateClass(): Argument #1 ($name) must be of type string, null given, called in /app/vendor/twig/twig/src/Template.php on line 319\" at /app/vendor/twig/twig/src/Environment.php line 262", "context": { "exception": { "class": "TypeError", "message": "Twig\\Environment::getTemplateClass(): Argument #1 ($name) must be of type string, null given, called in /app/vendor/twig/twig/src/Template.php on line 319", "code": 0, "file": "/app/vendor/twig/twig/src/Environment.php:262" } }, "level": 500, "level_name": "CRITICAL", "channel": "request", "datetime": "2023-09-28T13:11:19.762885+00:00", "extra": {} }

zpi12lmm avatar Oct 27 '23 19:10 zpi12lmm

I don't use Docker, so I can't help here. Let's see if anybody from the community can help debug this issue.

javiereguiluz avatar Oct 30 '23 19:10 javiereguiluz

I am also running through the same issue. Happens only in prod and I am using symfony-docker

josaliba avatar Nov 04 '23 15:11 josaliba

i'm facing the same issue too...even in a dev environment php docker image is php:8.1-fpm (v 8.1.23) symfony 6.1.12 easyadmin 4.8.4 (after upgrade from 4.7.7)

will post something more useful, if i can find anything...

jamiroconca avatar Nov 06 '23 14:11 jamiroconca

maybe it's not docker related... i've found that for me removing ->overrideTemplates([ 'crud/index' => 'admin/crud/custom_entity_index.html.twig', 'crud/detail' => 'admin/crud/custom_entity_detail.html.twig' ])

makes the error go away, at least in a dev environment

also symfony/twig-bridge v6.1.11 symfony/twig-bundle v6.1.11 twig/cssinliner-extra v3.7.1 twig/extra-bundle v3.7.1 twig/inky-extra v3.7.1 twig/intl-extra v3.7.1 twig/markdown-extra v3.7.1 twig/twig v3.7.1

jamiroconca avatar Nov 07 '23 10:11 jamiroconca

But if that error might be an issue for everyone, then everyone would have it; if you have to remove it in order to work properly, then its an indication that at least something in the orchastration is not going as expected.

FWIW, I am using Docker in my custom built environment (because mine matches the clients servers technology), I do not have this problem; so maybe it is indeed related to the Symfony Docker env.

Have you checked if you have the same problem in a pure Symfony Apache-PHP environment? If required, I could share mine, too.

bytes-commerce avatar Nov 07 '23 15:11 bytes-commerce

tested without docker on an ubuntu server with apache, php8.1-fpm and easyadmin 4.8.4 (prod APP_ENV), without overrideTemplates there's no error, with overrideTemplates it comes out

a more in-depth investigation led to verifying that my problem depended on the structure of the custom templates which did not reflect the new ones from easyadmin (specifically the use of render_detail_fields_with_tabs)

at this point, although the error is similar, it is possible that it is not connected to what was reported by @zpi12lmm ...

jamiroconca avatar Nov 08 '23 10:11 jamiroconca

I had the same problem in the prod environment only. I found out that it works fine with EasyAdminBundle version 4.6.1. and this decorator for the Twig Environment https://github.com/EasyCorp/EasyAdminBundle/issues/3715#issuecomment-999616605. Saved me a lot of headache.

plantas avatar Nov 16 '23 00:11 plantas

hello my docker fellow,

I've the same issue, I'm using the symfony dunglas template: https://github.com/dunglas/symfony-docker

I tried the twig environment work around but it didn't fix this issue. Any way to fix this without down versionned easy admin ?

I'll do some investigation about when and why this error is thrown

tdumalin avatar Nov 27 '23 15:11 tdumalin

Hi again,

I've manage to exract the stack trace:

#0 /app/vendor/twig/twig/src/Template.php(319): Twig\\Environment->getTemplateClass()
#1 /app/var/cache/prod/twig/33/33b42a927271b040446d8c8bdb1eed5c.php(39): Twig\\Template->loadTemplate()
#2 /app/vendor/twig/twig/src/Template.php(86): __TwigTemplate_81476204e24c3fd5ef9c449cfa80e5e4->doGetParent()
#3 /app/var/cache/prod/twig/33/33b42a927271b040446d8c8bdb1eed5c.php(48): Twig\\Template->getParent()
#4 /app/vendor/twig/twig/src/Template.php(394): __TwigTemplate_81476204e24c3fd5ef9c449cfa80e5e4->doDisplay()
#5 /app/vendor/twig/twig/src/Template.php(367): Twig\\Template->displayWithErrorHandling()
#6 /app/var/cache/prod/twig/d1/d1138a024533fd5875370119c00e1040.php(42): Twig\\Template->display()
#7 /app/vendor/twig/twig/src/Template.php(394): __TwigTemplate_45572567e494b5e89e637704a58a157e->doDisplay()
#8 /app/vendor/twig/twig/src/Template.php(367): Twig\\Template->displayWithErrorHandling()
#9 /app/vendor/twig/twig/src/Template.php(379): Twig\\Template->display()
#10 /app/vendor/twig/twig/src/TemplateWrapper.php(40): Twig\\Template->render()
#11 /app/vendor/twig/twig/src/Environment.php(280): Twig\\TemplateWrapper->render()
#12 /app/vendor/symfony/framework-bundle/Controller/AbstractController.php(243): Twig\\Environment->render()
#13 /app/vendor/symfony/framework-bundle/Controller/AbstractController.php(254): Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController->renderView()
#14 /app/src/Controller/Admin/DashboardController.php(38): Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController->render()
#15 /app/vendor/symfony/http-kernel/HttpKernel.php(181): App\\Controller\\Admin\\DashboardController->index()
#16 /app/vendor/symfony/http-kernel/HttpKernel.php(76): Symfony\\Component\\HttpKernel\\HttpKernel->handleRaw()
#17 /app/vendor/symfony/http-kernel/Kernel.php(197): Symfony\\Component\\HttpKernel\\HttpKernel->handle()
#18 /app/vendor/runtime/frankenphp-symfony/src/Runner.php(35): Symfony\\Component\\HttpKernel\\Kernel->handle()
#19 [internal function]: Runtime\\FrankenPhpSymfony\\Runner->Runtime\\FrankenPhpSymfony\\{closure}()
#20 /app/vendor/runtime/frankenphp-symfony/src/Runner.php(30): frankenphp_handle_request()
#21 /app/vendor/autoload_runtime.php(29): Runtime\\FrankenPhpSymfony\\Runner->run()
#22 /app/public/index.php(5): require_once('...')
#23 {main}
"} []

And here is the 33b42a927271b040446d8c8bdb1eed5c.php file:

<?php

use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Extension\SandboxExtension;
use Twig\Markup;
use Twig\Sandbox\SecurityError;
use Twig\Sandbox\SecurityNotAllowedTagError;
use Twig\Sandbox\SecurityNotAllowedFilterError;
use Twig\Sandbox\SecurityNotAllowedFunctionError;
use Twig\Source;
use Twig\Template;

/* @EasyAdmin/page/content.html.twig */
class __TwigTemplate_81476204e24c3fd5ef9c449cfa80e5e4 extends Template
{
    private $source;
    private $macros = [];

    public function __construct(Environment $env)
    {
        parent::__construct($env);

        $this->source = $this->getSourceContext();

        $this->blocks = [
            'body_class' => [$this, 'block_body_class'],
            'page_title' => [$this, 'block_page_title'],
            'page_content' => [$this, 'block_page_content'],
            'content_title' => [$this, 'block_content_title'],
            'main' => [$this, 'block_main'],
        ];
    }

    protected function doGetParent(array $context)
    {
        // line 2
        return $this->loadTemplate(twig_get_attribute($this->env, $this->source, ($context["ea"] ?? null), "templatePath", ["layout"], "method", false, false, false, 2), "@EasyAdmin/page/content.html.twig", 2);
    }

    protected function doDisplay(array $context, array $blocks = [])
    {
        $macros = $this->macros;
        // line 3
        $context["__internal_1b5724d1ff989db167bf5fb9925283416edb9743d6264d1a1f777ddfda4e1a6d"] = twig_get_attribute($this->env, $this->source, twig_get_attribute($this->env, $this->source, ($context["ea"] ?? null), "i18n", [], "any", false, false, false, 3), "translationDomain", [], "any", false, false, false, 3);
        // line 2
        $this->getParent($context)->display($context, array_merge($this->blocks, $blocks));
    }

    // line 5
    public function block_body_class($context, array $blocks = [])
    {
        $macros = $this->macros;
        echo "page-content";
    }

    // line 8
    public function block_page_title($context, array $blocks = [])
    {
        $macros = $this->macros;
    }

    // line 12
    public function block_page_content($context, array $blocks = [])
    {
        $macros = $this->macros;
    }

    // line 15
    public function block_content_title($context, array $blocks = [])
    {
        $macros = $this->macros;
        $this->displayBlock("page_title", $context, $blocks);
    }

    // line 17
    public function block_main($context, array $blocks = [])
    {
        $macros = $this->macros;
        $this->displayBlock("page_content", $context, $blocks);
    }

    public function getTemplateName()
    {
        return "@EasyAdmin/page/content.html.twig";
    }

    public function isTraitable()
    {
        return false;
    }

    public function getDebugInfo()
    {
        return array (  78 => 17,  71 => 15,  65 => 12,  59 => 8,  52 => 5,  48 => 2,  46 => 3,  39 => 2,);
    }

    public function getSourceContext()
    {
        return new Source("", "@EasyAdmin/page/content.html.twig", "/app/vendor/easycorp/easyadmin-bundle/src/Resources/views/page/content.html.twig");
    }
}

I hope this help found what's wrong

tdumalin avatar Nov 28 '23 13:11 tdumalin

I also encountered the same error using the latest version of https://github.com/dunglas/symfony-docker

Downgrading to 4.6.1 and adding the decorator class like @plantas mentioned worked for me but i wasn't happy with that.

It seems to me that this issue is caused by the FrankenPHP server that is used in the docker configuration. Going back to an older version that used separated containers for php and caddy solved the issue for me. https://github.com/dunglas/symfony-docker/tree/b5710da39cc9939c2eef4787ab50b4ee7d16e44f

ac-shadow avatar Dec 04 '23 08:12 ac-shadow

@ac-shadow, I confirm that it comes from the frankenphp worker mode. But i liked the one container stuff, so I disabled it on the Dockerfile like this: (don't know if there is a "proper" way)

#...
FROM frankenphp_base AS frankenphp_prod

ENV APP_ENV=prod
# Comment this line
#ENV FRANKENPHP_CONFIG="import worker.Caddyfile"
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"

COPY --link frankenphp/conf.d/app.prod.ini $PHP_INI_DIR/conf.d/
# Comment this line
#COPY --link frankenphp/worker.Caddyfile /etc/caddy/worker.Caddyfile
# ...

tdumalin avatar Dec 04 '23 08:12 tdumalin

Thank you @tdumalin. Just tried out your solution and everything works fine now. It also fixed another unrelated issue I had before when I used the FrankenPHP server. Seems like that was also caused by the worker mode.

ac-shadow avatar Dec 04 '23 10:12 ac-shadow

@ac-shadow, Can you tell me what's the other issue it solved? Juts by curiosity, for me it's solved another issue with session creation. If I mange to list all the related issues maybe it'll be easier to understand what's happening with worker mode ?

tdumalin avatar Dec 04 '23 10:12 tdumalin

@tdumalin, The issue was a memory leak but a workaround for that is also mentioned in the docs which I missed before: https://github.com/dunglas/frankenphp/blob/main/docs/worker.md#restart-the-worker-after-a-certain-number-of-requests

ac-shadow avatar Dec 04 '23 11:12 ac-shadow

I've created a simple fix that should update globals if they are different.

I can't test it in FrankenPHP at the moment, but I hope this will help. Additionally, it should be beneficial for any app servers where app states are persistent between requests (roadrunner,swoole, etc.).

Moreover, this fix will allow the use of subrequests in EasyAdmin.

Example concept:

{% extends '@EasyAdmin/page/content.html.twig' %}

{% block main %}
    <h2> Employees </h2>
    {{ render('/?crudAction=index&crudControllerFqcn=App\EmployeeCrudController') }}
    <h2> Organisations </h2>
    {{ render('/?crudAction=index&crudControllerFqcn=App\OrganisationCrudController') }}
{% endblock %}

It was unable due to issue with AdminContext in twig, ea variable was the same for sub requests as for master request;

Fix is just a simple listener for KernelView event:

<?php
declare(strict_types=1);

namespace App\EventListener;

use EasyCorp\Bundle\EasyAdminBundle\Twig\EasyAdminTwigExtension;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Twig\Environment;

final class RefreshAdminContext implements EventSubscriberInterface
{
    public function __construct(private Environment $twig){

    }
    public static function getSubscribedEvents():array
    {
        return [
            //Priority is set to 1 because it should be executed before CrudResponseListener (which has no priority, so it is 0 by default).
            ViewEvent::class => ['onKernelView',1]
        ];
    }

    public function onKernelView(ViewEvent $event):void
    {
        $extensionGlobals = $this->twig->getExtension(EasyAdminTwigExtension::class)->getGlobals();
        $twigGlobals = $this->twig->getGlobals();

        foreach ($extensionGlobals as $key=>$value){
            if(!isset($twigGlobals[$key]) || $twigGlobals[$key]===$value) {
                continue;
            }
            //Update the global variable if it exists in the Twig environment and its value is different from that in the extension.
            $this->twig->addGlobal($key,$value);
        }
    }

}

misterx avatar Dec 15 '23 11:12 misterx

@misterx's solution didn't end up working for me. While this caused the bug to be less frequent (however, it still occurred in some marginal cases), it also caused the Admin Context in Twig to not match the context in the rest of the application in some situations, which led to highlighted menu items not matching the route, for example.

I came up with the following fix:

services.yaml

    App\Service\Twig\Environment:
        parent: twig
        decorates: twig
        calls:
            - setAdminContextProvider: [ '@EasyCorp\Bundle\EasyAdminBundle\Provider\AdminContextProvider' ]

App\Service\Twig\Environment.php

<?php

namespace App\Service\Twig;

use EasyCorp\Bundle\EasyAdminBundle\Provider\AdminContextProvider;
use Symfony\Contracts\Service\Attribute\Required;

/**
 * Refreshes EasyAdmin context between requests. Fixes problem with production worker
 *
 * See:
 *  - https://github.com/EasyCorp/EasyAdminBundle/issues/5986
 *  - https://github.com/dunglas/symfony-docker/issues/474
 */
class Environment extends \Twig\Environment
{
    private AdminContextProvider $adminContextProvider;

    #[Required]
    public function setAdminContextProvider(AdminContextProvider $adminContextProvider): void
    {
        $this->adminContextProvider = $adminContextProvider;
    }

    public function getGlobals(): array
    {
        $globals = parent::getGlobals();

        $context = $this->adminContextProvider->getContext();

        if ($context !== null) {
            $globals['ea'] = $context;
        }

        return $globals;
    }
}

This does not change the fact that the solution is rather inelegant and the creators of EsayAdmin should, in my opinion, remove Twig Global and use an Twig Extension instead (e.g. replace ea -> ea()).

mozkomor05 avatar Jan 26 '24 23:01 mozkomor05

@mozkomor05 My fix is related to accessing the fresh AdminContext in Twig templates. Regarding the issue with the menu, as I see, menu highlighting is implemented in MenuItemMatcher. Therefore, it's not related to the Twig context and uses AdminContextProvider to retrieve the context from the request. Maybe there is some another service that stores state (similar to Twig\Environment with globals);

UPD: An error might be caused if the Twig\Environment::render method is called before the kernel View event is fired.

UPD2: After reviewing the EA flow again, I think it's better to update globals after initializing AdminContext instead of waiting until the end of the request. So, I suggest changing the onKernelView event in my fix to onKernelRequest and registering the listener after AdminRouterSubscriber.

misterx avatar Jan 27 '24 11:01 misterx

Changing the event to kernel.controller fixed the menu matching issue. However I still come across rare situations where Twig global ea is not accessible. I'll try to debug later. For now, the twig decorator hack works better for me.

mozkomor05 avatar Jan 27 '24 11:01 mozkomor05

@javiereguiluz any thoughts on moving the admin context from a global to a twig function and then deprecating the global?

Seems like it might fix this and is done pretty easily. Or is there a reason it's a global?

KDederichs avatar Feb 06 '24 12:02 KDederichs

+1

mozkomor05 avatar Feb 06 '24 13:02 mozkomor05

Thanks for your attempt to solve this issue by replacing the Twig global variable by a Twig function. However, I don't like that solution for two reasons:

  • I can't think of any way of making it in a BC way that doesn't break all apps that use the ea global variable
  • It doesn't really solve the problem, it just changes code to avoid it

I talked with @dunglas about this. Two quick comments:

  • This error is probably caused by EasyAdmin, not Symfony or FrankenPHP ... BUT, our code is pretty standard: we're just injecting an object as a global Twig variable in an event listener. It's 100% Symfony standard code.
  • Kévin mentioned that maybe we're missing some "reset" somewhere

Symfony for example has a lot of reset() calls which were introduced to make it compatible with apps like FrankenPHP in worker mode. See for example:

  • https://github.com/symfony/symfony/pull/43333/files
  • https://github.com/symfony/symfony/pull/45479/files

But there are also examples where we removed the reset() and replaced by a different solution:

  • https://github.com/symfony/symfony/pull/49104/files

I tried to install FrankenPHP to reproduce the issue. I can't even run my SF + EA apps with FrankenPHP, so I can't reproduce it.

So, can anyone please give a shot to this proposal and see if we're missing some reset somewhere? Thanks!

javiereguiluz avatar Mar 02 '24 18:03 javiereguiluz

@javiereguiluz, try Swoole, it works, https://github.com/php-runtime/swoole

maxkain avatar Mar 15 '24 13:03 maxkain

@javiereguiluz, the issue will be resolved, if Twig developers will add something like RefreshGlobalsInterface for Extension.

maxkain avatar Mar 15 '24 14:03 maxkain

Please check #6273 and report back if it fixes the issue for you.

nicolas-grekas avatar Apr 19 '24 15:04 nicolas-grekas