FOSHttpCacheBundle
FOSHttpCacheBundle copied to clipboard
Support controllers as invalidation targets
When using the YML configuration to define invalidation, you can specify a list of routes to invalidate the cache for when a particular "match" is made.
However, I'm caching a ESI controller, and it would make sense to invalidate this controller's cache but it doesn't have a route of its own. Can the "routes" support controller names perhaps? Or add an additional option alongside routes called "controllers" which contains an array of controllers to invalidate - e.g. the bottom 4 lines of this
fos_http_cache:
cache_control:
defaults:
overwrite: true
rules:
-
match:
attributes:
_controller: "AcmeBlog:Post:loadPosts"
invalidation:
rules:
-
match:
attributes: # When hitting these routes
_route: "comments_update"
routes: # Invalidate cache for the following routes
comments_index
comment_show
controllers: # Invalidate cache for the following controllers
"AcmeBlog:Post:loadPosts"
Is this the right approach? Any suggestions appreciated.
Thanks
Ben
As I workaround I have been able to get this working by physically defining a route for the controller:
fos_http_cache:
cache_control:
defaults:
overwrite: true
rules:
-
match:
attributes:
_route: ^load_posts_route$
invalidation:
rules:
-
match:
attributes: # When hitting these routes
_route: "comments_update"
routes: # Invalidate cache for the following routes
load_posts_route
And then in app/config/routing.yml
load_posts_route:
defaults: { _controller: AcmeBlog:Post:loadPosts }
path: /some-path
i think you are asking for #69 ?
can you check if that is the case? and would you be motivated to work on that? the idea was around for quite a while but we did not work on it yet... i am glad to help find out how and review a pull request.
@dbu TBH I'm not really sure if #69 is the same issue or not.
My original question was that seeing as you can match based on a controller string e.g. "AcmeBlog:Post:loadPosts" then could it be possible to also hang the invalidation off the controller string as well, rather than the list of routes.
Currently, you can invalidate paths and routes. Invalidation controllers, which is what @benr77 asks for, is not the same thing as #69.
Eventually, it’s always a URL (at the HTTP level) that we invalidate. I don’t think Symfony allows getting the path (or URL) for a controller if it has no route defined. The path that goes to that controller, after all, is defined in the routing. On the other hand, app/console debug:router --show-controllers does show the controller for each route. I guess we could use something similar to get the route that belongs to a controller.
OK. I have got things working for now. I have discovered that when you use a route to define an invalidation rule, you must also use the route format for any sub-requests.
e.g. invalidation of an ESI controller render will fail if you use the controller format
{{ render_esi(controller('AcmeBlog:Post:loadPosts')) }}
I have also found that the controller() method causes problems with setting up the caching in the first place - if you have two or more render_esi() methods they will both fail to cache. Switching them both to the route method described below fixes things up.
To make invalidation work you must define a route for the sub-request and then use the route to create a URL in the render_esi() method:
{{ render_esi(path('load_posts_route')) }}
with app/config/routing.yml
load_posts_route:
defaults: { _controller: AcmeBlog:Post:loadPosts }
path: /some-path
Unfortunately this means that you need to hard-code a route for every ESI you have. Not too much of a hassle but not ideal.
that workaround is one option. be sure to secure access to the fragment routes however, see for example http://stackoverflow.com/questions/22295858/how-do-i-invalidate-cache-for-a-controller-url
your other option would be creating a ControllerReference (just with new, there is no magic involved) and then throw that at the EsiFragmentHandler::render method (service fragment.renderer.esi) to get the url. we could add esi invalidation that way. the tricky part would however be knowing the right controller parameters and render options. so i fear rendering the esi path would need to be done by the user - so it can just happen inside a controller rather than by configuration.
Sorry to relaunch this issue but I'm facing the same problem and I'm not really satisfy by the esi route path workaround. I think it can be a good idea to deep on the ESIFragementHandler::render method and find a way to encapsulated those things into the FOSHTTPCacheBundle. @dbu I'm not really sure to well understand the way you want to use this into the ControllerReference ? can you explain it a little bit more ?
its been a while. i think the idea was to create a ControllerReference object and use the esi fragment render method to get the url to that fragment. then you can invalidate that. as this depends on parameters, there is no generic solution.
i think #69 would solve this in an elegant way: the RequestMatcher can match on a _controller request attribute. then you could define rules that match on a controller name instead of a route. as this is a regular expression match, it could be quite useful in this scenario.
Any news on this issue?
i think the idea was to create a ControllerReference object and use the esi fragment render method to get the url to that fragment. then you can invalidate that.
I've tried this but uri that goes from proxy client is different from what I've got:
/_fragment?_hash=nARJAl324p3l5l0EO2XE02uVhG96Lafpnnq3auqh6%2BQ%3D&_path=_format%3Dhtml%26_locale%3Dru%26_controller%3DAppBundle%253APage%252FCatalog%253AbrandsWidget
vs
/_fragment?_path=_format%3Dhtml%26_locale%3Dru%26_controller%3DAppBundle%253APage%252FCatalog%253AbrandsWidget&_hash=nARJAl324p3l5l0EO2XE02uVhG96Lafpnnq3auqh6%2BQ%3D
afaik nobody is actively working on this. if you have the time to figure out the details and propose documentation or code updates, they are welcome and i would review them.
As workaround I use this helper class:
namespace AppBundle\Cache;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Controller\ControllerReference;
use Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface;
class FragmentPathResolver
{
/**
* @var FragmentRendererInterface
*/
private $fragmentRenderer;
/**
* @var RequestStack
*/
private $requestStack;
public function __construct(FragmentRendererInterface $fragmentRenderer, RequestStack $requestStack)
{
$this->fragmentRenderer = $fragmentRenderer;
$this->requestStack = $requestStack;
}
/**
* @param string $controller
* @param array $parameters
* @param array $query
* @return null|string
*/
public function resolve($controller, array $parameters = array(), array $query = array())
{
$controllerReference = new ControllerReference($controller, $parameters, $query);
$response = $this->fragmentRenderer->render($controllerReference, $this->requestStack->getMasterRequest());
return $this->parsePath($response->getContent());
}
/**
* @param string $content
* @return null|string
*/
private function parsePath($content)
{
if (empty($content)) {
return null;
}
if (!preg_match('/src=[\'"](.*?)[\'"]/iu', $content, $matches)) {
return null;
}
return $this->fixOrder($matches[1]);
}
/**
* @param $uri
* @return string
*/
private function fixOrder($uri)
{
$hashPosition = strpos($uri, '&_hash=');
if ($hashPosition === false) {
return null;
}
$hash = substr($uri, $hashPosition + 1);
$uri = substr($uri, 0, $hashPosition);
$queryPosition = strpos($uri, '?');
if ($queryPosition === false) {
return null;
}
$fragment = substr($uri, 0, $queryPosition + 1);
$query = substr($uri, $queryPosition + 1);
return sprintf('%s%s&%s', $fragment, $hash, $query);
}
}
thanks @BoShurik ! i really don't know how the details how symfony fragment rendering works. it would be nice to have the bundle support this conveniently. if you have a good idea and manage to implement that from this code, with some tests that make sure it keeps working with symfony 2 and 3, i am happy to merge it into this bundle. if you don't have the time for that, a pull request on the documentation with a cookbook-like article with that code would probably be easier to do and would be welcome too.