Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support controllers as invalidation targets #233

Open
benr77 opened this issue Jul 16, 2015 · 12 comments
Open

Support controllers as invalidation targets #233

benr77 opened this issue Jul 16, 2015 · 12 comments

Comments

@benr77
Copy link
Contributor

benr77 commented Jul 16, 2015

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

@benr77
Copy link
Contributor Author

benr77 commented Jul 16, 2015

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

@dbu
Copy link
Contributor

dbu commented Jul 16, 2015

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.

@benr77
Copy link
Contributor Author

benr77 commented Jul 16, 2015

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

@ddeboer
Copy link
Member

ddeboer commented Jul 16, 2015

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.

@benr77
Copy link
Contributor Author

benr77 commented Jul 16, 2015

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.

@dbu
Copy link
Contributor

dbu commented Jul 17, 2015

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.

@mamiefurax
Copy link

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 ?

@dbu
Copy link
Contributor

dbu commented Oct 19, 2015

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.

@BoShurik
Copy link

BoShurik commented Jul 5, 2016

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

@dbu
Copy link
Contributor

dbu commented Jul 5, 2016

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.

@BoShurik
Copy link

BoShurik commented Jul 5, 2016

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

@dbu
Copy link
Contributor

dbu commented Jul 7, 2016

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants