FOSHttpCacheBundle cache invalidation with Symfony built-in reverse proxy doesn’t work

I’m trying to do a hard thing: implementing cache invalidation with Symfony 4.4.13 using FOSHttpCacheBundle 2.9.0 and built-in Symfony reverse proxy.
Unfortunately, I can’t use other caching solution (like Varnish or Nginx) because my hosting service doesn’t offer them. So, the Symfony built-in reverse proxy is the only solution I have.

I’ve installed and configured FOSHttpCacheBundle (following the documentation). Also created a CacheKernel class and modified Kernel to use it (following Symfony official documentation, FOSHttpCache documentation and FOSHttpCacheBundle documentation).

After few tests (with my browser), the HTTP caching works and GET responses are cached (seen in browser network analyzer). But, when I update a resource with PUT/PATCH/POST, the GET responses still come from the cache and are unchanged until the expiration. My deduction is the invalidation doesn’t work.

Have I do something wrong? Can you help me to troubleshoot?
See my code and configuration below.

config/packages/fos_http_cache.yaml

fos_http_cache:
    cache_control:
        rules:
            -
                match:
                    path: ^/
                headers:
                    cache_control:
                        public: true
                        max_age: 15
                        s_maxage: 30
                    etag: "strong"
    cache_manager:
        enabled: true
    invalidation:
        enabled: true
    proxy_client:
        symfony:
            tags_header: My-Cache-Tags
            tags_method: TAGPURGE
            header_length: 1234
            purge_method: PURGE
            use_kernel_dispatcher: true

src/CacheKernel.php

<?php
namespace App;

use FOSHttpCacheSymfonyCacheCacheInvalidation;
use FOSHttpCacheSymfonyCacheCustomTtlListener;
use FOSHttpCacheSymfonyCacheDebugListener;
use FOSHttpCacheSymfonyCacheEventDispatchingHttpCache;
use FOSHttpCacheSymfonyCachePurgeListener;
use FOSHttpCacheSymfonyCacheRefreshListener;
use FOSHttpCacheSymfonyCacheUserContextListener;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpKernelHttpCacheHttpCache;
use SymfonyComponentHttpKernelHttpCacheStore;
use SymfonyComponentHttpKernelHttpKernelInterface;

class CacheKernel extends HttpCache implements CacheInvalidation
{
    use EventDispatchingHttpCache;

    // Overwrite constructor to register event listeners for FOSHttpCache.
    public function __construct(HttpKernelInterface $kernel, SurrogateInterface $surrogate = null, array $options = [])
    {
        parent::__construct($kernel, new Store($kernel->getCacheDir()), $surrogate, $options);

        $this->addSubscriber(new CustomTtlListener());
        $this->addSubscriber(new PurgeListener());
        $this->addSubscriber(new RefreshListener());
        $this->addSubscriber(new UserContextListener());
        if (isset($options['debug']) && $options['debug'])
            $this->addSubscriber(new DebugListener());
    }

    // Made public to allow event listeners to do refresh operations.
    public function fetch(Request $request, $catch = false)
    {
        return parent::fetch($request, $catch);
    }
}

src/Kernel.php

<?php
namespace App;

use FOSHttpCacheSymfonyCacheHttpCacheAware;
use FOSHttpCacheSymfonyCacheHttpCacheProvider;
use SymfonyBundleFrameworkBundleKernelMicroKernelTrait;
use SymfonyComponentConfigLoaderLoaderInterface;
use SymfonyComponentConfigResourceFileResource;
use SymfonyComponentDependencyInjectionContainerBuilder;
use SymfonyComponentHttpKernelKernel as BaseKernel;
use SymfonyComponentRoutingRouteCollectionBuilder;

class Kernel extends BaseKernel implements HttpCacheProvider
{
    use MicroKernelTrait;
    use HttpCacheAware;

    private const CONFIG_EXTS = '.{php,xml,yaml,yml}';

    public function __construct(string $environment, bool $debug)
    {
        parent::__construct($environment, $debug);
        $this->setHttpCache(new CacheKernel($this));
    }
...

public/index.php

<?php
use AppKernel;
use SymfonyComponentErrorHandlerDebug;
use SymfonyComponentHttpFoundationRequest;

require dirname(__DIR__).'/config/bootstrap.php';

...

$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$kernel = $kernel->getHttpCache();
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);

One of mine controller, src/Controller/SectionController.php (NOTE: routes are defined in YAML files)

<?php

namespace AppController;

use AppEntitySection;
use AppEntitySectionCollection;
use AppFormSectionType;
use FOSHttpCacheBundleConfigurationInvalidateRoute;
use FOSRestBundleControllerAbstractFOSRestController;
use FOSRestBundleControllerAnnotations as Rest;
use FOSRestBundleViewView;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentHttpKernelExceptionHttpException;
use SymfonyComponentHttpKernelExceptionNotFoundHttpException;

class SectionController extends AbstractFOSRestController
{
    /**
     * List all sections.
     *
     * @RestView
     * @param Request $request the request object
     * @return array
     *
     * Route: get_sections
     */
    public function getSectionsAction(Request $request)
    {
        return new SectionCollection($this->getDoctrine()->getRepository(Section::class)->findAll());
    }

    /**
     * Get a single section.
     *
     * @RestView
     * @param Request $request the request object
     * @param int     $id      the section id
     * @return array
     * @throws NotFoundHttpException when section not exist
     *
     * Route: get_section
     */
    public function getSectionAction(Request $request, $id)
    {
        if (!$section = $this->getDoctrine()->getRepository(Section::class)->find($id))
            throw $this->createNotFoundException('Section does not exist.');

        return array('section' => $section);
    }

    /**
     * Get friends of the section's user.
     *
     * @RestView
     * @return array
     *
     * Route: get_friendlysections
     */
    public function getFriendlysectionsAction()
    {
        return $this->get('security.token_storage')->getToken()->getUser()->getSection()->getMyFriends();
    }

    private function processForm(Request $request, Section $section)
    {
        $em = $this->getDoctrine()->getManager();

        $statusCode = $em->contains($section) ? Response::HTTP_NO_CONTENT : Response::HTTP_CREATED;

        $form = $this->createForm(SectionType::class, $section, array('method' => $request->getMethod()));
        // If PATCH method, don't clear missing data.
        $form->submit($request->request->get($form->getName()), $request->getMethod() === 'PATCH' ? false : true);

        if ($form->isSubmitted() && $form->isValid()) {
            $em->persist($section);
            $em->flush();

            $response = new Response();
            $response->setStatusCode($statusCode);

            // set the 'Location' header only when creating new resources
            if ($statusCode === Response::HTTP_CREATED) {
                $response->headers->set('Location',
                    $this->generateUrl(
                        'get_section', array('id' => $section->getId()),
                        true // absolute
                    )
                );
            }

            return $response;
        }

        return View::create($form, Response::HTTP_BAD_REQUEST);
    }

    /**
     *
     * Creates a new section from the submitted data.
     *
     * @RestView
     * @return FormTypeInterface[]
     *
     * @InvalidateRoute("get_friendlysections")
     * @InvalidateRoute("get_sections")
     *
     * Route: post_section
     */
    public function postSectionsAction(Request $request)
    {
        return $this->processForm($request, new Section());
    }

    /**
     * Update existing section from the submitted data.
     *
     * @RestView
     * @param int     $id      the section id
     * @return FormTypeInterface[]
     * @throws NotFoundHttpException when section not exist
     *
     * @InvalidateRoute("get_friendlysections")
     * @InvalidateRoute("get_sections")
     * @InvalidateRoute("get_section", params={"id" = {"expression"="id"}})")
     *
     * Route: put_section
     */
    public function putSectionsAction(Request $request, $id)
    {
        if (!$section = $this->getDoctrine()->getRepository(Section::class)->find($id))
            throw $this->createNotFoundException('Section does not exist.');

        return $this->processForm($request, $section);
    }

    /**
     * Partially update existing section from the submitted data.
     *
     * @RestView
     * @param int     $id      the section id
     * @return FormTypeInterface[]
     * @throws NotFoundHttpException when section not exist
     *
     * @InvalidateRoute("get_friendlysections")
     * @InvalidateRoute("get_sections")
     * @InvalidateRoute("get_section", params={"id" = {"expression"="id"}})")
     *
     * Route: patch_section
     */
    public function patchSectionsAction(Request $request, $id)
    {
        return $this->putSectionsAction($request, $id);
    }

    /**
     * Remove a section.
     *
     * @RestView(statusCode=204)
     * @param int     $id      the section id
     * @return View
     *
     * @InvalidateRoute("get_friendlysections")
     * @InvalidateRoute("get_sections")
     * @InvalidateRoute("get_section", params={"id" = {"expression"="id"}})")
     *
     * Route: delete_section
     */
    public function deleteSectionsAction($id)
    {
        $em = $this->getDoctrine()->getManager();
        if ($section = $this->getDoctrine()->getRepository(Section::class)->find($id)) {
            $em->remove($section);
            $em->flush();
        }
    }
}

Source: Symfony Questions

Was this helpful?

0 / 0

Leave a Reply 0

Your email address will not be published. Required fields are marked *