首页 > 解决方案 > 使用 Symfony 内置反向代理的 FOSHttpCacheBundle 缓存失效不起作用

问题描述

我正在尝试做一件困难的事情:使用 FOSHttpCacheBundle 2.9.0 和内置的 Symfony 反向代理,使用 Symfony 4.4.13 实现缓存失效。不幸的是,我不能使用其他缓存解决方案(如 Varnish 或 Nginx),因为我的托管服务不提供它们。所以,Symfony 内置的反向代理是我唯一的解决方案。

我已经安装并配置了 FOSHttpCacheBundle(按照文档)。还创建了一个 CacheKernel 类并修改了 Kernel 以使用它(遵循Symfony 官方文档FOSHttpCache 文档FOSHttpCacheBundle 文档)。

经过几次测试(使用我的浏览器),HTTP 缓存工作并且 GET 响应被缓存(在浏览器网络分析器中看到)。但是,当我使用 PUT/PATCH/POST 更新资源时,GET 响应仍然来自缓存,并且在到期之前保持不变。我的推论是无效化不起作用。

我做错了吗?你能帮我解决问题吗?请参阅下面的代码和配置。

配置/包/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 FOS\HttpCache\SymfonyCache\CacheInvalidation;
use FOS\HttpCache\SymfonyCache\CustomTtlListener;
use FOS\HttpCache\SymfonyCache\DebugListener;
use FOS\HttpCache\SymfonyCache\EventDispatchingHttpCache;
use FOS\HttpCache\SymfonyCache\PurgeListener;
use FOS\HttpCache\SymfonyCache\RefreshListener;
use FOS\HttpCache\SymfonyCache\UserContextListener;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
use Symfony\Component\HttpKernel\HttpCache\Store;
use Symfony\Component\HttpKernel\HttpKernelInterface;

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/内核.php

<?php
namespace App;

use FOS\HttpCache\SymfonyCache\HttpCacheAware;
use FOS\HttpCache\SymfonyCache\HttpCacheProvider;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\Component\Routing\RouteCollectionBuilder;

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

公共/index.php

<?php
use App\Kernel;
use Symfony\Component\ErrorHandler\Debug;
use Symfony\Component\HttpFoundation\Request;

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

我的控制器之一,src/Controller/SectionController.php(注意:路由在 YAML 文件中定义)

<?php

namespace App\Controller;

use App\Entity\Section;
use App\Entity\SectionCollection;
use App\Form\SectionType;
use FOS\HttpCacheBundle\Configuration\InvalidateRoute;
use FOS\RestBundle\Controller\AbstractFOSRestController;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\View\View;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class SectionController extends AbstractFOSRestController
{
    /**
     * List all sections.
     *
     * @Rest\View
     * @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.
     *
     * @Rest\View
     * @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.
     *
     * @Rest\View
     * @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.
     *
     * @Rest\View
     * @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.
     *
     * @Rest\View
     * @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.
     *
     * @Rest\View
     * @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.
     *
     * @Rest\View(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();
        }
    }
}

标签: symfonycachingcache-invalidationfoshttpcachebundle

解决方案


经过几天的搜索,我自己找到了解决方案。

CacheKernel中,我Symfony\Component\HttpKernel\HttpCache\HttpCache按照 FOSHttpCache 文档中的描述进行扩展。但是,该类必须Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache按照 Symfony 文档中的描述进行扩展。结果,构造函数也发生了变化。

老实说,我不知道这两个类之间的区别,但如果你想拥有一个内置的功能反向代理,你必须使用第二个。它现在对我有用。

我把src/CacheKernel.php的最终代码放在这里:

<?php

namespace App;

use FOS\HttpCache\SymfonyCache\CacheInvalidation;
use FOS\HttpCache\SymfonyCache\CustomTtlListener;
use FOS\HttpCache\SymfonyCache\DebugListener;
use FOS\HttpCache\SymfonyCache\EventDispatchingHttpCache;
use FOS\HttpCache\SymfonyCache\PurgeListener;
use FOS\HttpCache\SymfonyCache\RefreshListener;
use FOS\HttpCache\SymfonyCache\UserContextListener;
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;

class CacheKernel extends HttpCache implements CacheInvalidation
{
    use EventDispatchingHttpCache;

    /**
     * Overwrite constructor to register event listeners for FOSHttpCache.
     */
    public function __construct(HttpKernelInterface $kernel)
    {
        parent::__construct($kernel, $kernel->getCacheDir());

        $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.
     *
     * {@inheritDoc}
     */
    public function fetch(Request $request, $catch = false)
    {
        return parent::fetch($request, $catch);
    }
}

其余代码不变。

希望能帮助到你。再见。


推荐阅读