symfony6esi

Symfony 6 ESI (Edge Side Includes) not caching


I'm trying to cache a mega-menu on my website, since it doesn't change often and is resource heavy to compute.

The menu is already an embedded controller, I just want to change it to an ESI. I've put a timestamp on the embedded menu controller, but the timestamp always ticks up on refresh and the profiler doesn't show any changes.

# config/packages/framework.yaml

framework:
    esi: true
    fragments: { path: /_fragment }

Controller for the menu:

    // src/Controller/HeaderController.php

    ...
    #[Cache(smaxage: 60)]
    public function menu(ProductRepository $productRepository): Response
    {
        /** 
         * @todo: possibly this should be one query and return an array instead of one query for each section?
         */
        /** @var Section[] $sections */
        $sections = $this->em->getRepository(Section::class)->findBy(['showInMenu' => true], ['rank' => 'ASC']);
        foreach ($sections as $section) {
            $section->setProductsInMenu($productRepository->getTopRankedProductsInSection($section));
        }

        $timestamp = time();

        $searchForm = $this->createForm(SearchFormType::class, null, ['action' => $this->generateUrl('app_search')]);

        return $this->render('header/_menu.html.twig', [
            'searchForm' => $searchForm->createView(),
            'sections' => $sections,
            'timestamp' => $timestamp,
        ])
            ->setPublic();
    }

Controller for the homepage. This includes the embedded menu controller in the base.html.twig file.

// src/Controller/HomeController.php

class HomeController extends AbstractController
{
    #[Route('/', name: 'app_home')]
    public function index(EntityManagerInterface $em, BannerRepository $bannerRepository): Response
    {
        $banners = $bannerRepository->getActiveBanners();
        $featuredProducts = $em->getRepository(Product::class)->findBy(['featured' => 1], ['rank' => 'ASC'], 12);
        return $this->render('home/index.html.twig', [
            'banners' => $banners,
            'featuredProducts' => $featuredProducts,
        ])
        ->setPublic()
        ->setMaxAge(120);
    }
}

base.html.twig that is included in all pages and includes the menu embedded controller.

    <!-- src/templates/base.html.twig -->
    ...
    <body>
        {{ render(controller(
            'App\\Controller\\HeaderController::account'
        )) }}
        {{ render_esi(controller(
            'App\\Controller\\HeaderController::menu'
        )) }}
        <div class="body"> {% block body %}{% endblock %}
            </div>
            {{ render(controller(
            'App\\Controller\\FooterController::links'
        )) }}
            {{ render(controller(
            'App\\Controller\\FooterController::information'
        )) }}
            {{ render(controller(
            'App\\Controller\\FooterController::compliance'
        )) }}
    </body>
    ...

Solution

  • The ESI tag generated by Symfony is only used when a gateway cache is able to handle it. See this part of the documentation :

    When using the default render() function (or setting the renderer to inline), Symfony merges the included page content into the main one before sending the response to the client. But if you use the esi renderer (i.e. call render_esi()) and if Symfony detects that it's talking to a gateway cache that supports ESI, it generates an ESI include tag. But if there is no gateway cache or if it does not support ESI, Symfony will just merge the included page content within the main one as it would have done if you had used render().

    Symfony also provides some informations about gateway caches here.

    Therefore, you could try to enable the Symfony reverse proxy with this option, to quickly test if the ESI tag on your request works.

    framework:
        http_cache: true