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

Template twig error in production mode #5986

Open
zpi12lmm opened this issue Oct 27, 2023 · 24 comments
Open

Template twig error in production mode #5986

zpi12lmm opened this issue Oct 27, 2023 · 24 comments
Labels
bug help wanted Issues and PRs which are looking for volunteers to complete them. status: unconfirmed
Milestone

Comments

@zpi12lmm
Copy link

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": {} }

@javiereguiluz javiereguiluz added this to the 4.x milestone Oct 30, 2023
@javiereguiluz javiereguiluz added bug status: unconfirmed help wanted Issues and PRs which are looking for volunteers to complete them. labels Oct 30, 2023
@javiereguiluz
Copy link
Collaborator

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

@josaliba
Copy link

josaliba commented Nov 4, 2023

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

@jamiroconca
Copy link

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
Copy link

jamiroconca commented Nov 7, 2023

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

@nopenopenope
Copy link
Contributor

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.

@jamiroconca
Copy link

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 ...

@plantas
Copy link

plantas commented Nov 16, 2023

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 #3715 (comment). Saved me a lot of headache.

@tdumalin
Copy link

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
Copy link

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

@ac-shadow
Copy link

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

@tdumalin
Copy link

tdumalin commented Dec 4, 2023

@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
# ...

@ac-shadow
Copy link

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.

@tdumalin
Copy link

tdumalin commented Dec 4, 2023

@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 ?

@ac-shadow
Copy link

@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

@misterx
Copy link

misterx commented Dec 15, 2023

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

}

@mozkomor05
Copy link

@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()).

@misterx
Copy link

misterx commented Jan 27, 2024

@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.

@mozkomor05
Copy link

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.

@KDederichs
Copy link
Contributor

@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?

@mozkomor05
Copy link

+1

@javiereguiluz
Copy link
Collaborator

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:

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

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!

@maxkain
Copy link

maxkain commented Mar 15, 2024

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

@maxkain
Copy link

maxkain commented Mar 15, 2024

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

@nicolas-grekas
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug help wanted Issues and PRs which are looking for volunteers to complete them. status: unconfirmed
Projects
None yet
Development

Successfully merging a pull request may close this issue.