EasyAdminBundle: Template twig error in production mode

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

About this issue

  • Original URL
  • State: open
  • Created 8 months ago
  • Reactions: 6
  • Comments: 24 (3 by maintainers)

Most upvoted comments

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!

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

}

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

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

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

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