├── .gitignore
├── LiipCacheControlBundle.php
├── Tests
├── bootstrap.php
└── EventListener
│ └── CacheControlListenerTest.php
├── Resources
├── config
│ ├── authorization_request_listener.xml
│ ├── flash_message_listener.xml
│ ├── varnish_helper.xml
│ └── rule_response_listener.xml
└── meta
│ └── LICENSE
├── phpunit.xml.dist
├── .travis.yml
├── EventListener
├── CacheAuthorizationListener.php
├── FlashMessageListener.php
└── CacheControlListener.php
├── composer.json
├── Command
└── InvalidateVarnishCommand.php
├── MIGRATE_FOS.md
├── DependencyInjection
├── LiipCacheControlExtension.php
└── Configuration.php
├── Helper
└── Varnish.php
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor/
2 | composer.lock
3 | composer.phar
--------------------------------------------------------------------------------
/LiipCacheControlBundle.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | ./Tests
8 |
9 |
10 |
11 |
12 |
13 | ./
14 |
15 | ./Resources
16 | ./Tests
17 | ./vendor
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 5.3
5 | - 5.4
6 | - 5.5
7 | - 5.6
8 | - hhvm
9 |
10 | env:
11 | - SYMFONY_VERSION=2.3.*
12 |
13 | before_script:
14 | - composer require symfony/framework-bundle:${SYMFONY_VERSION}
15 |
16 | script: phpunit --coverage-text
17 |
18 | notifications:
19 | email:
20 | - travis-ci@liip.ch
21 |
22 | matrix:
23 | allow_failures:
24 | - env: SYMFONY_VERSION=dev-master
25 | - php: hhvm
26 | include:
27 | - php: 5.5
28 | env: SYMFONY_VERSION=2.1.*
29 | - php: 5.5
30 | env: SYMFONY_VERSION=2.2.*
31 | - php: 5.5
32 | env: SYMFONY_VERSION=2.4.*
33 | - php: 5.5
34 | env: SYMFONY_VERSION=2.5.*
35 | - php: 5.5
36 | env: SYMFONY_VERSION=dev-master
37 |
--------------------------------------------------------------------------------
/Resources/config/flash_message_listener.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | %liip_cache_control.flash_message_listener.options%
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/Resources/config/varnish_helper.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 | %liip_cache_control.varnish.host%
11 | %liip_cache_control.varnish.ips%
12 | %liip_cache_control.varnish.port%
13 | %liip_cache_control.varnish.purge_instruction%
14 | %liip_cache_control.varnish.headers%
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/EventListener/CacheAuthorizationListener.php:
--------------------------------------------------------------------------------
1 | getRequest();
21 |
22 | if ($request->getMethod() == 'HEAD') {
23 | // return a 200 "OK" Response to stop processing
24 | $response = new Response('', 200);
25 | $event->setResponse($response);
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Resources/config/rule_response_listener.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | %liip_cache_control.debug%
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/Resources/meta/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2010-2011 Liip
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is furnished
8 | to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "liip/cache-control-bundle",
3 | "type": "symfony-bundle",
4 | "description": "This Bundle provides a way to set path based cache expiration headers via the app configuration and provides a helper to control the reverse proxy varnish.",
5 | "keywords": ["esi", "varnish", "caching", "http"],
6 | "license": "MIT",
7 | "minimum-stability": "dev",
8 | "require-dev": {
9 | "ext-curl": "*"
10 | },
11 | "authors": [
12 | {
13 | "name": "Liip AG",
14 | "homepage": "http://www.liip.ch/"
15 | },
16 | {
17 | "name": "Community contributions",
18 | "homepage": "https://github.com/liip/LiipThemeBundle/contributors"
19 | }
20 | ],
21 | "require": {
22 | "php": ">=5.3.2",
23 | "symfony/framework-bundle": "~2.1"
24 | },
25 | "autoload": {
26 | "psr-0": { "Liip\\CacheControlBundle": "" }
27 | },
28 | "suggest": {
29 | "ext-curl": "Used by Varnish Helper to construct requests."
30 | },
31 | "target-dir": "Liip/CacheControlBundle",
32 | "extra": {
33 | "branch-alias": {
34 | "dev-master": "1.1-dev"
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Command/InvalidateVarnishCommand.php:
--------------------------------------------------------------------------------
1 | setName('liip:cache-control:varnish:invalidate')
32 | ->setDescription('Clear cached entries from Varnish servers')
33 | ->addArgument(
34 | 'path',
35 | InputArgument::OPTIONAL,
36 | 'What URLs do you want to invalidate? (default: .)'
37 | );
38 | }
39 |
40 | /**
41 | * Executes the current command.
42 | *
43 | * @param InputInterface $input Input
44 | * @param OutputInterface $output Output
45 | *
46 | * @return void
47 | */
48 | protected function execute(InputInterface $input, OutputInterface $output)
49 | {
50 | $container = $this->getContainer();
51 |
52 | $this->logger = $container->get('logger');
53 | $helper = $this->getContainer()->get('liip_cache_control.varnish');
54 |
55 | $path = $input->getArgument('path');
56 | if (!$path) {
57 | $path = ".";
58 | }
59 | $this->getLogger()->notice('Starting clearing varnish with path: "' . $path .'"');
60 |
61 | $helper->invalidatePath($path);
62 |
63 | $this->getLogger()->notice('Done clearing varnish');
64 | }
65 |
66 | /**
67 | * Returns the logger
68 | *
69 | * @return LoggerInterface
70 | */
71 | protected function getLogger()
72 | {
73 | if (null == $this->logger) {
74 | $this->logger = new NullLogger();
75 | }
76 |
77 | return $this->logger;
78 | }
79 | }
--------------------------------------------------------------------------------
/EventListener/FlashMessageListener.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | class FlashMessageListener
17 | {
18 | /**
19 | * @var array
20 | */
21 | private $options;
22 |
23 | /**
24 | * @var Session
25 | */
26 | private $session;
27 |
28 | /**
29 | * Set a serializer instance
30 | *
31 | * @param Session $session A session instance
32 | * @param array $options
33 | */
34 | public function __construct($session, array $options = array())
35 | {
36 | $this->session = $session;
37 | $this->options = $options;
38 | }
39 |
40 | /**
41 | * Moves flash messages from the session to a cookie inside a Response Kernel listener
42 | *
43 | * @param EventInterface $event
44 | */
45 | public function onKernelResponse(FilterResponseEvent $event)
46 | {
47 | if ($event->getRequestType() !== HttpKernel::MASTER_REQUEST) {
48 | return;
49 | }
50 |
51 | // Flash messages are stored in the session. If there is none, there
52 | // can't be any flash messages in it. $session->getFlashBag() would
53 | // create a session, we need to avoid that.
54 | if (!$this->session->isStarted()) {
55 | return;
56 | }
57 |
58 | $flashBag = $this->session->getFlashBag();
59 | $flashes = $flashBag->all();
60 |
61 | if (empty($flashes)) {
62 | return;
63 | }
64 |
65 | $response = $event->getResponse();
66 |
67 | $cookies = $response->headers->getCookies(ResponseHeaderBag::COOKIES_ARRAY);
68 | if (isset($cookies[$this->options['host']][$this->options['path']][$this->options['name']])) {
69 | $rawCookie = $cookies[$this->options['host']][$this->options['path']][$this->options['name']]->getValue();
70 | $flashes = array_merge($flashes, json_decode($rawCookie));
71 | }
72 |
73 | $cookie = new Cookie(
74 | $this->options['name'],
75 | json_encode($flashes),
76 | 0,
77 | $this->options['path'],
78 | $this->options['host'],
79 | $this->options['secure'],
80 | $this->options['httpOnly']
81 | );
82 |
83 | $response->headers->setCookie($cookie);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/MIGRATE_FOS.md:
--------------------------------------------------------------------------------
1 | Migrate to FOSHttpCacheBundle
2 | =============================
3 |
4 | The LiipCacheControlBundle went into maintenance only mode. It is replaced by
5 | the [FOSHttpCacheBundle](https://github.com/FriendsOfSymfony/FOSHttpCacheBundle).
6 |
7 | This new bundle is a lot more flexible and cleaner, as well as better documented
8 | and tested. It is a cleanup of the features found in the LiipCacheControlBundle
9 | with the addition of things from [DirebitHttpCacheBundle](https://github.com/driebit/DriebitHttpCacheBundle).
10 |
11 | This guide should help you to adapt your project. If you have questions or additions,
12 | please use the LiipCacheControlBundle issue tracker. Pull requests on this file are
13 | probably the only thing that will still be merged in the future ;-)
14 |
15 | Configuration changes
16 | ---------------------
17 |
18 | The configuration namespace changes from `liip_cache_control` to `fos_http_cache`.
19 |
20 | ### Configuration format for cache control rules changed
21 |
22 | The rules are now under fos_http_cache.cache_control.rules, match criteria are grouped under `match`
23 | and the headers under a `header` element, with `controls` becoming `cache_control`. For example, this
24 | old configuration:
25 |
26 | ```yaml
27 | liip_cache_control:
28 | rules:
29 | -
30 | path: ^/products.*
31 | controls:
32 | public: true
33 | max_age: 42
34 | reverse_proxy_ttl: 3600
35 | ```
36 |
37 | becomes the following:
38 |
39 | ```yaml
40 | fos_http_cache:
41 | cache_control:
42 | rules:
43 | -
44 | match:
45 | path: ^/products.*
46 | headers:
47 | cache_control:
48 | public: true
49 | max_age: 42
50 | reverse_proxy_ttl: 3600
51 | ```
52 |
53 | Note that the FOSHttpCacheBundle only sets cache headers if the response has a
54 | "safe" status, that is one of 200, 203, 300, 301, 302, 404, 410. You can configure
55 | additional_cacheable_status to add more status, or use a match_response expression
56 | to operate on the Response object.
57 |
58 | ### flash_message_listener becomes flash_message
59 |
60 | The name of this configuration section changed.
61 |
62 | Authorization Listener becomes User Context Subscriber
63 | ------------------------------------------------------
64 |
65 | Instead of simply aborting HEAD requests, the FOSHttpCacheBundle can provide a "context token",
66 | e.g. a hash over the *roles* of a user. With this, it becomes possible to share the cache between
67 | different users sharing the same role. If you where using the authorization listener, you want to
68 | study the [user context](http://foshttpcachebundle.readthedocs.org/en/latest/event-subscribers/user-context.html)
69 | section of the new documentation.
70 |
71 | Varnish client becomes cache manager
72 | ------------------------------------
73 |
74 | The cache manager abstracts from the caching proxy. Currently supported are varnish and nginx, and we
75 | hope to get support for the symfony built in cache as well at some point. Instead of the mess with choosing
76 | between BAN and PURGE, the new bundle supports both of them, along with REFRESH.
77 |
78 | The configuration changes:
79 |
80 | ```yaml
81 | varnish:
82 | purge_instruction: ban
83 | ips: "%varnish_ips%"
84 | port: "%varnish_port%"
85 | host: "%varnish_hostname%"
86 | headers: "%varnish_headers%"
87 | ```
88 |
89 | Becomes:
90 |
91 | ```yaml
92 | proxy_client:
93 | varnish:
94 | servers: "%varnish_ips%"
95 | base_url: "%varnish_hostname%"
96 | guzzle_client: acme.varnish.guzzle.client
97 | ```
98 |
99 | The ip and port are combined in the `server` field. This means that you need to
100 | append the port on each IP: [1.2.3.4:8080, 5.6.7.8:8080]. `host` becomes `base_url`
101 | and may contain a path prefix if needed.
102 |
103 | Extra `headers` are no longer supported, but you get more flexibility by
104 | supplying your guzzle client service if the default client is not good.
105 |
106 | ### Commands
107 |
108 | Instead of the `liip:cache-control:varnish:invalidate` command, you can now use
109 |
110 | * fos:httpcache:invalidate:path
111 | * fos:httpcache:invalidate:regex
112 | * fos:httpcache:invalidate:tag
113 | * fos:httpcache:refresh:path
114 |
115 | The commands are configured as services, allowing you to reuse them (with symfony 2.4 or later)
116 | should you need to work with several caching proxies.
117 |
118 | General Cleanup in your Project
119 | -------------------------------
120 |
121 | Search your codebase for mentions of `CacheControlBundle` and check what classes you are using
122 | and how to replace them. Search for `liip_cache_control` and check what services you are using
123 | and how to replace them.
124 |
--------------------------------------------------------------------------------
/DependencyInjection/LiipCacheControlExtension.php:
--------------------------------------------------------------------------------
1 | processConfiguration($configuration, $configs);
27 |
28 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
29 |
30 | $container->setParameter($this->getAlias().'.debug', $config['debug']);
31 |
32 | if (!empty($config['rules'])) {
33 | $loader->load('rule_response_listener.xml');
34 | foreach ($config['rules'] as $cache) {
35 | // domain is depreciated and will be removed in future
36 | $host = is_null($cache['host']) && $cache['domain'] ? $cache['domain'] : $cache['host'];
37 | $cache['ips'] = (empty($cache['ips'])) ? null : $cache['ips'];
38 |
39 | $matcher = $this->createRequestMatcher(
40 | $container,
41 | $cache['path'],
42 | $host,
43 | $cache['method'],
44 | $cache['ips'],
45 | $cache['attributes'],
46 | $cache['controller']
47 | );
48 |
49 | unset(
50 | $cache['path'],
51 | $cache['method'],
52 | $cache['ips'],
53 | $cache['attributes'],
54 | $cache['domain'],
55 | $cache['host'],
56 | $cache['controller']
57 | );
58 |
59 | $container->getDefinition($this->getAlias().'.response_listener')
60 | ->addMethodCall('add', array($matcher, $cache));
61 | }
62 | }
63 |
64 | if (!empty($config['varnish'])) {
65 |
66 | if (!extension_loaded('curl')) {
67 | throw new RuntimeException('Varnish Helper requires cUrl php extension. Please install it to continue');
68 |
69 | }
70 |
71 | // domain is depreciated and will be removed in future
72 | $host = is_null($config['varnish']['host']) && $config['varnish']['domain'] ? $config['varnish']['domain'] : $config['varnish']['host'];
73 |
74 | $loader->load('varnish_helper.xml');
75 | $container->setParameter($this->getAlias().'.varnish.ips', $config['varnish']['ips']);
76 | $container->setParameter($this->getAlias().'.varnish.host', $host);
77 | $container->setParameter($this->getAlias().'.varnish.port', $config['varnish']['port']);
78 | $container->setParameter($this->getAlias().'.varnish.purge_instruction', $config['varnish']['purge_instruction']);
79 | $container->setParameter($this->getAlias().'.varnish.headers', $config['varnish']['headers']);
80 | }
81 |
82 | if ($config['authorization_listener']) {
83 | $loader->load('authorization_request_listener.xml');
84 | }
85 |
86 | if (!empty($config['flash_message_listener']) && $config['flash_message_listener']['enabled']) {
87 | $loader->load('flash_message_listener.xml');
88 |
89 | $container->setParameter($this->getAlias().'.flash_message_listener.options', $config['flash_message_listener']);
90 | }
91 | }
92 |
93 | protected function createRequestMatcher(ContainerBuilder $container, $path = null, $host = null, $methods = null, $ips = null, array $attributes = array(), $controller = null)
94 | {
95 | if (null !== $controller) {
96 | $attributes['_controller'] = $controller;
97 | }
98 |
99 | $arguments = array($path, $host, $methods, $ips, $attributes);
100 | $serialized = serialize($arguments);
101 | $id = $this->getAlias().'.request_matcher.'.md5($serialized).sha1($serialized);
102 |
103 | if (!$container->hasDefinition($id)) {
104 | // only add arguments that are necessary
105 | $container
106 | ->setDefinition($id, new DefinitionDecorator($this->getAlias().'.request_matcher'))
107 | ->setArguments($arguments)
108 | ;
109 | }
110 |
111 | return new Reference($id);
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/EventListener/CacheControlListener.php:
--------------------------------------------------------------------------------
1 |
17 | */
18 | class CacheControlListener
19 | {
20 | /**
21 | * @var \Symfony\Component\Security\Core\SecurityContext
22 | */
23 | protected $securityContext;
24 |
25 | /**
26 | * @var array
27 | */
28 | protected $map = array();
29 |
30 | /**
31 | * supported headers from Response
32 | *
33 | * @var array
34 | */
35 | protected $supportedHeaders = array(
36 | 'etag' => true,
37 | 'last_modified' => true,
38 | 'max_age' => true,
39 | 's_maxage' => true,
40 | 'private' => true,
41 | 'public' => true,
42 | );
43 |
44 | /**
45 | * add debug header, allows vcl to display debug information
46 | *
47 | * @var bool
48 | */
49 | protected $debug = false;
50 |
51 | /**
52 | * Constructor.
53 | *
54 | * @param \Symfony\Component\Security\Core\SecurityContext $securityContext
55 | * @param Boolean $debug The current debug mode
56 | */
57 | public function __construct(SecurityContext $securityContext = null, $debug = false)
58 | {
59 | $this->securityContext = $securityContext;
60 | $this->debug = $debug;
61 | }
62 |
63 | /**
64 | * @param RequestMatcherInterface $requestMatcher A RequestMatcherInterface instance
65 | * @param array $options An array of options
66 | */
67 | public function add(RequestMatcherInterface $requestMatcher, array $options = array())
68 | {
69 | $this->map[] = array($requestMatcher, $options);
70 | }
71 |
72 | /**
73 | * @param FilterResponseEvent $event
74 | */
75 | public function onKernelResponse(FilterResponseEvent $event)
76 | {
77 | $options = $this->getOptions($event->getRequest());
78 | if ($options) {
79 | $response = $event->getResponse();
80 | if (!empty($options['controls'])) {
81 | $controls = array_intersect_key($options['controls'], $this->supportedHeaders);
82 | $extraControls = array_diff_key($options['controls'], $controls);
83 |
84 | //set supported headers
85 | if (!empty($controls)) {
86 | $response->setCache($this->prepareControls($controls));
87 | }
88 |
89 | //set extra headers, f.e. varnish specific headers
90 | if (!empty($extraControls)) {
91 | $this->setExtraControls($response, $extraControls);
92 | }
93 | }
94 |
95 | if ($this->debug) {
96 | $response->headers->set('X-Cache-Debug', 1, false);
97 | }
98 |
99 | if (isset($options['reverse_proxy_ttl']) && null !== $options['reverse_proxy_ttl']) {
100 | $response->headers->set('X-Reverse-Proxy-TTL', (int) $options['reverse_proxy_ttl'], false);
101 | }
102 |
103 | if (!empty($options['vary'])) {
104 | $response->setVary(array_merge($response->getVary(), $options['vary']), true); //update if already has vary
105 | }
106 | }
107 | }
108 |
109 | /**
110 | * adds extra cache controls
111 | *
112 | * @param Response $response
113 | * @param $controls
114 | */
115 | protected function setExtraControls(Response $response, array $controls)
116 | {
117 | if (!empty($controls['must_revalidate'])) {
118 | $response->headers->addCacheControlDirective('must-revalidate', $controls['must_revalidate']);
119 | }
120 |
121 | if (!empty($controls['proxy_revalidate'])) {
122 | $response->headers->addCacheControlDirective('proxy-revalidate', true);
123 | }
124 |
125 | if (!empty($controls['no_transform'])) {
126 | $response->headers->addCacheControlDirective('no-transform', true);
127 | }
128 |
129 | if (!empty($controls['stale_if_error'])) {
130 | $response->headers->addCacheControlDirective('stale-if-error='.$controls['stale_if_error'], true);
131 | }
132 |
133 | if (!empty($controls['stale_while_revalidate'])) {
134 | $response->headers->addCacheControlDirective('stale-while-revalidate='.$controls['stale_while_revalidate'], true);
135 | }
136 |
137 | if (!empty($controls['no_cache'])) {
138 | $response->headers->remove('Cache-Control');
139 | $response->headers->set('Cache-Control','no-cache', true);
140 | }
141 | }
142 |
143 | /**
144 | * Return the cache options for the current request
145 | *
146 | * @param Request $request
147 | * @return array of settings
148 | */
149 | protected function getOptions(Request $request)
150 | {
151 | foreach ($this->map as $elements) {
152 | if (!empty($elements[1]['unless_role'])
153 | && $this->securityContext
154 | && $this->securityContext->isGranted($elements[1]['unless_role'])
155 | ) {
156 | continue;
157 | }
158 |
159 | if (null === $elements[0] || $elements[0]->matches($request)) {
160 | return $elements[1];
161 | }
162 | }
163 |
164 | return array();
165 | }
166 |
167 | /**
168 | * Create php values for needed controls
169 | *
170 | * @param array $controls
171 | * @return array
172 | */
173 | protected function prepareControls(array $controls)
174 | {
175 | if (isset($controls['last_modified'])) {
176 | // this must be a DateTime, convert from the string in configuration
177 | $controls['last_modified'] = new \DateTime($controls['last_modified']);
178 | }
179 |
180 | return $controls;
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 |
16 | */
17 | class Configuration implements ConfigurationInterface
18 | {
19 | /**
20 | * Generates the configuration tree.
21 | *
22 | * @return TreeBuilder
23 | */
24 | public function getConfigTreeBuilder()
25 | {
26 | $treeBuilder = new TreeBuilder();
27 | $rootNode = $treeBuilder->root('liip_cache_control', 'array');
28 |
29 | $rootNode
30 | ->children()
31 | ->booleanNode('debug')->defaultValue('%kernel.debug%')->end()
32 | ->booleanNode('authorization_listener')->defaultFalse()->end()
33 | ->end()
34 | ;
35 |
36 | $this->addRulesSection($rootNode);
37 | $this->addVarnishSection($rootNode);
38 | $this->addFlashMessageListenerSection($rootNode);
39 |
40 | return $treeBuilder;
41 | }
42 |
43 | private function addRulesSection(ArrayNodeDefinition $rootNode)
44 | {
45 | $rootNode
46 | ->fixXmlConfig('rule', 'rules')
47 | ->children()
48 | ->arrayNode('rules')
49 | ->cannotBeOverwritten()
50 | ->prototype('array')
51 | ->children()
52 | ->scalarNode('unless_role')->defaultNull()->end()
53 | ->scalarNode('path')->defaultNull()->info('URL path info')->end()
54 | ->arrayNode('method')
55 | ->beforeNormalization()->ifString()->then(function($v) { return preg_split('/\s*,\s*/', $v); })->end()
56 | ->useAttributeAsKey('name')
57 | ->prototype('scalar')->end()
58 | ->info('HTTP method')
59 | ->end()
60 | ->arrayNode('ips')
61 | ->beforeNormalization()->ifString()->then(function($v) { return preg_split('/\s*,\s*/', $v); })->end()
62 | ->useAttributeAsKey('name')
63 | ->prototype('scalar')->end()
64 | ->info('List of ips')
65 | ->end()
66 | ->arrayNode('attributes')
67 | ->addDefaultsIfNotSet()
68 | ->cannotBeEmpty()
69 | ->treatNullLike(array())
70 | ->info('Request attributes')
71 | ->end()
72 | ->scalarNode('domain')->defaultNull()->info('depreciated, use host instead')->end()
73 | ->scalarNode('host')->defaultNull()->info('URL host name')->end()
74 | ->scalarNode('controller')->defaultNull()->info('controller action name')->end()
75 | ->scalarNode('reverse_proxy_ttl')->defaultNull()->end()
76 | ->arrayNode('controls')
77 | ->beforeNormalization()->ifString()->then(function($v) { return preg_split('/\s*,\s*/', $v); })->end()
78 | ->useAttributeAsKey('name')
79 | ->prototype('scalar')->end()
80 | ->end()
81 | ->arrayNode('vary')
82 | ->beforeNormalization()->ifString()->then(function($v) { return preg_split('/\s*,\s*/', $v); })->end()
83 | ->prototype('scalar')->end()
84 | ->end()
85 | ->end()
86 | ->end()
87 | ->end()
88 | ->end();
89 | }
90 |
91 | private function addVarnishSection(ArrayNodeDefinition $rootNode)
92 | {
93 | $rootNode
94 | ->children()
95 | ->arrayNode('varnish')
96 | ->children()
97 | ->arrayNode('ips')
98 | ->beforeNormalization()->ifString()->then(function($v) { return preg_split('/\s*,\s*/', $v); })->end()
99 | ->useAttributeAsKey('name')
100 | ->prototype('scalar')->end()
101 | ->end()
102 | ->scalarNode('domain')->defaultNull()->info('depreciated, use host instead')->end()
103 | ->scalarNode('host')->defaultNull()->info('URL host name')->end()
104 | ->scalarNode('port')->defaultNull()->end()
105 | ->arrayNode('headers')
106 | ->treatNullLike(array())
107 | ->prototype('scalar')->end()
108 | ->info('Extra HTTP headers sent to varnish')
109 | ->end()
110 | ->enumNode('purge_instruction')
111 | ->values(array('purge', 'ban'))
112 | ->defaultValue('purge')
113 | ->info('the purge instruction (purge in Varnish 2, ban possible since Varnish 3)')
114 | ->end()
115 | ->end()
116 | ->end()
117 | ->end();
118 | }
119 |
120 | private function addFlashMessageListenerSection(ArrayNodeDefinition $rootNode)
121 | {
122 | $rootNode
123 | ->children()
124 | ->arrayNode('flash_message_listener')
125 | ->canBeUnset()
126 | ->treatFalseLike(array('enabled' => false))
127 | ->treatTrueLike(array('enabled' => true))
128 | ->treatNullLike(array('enabled' => true))
129 | ->children()
130 | ->scalarNode('enabled')->defaultTrue()->end()
131 | ->scalarNode('name')->defaultValue('flashes')->end()
132 | ->scalarNode('path')->defaultValue('/')->end()
133 | ->scalarNode('domain')->defaultNull()->info('depreciated, use host instead')->end()
134 | ->scalarNode('host')->defaultNull()->info('URL host name')->end()
135 | ->scalarNode('secure')->defaultFalse()->end()
136 | ->scalarNode('httpOnly')->defaultTrue()->end()
137 | ->end()
138 | ->end()
139 | ->end();
140 | }
141 |
142 | }
143 |
--------------------------------------------------------------------------------
/Tests/EventListener/CacheControlListenerTest.php:
--------------------------------------------------------------------------------
1 | getMockBuilder('Liip\CacheControlBundle\EventListener\CacheControlListener')
20 | ->setMethods(array('getOptions'))
21 | ->getMock();
22 |
23 | $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
24 | $response = new Response();
25 | $request = new Request();
26 | $event = new FilterResponseEvent($kernel, $request, 'GET', $response);
27 | $headers = array( 'controls' => array(
28 | 'etag' => '1337',
29 | 'last_modified' => '13.07.2003',
30 | 'max_age' => '900',
31 | 's_maxage' => '300',
32 | 'public' => true,
33 | 'private' => false
34 | ));
35 |
36 | $listener->expects($this->once())->method('getOptions')->will($this->returnValue($headers));
37 |
38 | $listener->onKernelResponse($event);
39 |
40 | $newHeaders = $response->headers->all();
41 |
42 | $this->assertEquals('max-age=900, public, s-maxage=300', $newHeaders['cache-control'][0]);
43 | $this->assertEquals(strtotime('13.07.2003'), strtotime($newHeaders['last-modified'][0]));
44 | }
45 |
46 | public function testExtraHeaders()
47 | {
48 | $listener = $this->getMockBuilder('Liip\CacheControlBundle\EventListener\CacheControlListener')
49 | ->setMethods(array('getOptions'))
50 | ->getMock();
51 |
52 | $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
53 | $response = new Response();
54 | $request = new Request();
55 | $event = new FilterResponseEvent($kernel, $request, 'GET', $response);
56 | $headers = array( 'controls' => array(
57 | 'must_revalidate' => true,
58 | 'proxy_revalidate' => true,
59 | 'no_transform' => true,
60 | 'stale_if_error' => '300',
61 | 'stale_while_revalidate' => '400',
62 | ));
63 |
64 | $listener->expects($this->once())->method('getOptions')->will($this->returnValue($headers));
65 |
66 | $listener->onKernelResponse($event);
67 |
68 | $newHeaders = $response->headers->all();
69 |
70 | $this->assertEquals('must-revalidate, no-transform, proxy-revalidate, stale-if-error=300, stale-while-revalidate=400, private', $newHeaders['cache-control'][0]);
71 | }
72 |
73 | public function testCompoundHeaders()
74 | {
75 | $listener = $this->getMockBuilder('Liip\CacheControlBundle\EventListener\CacheControlListener')
76 | ->setMethods(array('getOptions'))
77 | ->getMock();
78 |
79 | $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
80 | $response = new Response();
81 | $request = new Request();
82 | $event = new FilterResponseEvent($kernel, $request, 'GET', $response);
83 | $headers = array( 'controls' => array(
84 | 'etag' => '1337',
85 | 'last_modified' => '13.07.2003',
86 | 'max_age' => '900',
87 | 's_maxage' => '300',
88 | 'public' => true,
89 | 'private' => false,
90 | 'must_revalidate' => true,
91 | 'proxy_revalidate' => true,
92 | 'no_transform' => true,
93 | 'stale_if_error' => '300',
94 | 'stale_while_revalidate' => '400',
95 | ));
96 |
97 | $listener->expects($this->once())->method('getOptions')->will($this->returnValue($headers));
98 |
99 | $listener->onKernelResponse($event);
100 |
101 | $newHeaders = $response->headers->all();
102 |
103 | $this->assertEquals('max-age=900, must-revalidate, no-transform, proxy-revalidate, public, s-maxage=300, stale-if-error=300, stale-while-revalidate=400', $newHeaders['cache-control'][0]);
104 | }
105 |
106 | public function testSetNoCacheHeaders()
107 | {
108 | $listener = $this->getMockBuilder('Liip\CacheControlBundle\EventListener\CacheControlListener')
109 | ->setMethods(array('getOptions'))
110 | ->getMock();
111 |
112 | $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
113 | $response = new Response();
114 | $request = new Request();
115 | $event = new FilterResponseEvent($kernel, $request, 'GET', $response);
116 | $headers = array( 'controls' => array(
117 | 'etag' => '1337',
118 | 'last_modified' => '13.07.2003',
119 | 'max_age' => '900',
120 | 's_maxage' => '300',
121 | 'public' => true,
122 | 'private' => false,
123 | 'no_cache' => true,
124 | 'must_revalidate' => true,
125 | 'proxy_revalidate' => true,
126 | 'no_transform' => true,
127 | 'stale_if_error' => '300',
128 | 'stale_while_revalidate' => '400',
129 | ));
130 |
131 | $listener->expects($this->once())->method('getOptions')->will($this->returnValue($headers));
132 |
133 | $listener->onKernelResponse($event);
134 |
135 | $newHeaders = $response->headers->all();
136 |
137 | $this->assertEquals('no-cache, private', $newHeaders['cache-control'][0]);
138 | }
139 |
140 | public function testConfigDefineRequestMatcherWithControllerName() {
141 | $extension = new LiipCacheControlExtension();
142 | $container = new ContainerBuilder();
143 |
144 | // Load configuration
145 | $extension->load(array(
146 | array('rules' =>
147 | array(
148 | array('controller' => '^AcmeBundle:Default:index$', 'controls' => array())
149 | )
150 | )
151 | ), $container);
152 |
153 | // Extract the corresponding definition
154 | $matcherDefinition = null;
155 | foreach ($container->getDefinitions() as $definition) {
156 | if ($definition instanceof DefinitionDecorator &&
157 | $definition->getParent() === 'liip_cache_control.request_matcher'
158 | ) {
159 | $matcherDefinition = $definition;
160 | }
161 | }
162 |
163 | // definition should exist
164 | $this->assertNotNull($matcherDefinition);
165 |
166 | // 4th argument should contain the controller name value
167 | $this->assertEquals(array('_controller' => '^AcmeBundle:Default:index$'), $matcherDefinition->getArgument(4));
168 | }
169 |
170 | public function testMatchRuleWithActionName()
171 | {
172 | $listener = new \Liip\CacheControlBundle\EventListener\CacheControlListener();
173 |
174 | $headers = array( 'controls' => array(
175 | 'etag' => '1337',
176 | 'last_modified' => '13.07.2003',
177 | 'max_age' => '900',
178 | 's_maxage' => '300',
179 | 'public' => true,
180 | 'private' => false
181 | ));
182 |
183 | $listener->add(
184 | new RequestMatcher(null, null, null, null, array('_controller' => '^AcmeBundle:Default:index$')),
185 | $headers
186 | );
187 |
188 | // Request with a matching controller name
189 | $kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
190 | $request = new Request();
191 | $request->attributes->set('_controller', 'AcmeBundle:Default:index');
192 | $response = new Response();
193 | $event = new FilterResponseEvent($kernel, $request, 'GET', $response);
194 |
195 | $listener->onKernelResponse($event);
196 |
197 | $newHeaders = $response->headers->all();
198 |
199 | $this->assertEquals('max-age=900, public, s-maxage=300', $newHeaders['cache-control'][0]);
200 | $this->assertEquals(strtotime('13.07.2003'), strtotime($newHeaders['last-modified'][0]));
201 |
202 | // Request with a non-matching controller name
203 | $request = new Request();
204 | $request->attributes->set('_controller', 'AcmeBundle:Default:notIndex');
205 | $response = new Response();
206 | $event = new FilterResponseEvent($kernel, $request, 'GET', $response);
207 |
208 | $listener->onKernelResponse($event);
209 |
210 | $newHeaders = $response->headers->all();
211 |
212 | $this->assertEquals('no-cache', $newHeaders['cache-control'][0]);
213 | }
214 | }
215 |
--------------------------------------------------------------------------------
/Helper/Varnish.php:
--------------------------------------------------------------------------------
1 | host = $url['host'];
80 | if (isset($url['port'])) {
81 | $this->host .= ':' . $url['port'];
82 | }
83 | } else {
84 | $this->host = $host;
85 | }
86 | $this->ips = $ips;
87 | $this->port = $port;
88 | $this->purgeInstruction = $purgeInstruction;
89 | $this->headers = $headers;
90 | }
91 |
92 | /**
93 | * Purge this path at all registered cache server.
94 | * See https://www.varnish-cache.org/docs/trunk/users-guide/purging.html
95 | *
96 | * @param string $path Path to be purged, since varnish 3 this can
97 | * also be a regex for banning
98 | * @param array $options Options for cUrl Request
99 | * @param string $contentType Banning option: invalidate all or fe. only html
100 | * @param array $hosts Banning option: hosts to ban, leave null to
101 | * use default host and an empty array to ban
102 | * all hosts
103 | *
104 | * @return array An associative array with keys 'headers' and 'body' which
105 | * holds a raw response from the server
106 | *
107 | * @throws \RuntimeException if connection to one of the varnish servers fails.
108 | */
109 | public function invalidatePath($path, array $options = array(), $contentType = self::CONTENT_TYPE_ALL, array $hosts = null)
110 | {
111 | if ($this->purgeInstruction === self::PURGE_INSTRUCTION_BAN) {
112 | return $this->requestBan($path, $contentType, $hosts, $options);
113 | } else {
114 | return $this->requestPurge($path, $options);
115 | }
116 | }
117 |
118 | /**
119 | * Force this path to be refreshed
120 | *
121 | * @param string $path Path to be refreshed
122 | * @param array $options Options for cUrl Request
123 | *
124 | * @return array An associative array with keys 'headers' and 'body' which
125 | * holds a raw response from the server
126 | * @throws \RuntimeException if connection to one of the varnish servers fails.
127 | */
128 | public function refreshPath($path, array $options = array())
129 | {
130 | $headers = array("Cache-Control: no-cache, no-store, max-age=0, must-revalidate");
131 |
132 | $options[CURLOPT_CUSTOMREQUEST] = 'GET';
133 |
134 | return $this->sendRequestToAllVarnishes($path, $headers, $options);
135 | }
136 |
137 | /**
138 | * Do a request using the purge instruction
139 | *
140 | * @param string $path Path to be purged
141 | * @param array $options Options for cUrl Request
142 | *
143 | * @return array An associative array with keys 'headers' and 'body' which
144 | * holds a raw response from the server
145 | * @throws \RuntimeException if connection to one of the varnish servers fails.
146 | */
147 | protected function requestPurge($path, array $options = array())
148 | {
149 | $headers = array(
150 | sprintf('Host: %s', $this->host),
151 | );
152 |
153 | //Garanteed to be a purge request
154 | $options[CURLOPT_CUSTOMREQUEST] = 'PURGE';
155 |
156 | return $this->sendRequestToAllVarnishes($path, $headers, $options);
157 | }
158 |
159 | /**
160 | * Do a request using the ban instruction (available since varnish 3)
161 | *
162 | * @param string $path Path to be purged, this can also be a regex
163 | * @param string $contentType Invalidate all or fe. only html
164 | * @param array $hosts Hosts to ban, leave null to use default host
165 | * and an empty array to ban all hosts
166 | * @param array $options Options for cUrl Request
167 | *
168 | * @return array An associative array with keys 'headers' and 'body' which
169 | * holds a raw response from the server
170 | * @throws \RuntimeException if connection to one of the varnish servers fails.
171 | */
172 | protected function requestBan($path, $contentType = self::CONTENT_TYPE_ALL, array $hosts = null, array $options = array())
173 | {
174 | $hosts = is_null($hosts) ? array($this->host) : $hosts;
175 | $hostRegEx = count($hosts) > 0 ? '^('.join('|', $hosts).')$' : '.*';
176 |
177 | $headers = array(
178 | sprintf('%s: %s', self::PURGE_HEADER_HOST, $hostRegEx),
179 | sprintf('%s: %s', self::PURGE_HEADER_REGEX, $path),
180 | sprintf('%s: %s', self::PURGE_HEADER_CONTENT_TYPE, $contentType),
181 | );
182 |
183 | //Garanteed to be a purge request
184 | $options[CURLOPT_CUSTOMREQUEST] = 'PURGE';
185 |
186 | return $this->sendRequestToAllVarnishes('/', $headers, $options);
187 | }
188 |
189 | /**
190 | * Send a request to all configured varnishes
191 | *
192 | * @param string $path URL path for request
193 | * @param array $headers Headers for cUrl Request
194 | * @param array $options Options for cUrl Request
195 | *
196 | * @return array An associative array with keys 'headers', 'body', 'error'
197 | * and 'errorNumber' for each configured Ip
198 | * @throws \RuntimeException if connection to one of the varnish servers fails. TODO: should we be more tolerant?
199 | */
200 | protected function sendRequestToAllVarnishes($path, array $headers = array(), array $options = array())
201 | {
202 | $requestResponseByIp = array();
203 | $curlHandler = curl_init();
204 |
205 | if (isset($options[CURLOPT_HTTPHEADER])) {
206 | $options[CURLOPT_HTTPHEADER] = array_merge($headers, $options[CURLOPT_HTTPHEADER]);
207 | } else {
208 | $options[CURLOPT_HTTPHEADER] = $headers;
209 | }
210 |
211 | $options[CURLOPT_HTTPHEADER] = array_merge($this->headers, $options[CURLOPT_HTTPHEADER]);
212 |
213 | foreach ($options as $option => $value) {
214 | curl_setopt($curlHandler, (int) $option, $value);
215 | }
216 |
217 | //Default Options
218 | curl_setopt($curlHandler, CURLOPT_RETURNTRANSFER, true);
219 | curl_setopt($curlHandler, CURLOPT_HEADER, true); // Display headers
220 |
221 | foreach ($this->ips as $ip) {
222 |
223 | curl_setopt($curlHandler, CURLOPT_URL, $ip.':'.$this->port.$path);
224 |
225 | $response = curl_exec($curlHandler);
226 |
227 | //Failed
228 | if ($response === false) {
229 | $header = '';
230 | $body = '';
231 | $error = curl_error($curlHandler);
232 | $errorNumber = curl_errno($curlHandler);
233 |
234 | } else {
235 | $error = null;
236 | $errorNumber = CURLE_OK;
237 | list($header, $body) = explode("\r\n\r\n", $response, 2);
238 | }
239 |
240 | $requestResponseByIp[$ip] = array('headers' => $header,
241 | 'body' => $body,
242 | 'error' => $error,
243 | 'errorNumber' => $errorNumber);
244 |
245 | }
246 |
247 | curl_close($curlHandler);
248 |
249 | return $requestResponseByIp;
250 | }
251 |
252 | }
253 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | UNMAINTAINED
2 | ============
3 |
4 | This bundle is no longer maintained. Feel free to fork it if needed.
5 |
6 | CacheControlBundle
7 | ==================
8 |
9 | This Bundle provides a way to set path based cache expiration headers via the
10 | app configuration and provides a helper to control the reverse proxy varnish.
11 |
12 |
13 | [](http://travis-ci.org/liip/LiipCacheControlBundle)
14 |
15 |
16 | This Bundle is Deprecated!
17 | ==========================
18 |
19 | The LiipCacheControlBundle went into maintenance only mode. It is replaced by
20 | the [FOSHttpCacheBundle](https://github.com/FriendsOfSymfony/FOSHttpCacheBundle).
21 |
22 | See our [migration guide](MIGRATE_FOS.md) for help how to transition to the new bundle.
23 |
24 | This repository will stay available to not break existing installations, but
25 | there will only be minimal maintenance at most.
26 |
27 |
28 | Installation with composer
29 | ==========================
30 |
31 | Just add the following line to your projects composer.json require section:
32 |
33 | ```
34 | "liip/cache-control-bundle": "~1.0"
35 | ```
36 |
37 |
38 | Enable the module
39 | =================
40 |
41 | Add this bundle to your application's kernel:
42 |
43 | ``` php
44 | // application/ApplicationKernel.php
45 | public function registerBundles()
46 | {
47 | return array(
48 | // ...
49 | new Liip\CacheControlBundle\LiipCacheControlBundle(),
50 | // ...
51 | );
52 | }
53 | ```
54 |
55 | Cache control
56 | =============
57 |
58 | Simply configure as many paths as needed with the given cache control rules:
59 |
60 | ``` yaml
61 | # app/config.yml
62 | liip_cache_control:
63 | rules:
64 | # the controls section values are used in a call to Response::setCache();
65 | - { path: ^/, controls: { public: true, max_age: 15, s_maxage: 30, last_modified: "-1 hour" }, vary: [Accept-Encoding, Accept-Language] }
66 |
67 | # only match login.example.com
68 | - { host: ^login.example.com$, controls: { public: false, max_age: 0, s_maxage: 0, last_modified: "-1 hour" }, vary: [Accept-Encoding, Accept-Language] }
69 |
70 | # match a specific controller action
71 | - { controller: ^AcmeBundle:Default:index$, controls: { public: true, max_age: 15, s_maxage: 30, last_modified: "-1 hour" }, vary: [Accept-Encoding, Accept-Language] }
72 |
73 | ```
74 |
75 | The matches are tried from top to bottom, the first match is taken and applied.
76 |
77 | Run ``app/console config:dump-reference liip_cache_control`` to get the full list of configuration options.
78 |
79 | About the path parameter
80 | ------------------------
81 |
82 | The ``path``, ``host`` and ``controller`` parameter of the rules represent a regular
83 | expression that a page must match to use the rule.
84 |
85 | For this reason, and it's probably not the behaviour you'd have expected, the
86 | path ``^/`` will match any page.
87 |
88 | If you just want to match the homepage you need to use the path ``^/$``.
89 |
90 | To match pages URLs with caching rules, this bundle uses the class
91 | ``Symfony\Component\HttpFoundation\RequestMatcher``.
92 |
93 | The ``unless_role`` makes it possible to skip rules based on if the current
94 | authenticated user has been granted the provided role.
95 |
96 | Debug information
97 | -----------------
98 |
99 | The debug parameter adds a ``X-Cache-Debug`` header to each response that you
100 | can use in your Varnish configuration.
101 |
102 | ``` yaml
103 | # app/config.yml
104 | liip_cache_control:
105 | debug: true
106 | ```
107 |
108 | Add the following code to your Varnish configuration to have debug headers
109 | added to the response if it is enabled:
110 |
111 | ```
112 | #in sub vcl_deliver
113 | # debug info
114 | # https://www.varnish-cache.org/trac/wiki/VCLExampleHitMissHeader
115 | if (resp.http.X-Cache-Debug) {
116 | if (obj.hits > 0) {
117 | set resp.http.X-Cache = "HIT";
118 | set resp.http.X-Cache-Hits = obj.hits;
119 | } else {
120 | set resp.http.X-Cache = "MISS";
121 | }
122 | set resp.http.X-Cache-Expires = resp.http.Expires;
123 | } else {
124 | # remove Varnish/proxy header
125 | remove resp.http.X-Varnish;
126 | remove resp.http.Via;
127 | remove resp.http.X-Purge-URL;
128 | remove resp.http.X-Purge-Host;
129 | }
130 | ```
131 |
132 | Custom Varnish Parameters
133 | -------------------------
134 |
135 | Additionally to the default supported headers, you may want to set custom
136 | caching headers for varnish.
137 |
138 | ``` yaml
139 | # app/config.yml
140 | liip_cache_control:
141 | rules:
142 | # the controls section values are used in a call to Response::setCache();
143 | - { path: /, controls: { stale_while_revalidate=9000, stale_if_error=3000, must-revalidate=false, proxy_revalidate=true } }
144 | ```
145 |
146 | Custom Varnish Time-Outs
147 | ------------------------
148 |
149 | Varnish checks the `Cache-Control` header of your response to set the TTL.
150 | Sometimes you may want that varnish should cache your response for a longer
151 | time than the browser. This way you can increase the performance by reducing
152 | requests to the backend.
153 |
154 | To achieve this you can set the `reverse_proxy_ttl` option for your rule:
155 |
156 | ``` yaml
157 | # app/config.yml
158 | liip_cache_control:
159 | rules:
160 | # the controls section values are used in a call to Response::setCache();
161 | - { path: /, reverse_proxy_ttl: 300, controls: { public: true, max_age: 15, s_maxage: 30, last_modified: "-1 hour" } }
162 | ```
163 |
164 | This example will add the header `X-Reverse-Proxy-TTL: 300` to your response.
165 |
166 | But by default, varnish will not know anything about it. To get it to work
167 | you have to extend your varnish `vcl_fetch` configuration:
168 |
169 | ```
170 | sub vcl_fetch {
171 |
172 | /* ... */
173 |
174 | if (beresp.http.X-Reverse-Proxy-TTL) {
175 | C{
176 | char *ttl;
177 | ttl = VRT_GetHdr(sp, HDR_BERESP, "\024X-Reverse-Proxy-TTL:");
178 | VRT_l_beresp_ttl(sp, atoi(ttl));
179 | }C
180 | unset beresp.http.X-Reverse-Proxy-TTL;
181 | }
182 |
183 | /* ... */
184 |
185 | }
186 | ```
187 |
188 | Varnish will then look for the `X-Reverse-Proxy-TTL` header and if it exists,
189 | varnish will use the found value as TTL and then remove the header.
190 | There is a beresp.ttl field in VCL but unfortunately it can only be set to
191 | absolute values and not dynamically. Thus we have to use a C code fragment.
192 |
193 | Note that if you are using this, you should have a good purging strategy.
194 |
195 | Varnish helper
196 | ==============
197 |
198 | This helper can be used to talk back to varnish to invalidate cached URLs.
199 | Configure the location of the varnish reverse proxies (be sure not to forget
200 | any, as each varnish must be notified separately):
201 |
202 | ``` yaml
203 | # app/config.yml
204 | liip_cache_control:
205 | varnish:
206 | host: http://www.liip.ch
207 | ips: 10.0.0.10, 10.0.0.11
208 | port: 80
209 | headers: ["Authorization: Basic Zm9vOmJhcg==", "X-Another-Header: here"]
210 | ```
211 |
212 | * **host**: This must match the web host clients are using when connecting to varnish.
213 | You will not notice if this is mistyped, but cache invalidation will never happen.
214 | You can also add a regexp here like ".*" to clear all host entries. The regexp will be
215 | surrounded by "^(" and ")$" ending in "^(.*)$" in this example.
216 | * **ips**: List of IP adresses of your varnish servers. Comma separated.
217 | * **port**: The port varnish is listening on for incoming web connections.
218 | * **headers**: (optional) If you want to send special headers with each request sent to varnish,
219 | you can add them here (as array)
220 |
221 | To use the varnish cache helper you must inject the
222 | ``liip_cache_control.varnish`` service or fetch it from the service container:
223 |
224 | ``` php
225 | // using a "manual" url
226 | $varnish = $this->container->get('liip_cache_control.varnish');
227 | /* $response Is an associative array with keys 'headers', 'body', 'error' and 'errorNumber' for each configured IP.
228 | A sample response will look like:
229 | array('10.0.0.10' => array('body' => 'raw-request-body',
230 | 'headers' => 'raw-headers',
231 | 'error' => 'curl-error-msg',
232 | 'errorNumber' => integer-curl-error-number),
233 | '10.0.0.11' => ...)
234 | */
235 | $response = $varnish->invalidatePath('/some/path');
236 |
237 | // using the router to generate the url
238 | $router = $this->container->get('router');
239 | $varnish = $this->container->get('liip_cache_control.varnish');
240 | $response = $varnish->invalidatePath($router->generate('myRouteName'));
241 | ```
242 |
243 | When using ESI, you will want to purge individual fragments. To generate the
244 | corresponding ``_internal`` route, inject the ``http_kernel`` into your controller and
245 | use HttpKernel::generateInternalUri with the parameters as in the twig
246 | ``render`` tag.
247 |
248 | Purging
249 | -------
250 |
251 | Add the following code to your Varnish configuration to have it handle PURGE
252 | requests (make sure to uncomment the appropiate line(s))
253 |
254 | varnish 3.x
255 | ```
256 | #top level:
257 | # who is allowed to purge from cache
258 | # https://www.varnish-cache.org/docs/trunk/users-guide/purging.html
259 | acl purge {
260 | "127.0.0.1"; #localhost for dev purposes
261 | "10.0.11.0"/24; #server closed network
262 | }
263 |
264 | #in sub vcl_recv
265 | # purge if client is in correct ip range
266 | if (req.request == "PURGE") {
267 | if (!client.ip ~ purge) {
268 | error 405 "Not allowed.";
269 | }
270 |
271 | return(lookup);
272 | }
273 |
274 | sub vcl_hit {
275 | if (req.request == "PURGE") {
276 | purge;
277 | error 200 "Purged";
278 | return (error);
279 | }
280 | }
281 |
282 | sub vcl_miss {
283 | if (req.request == "PURGE") {
284 | purge;
285 | error 404 "Not in cache";
286 | return (error);
287 | }
288 | }
289 |
290 | ```
291 |
292 | In Varnish 2, the `purge` action is actually just marking caches as invalid.
293 | This is called `ban` in Varnish 3.
294 |
295 | Varnish 2.x
296 | ```
297 | #top level:
298 | # who is allowed to purge from cache
299 | # https://www.varnish-cache.org/docs/trunk/users-guide/purging.html
300 | acl purge {
301 | "127.0.0.1"; #localhost for dev purposes
302 | "10.0.11.0"/24; #server closed network
303 | }
304 |
305 | #in sub vcl_recv
306 | # purge if client is in correct ip range
307 | if (req.request == "PURGE") {
308 | if (!client.ip ~ purge) {
309 | error 405 "Not allowed.";
310 | }
311 |
312 | purge("req.url ~ " req.url);
313 | purge("req.url ~ " req.url);
314 | error 200 "Success";
315 | }
316 | ```
317 |
318 | NOTE: this code invalidates the url for all domains. If your varnish serves
319 | multiple domains, you should improve this configuration.
320 |
321 | The varnish path invalidation is about equivalent to doing this:
322 |
323 | netcat localhost 6081 << EOF
324 | PURGE /url/to/purge HTTP/1.1
325 | Host: webapp-host.name
326 |
327 | EOF
328 |
329 | Banning
330 | -------
331 |
332 | Since varnish 3 banning can be used to invalidate the cache. Banning
333 | invalidates whole section with regular expressions, so you will need to be
334 | careful to not invalidate too much.
335 |
336 | Configure the varnish reverse proxies to use ban as purge instruction:
337 |
338 | ``` yaml
339 | # app/config.yml
340 | liip_cache_control:
341 | varnish:
342 | purge_instruction: ban
343 | ```
344 |
345 | This will do a purge request and will add X-Purge headers which can be used by
346 | your Varnish configuration:
347 |
348 | varnish 3.x
349 | ```
350 | #top level:
351 | # who is allowed to purge from cache
352 | # https://www.varnish-cache.org/docs/trunk/users-guide/purging.html
353 | acl purge {
354 | "127.0.0.1"; #localhost for dev purposes
355 | "10.0.11.0"/24; #server closed network
356 | }
357 |
358 | #in sub vcl_recv
359 | # purge if client is in correct ip range
360 | if (req.request == "PURGE") {
361 | if (!client.ip ~ purge) {
362 | error 405 "Not allowed.";
363 | }
364 | ban("obj.http.X-Purge-Host ~ " + req.http.X-Purge-Host + " && obj.http.X-Purge-URL ~ " + req.http.X-Purge-Regex + " && obj.http.Content-Type ~ " + req.http.X-Purge-Content-Type);
365 | error 200 "Purged.";
366 | }
367 |
368 | #in sub vcl_fetch
369 | # add ban-lurker tags to object
370 | set beresp.http.X-Purge-URL = req.url;
371 | set beresp.http.X-Purge-Host = req.http.host;
372 |
373 | ```
374 |
375 | Force refresh
376 | -------------
377 |
378 | Alternatively one can also force a refresh using the approach
379 |
380 | ```
381 | #top level:
382 | # who is allowed to purge from cache
383 | # http://www.varnish-cache.org/trac/wiki/VCLExampleEnableForceRefresh
384 | acl refresh {
385 | "127.0.0.1"; #localhost for dev purposes
386 | "10.0.11.0"/24; #server closed network
387 | }
388 |
389 | sub vcl_hit {
390 | if (!obj.cacheable) {
391 | pass;
392 | }
393 |
394 | if (req.http.Cache-Control ~ "no-cache" && client.ip ~ refresh) {
395 | set obj.ttl = 0s;
396 | return (restart);
397 | }
398 | deliver;
399 | }
400 | ```
401 |
402 | The vanish path force refresh is about equivalent to doing this:
403 |
404 | netcat localhost 6081 << EOF
405 | GET /url/to/refresh HTTP/1.1
406 | Host: webapp-host.name
407 | Cache-Control: no-cache, no-store, max-age=0, must-revalidate
408 |
409 | EOF
410 |
411 | To use the varnish cache helper you must inject the
412 | ``liip_cache_control.varnish`` service or fetch it from the service container:
413 |
414 | ``` php
415 | // using a "manual" url
416 | $varnish = $this->container->get('liip_cache_control.varnish');
417 | $varnish->refreshPath('/some/path');
418 | ```
419 |
420 | Banning from the console
421 | ------------------------
422 |
423 | You can also ban URLs from the console
424 |
425 | ``` shell
426 | app/console liip:cache-control:varnish:invalidate
427 | ```
428 |
429 | will ban (invalidate) all entries in your configured varnish servers (matching
430 | varnish.host)
431 |
432 | ``` shell
433 | app/console liip:cache-control:varnish:invalidate /posts.*
434 | ```
435 |
436 | will ban (invalidate) all entries in your configured varnish servers, where the
437 | URL starts with "/posts". Any regular expression understood by varnish can be
438 | used here.
439 |
440 | It uses the Varnish Helper class, therefore if you defined more than one varnish
441 | server in the config file (in varnish.ips), the entries will be deleted in all
442 | servers.
443 |
444 | Cache authorization listener
445 | ============================
446 |
447 | Enable the authorization listener:
448 |
449 | ``` yaml
450 | # app/config.yml
451 | liip_cache_control:
452 | authorization_listener: true
453 | ```
454 |
455 | This listener makes it possible to stop a request with a 200 "OK" for HEAD
456 | requests right after the security firewall has finished. This is useful when
457 | one uses Varnish while handling content that is not available for all users.
458 |
459 | In this scenario on a cache hit, Varnish can be configured to issue a HEAD
460 | request when this content is accessed. This way Symfony2 can be used to
461 | validate the authorization, but no work needs to be made to regenerate the
462 | content that is already in the Varnish cache.
463 |
464 | Note this obviously means that it only works with path based Security. Any
465 | additional security implemented inside the Controller will be ignored.
466 |
467 | Note further that a HEAD response is supposed to contain the same HTTP header
468 | meta data as the GET response to the same URL. However for the purpose of this
469 | use case we have no other choice but to assume a 200.
470 |
471 | ```
472 | backend default {
473 | .host = “127.0.0.1″;
474 | .port = “81″;
475 | }
476 |
477 | acl purge {
478 | “127.0.0.1″; #localhost for dev purposes
479 | }
480 |
481 | sub vcl_recv {
482 | # pipe HEAD requests as we convert all GET requests to HEAD and back later on
483 | if (req.request == “HEAD”) {
484 | return (pipe);
485 | }
486 |
487 |
488 | if (req.request == "GET") {
489 | if (req.restarts == 0) {
490 | set req.request = "HEAD";
491 | return (pass);
492 | } else {
493 | set req.http.Surrogate-Capability = "abc=ESI/1.0";
494 | return (lookup);
495 | }
496 | }
497 | }
498 |
499 | sub vcl_hash {
500 | }
501 |
502 | sub vcl_fetch {
503 | if (beresp.http.Cache-Control ~ “(private|no-cache|no-store)”) {
504 | return (pass);
505 | }
506 |
507 | if (beresp.status >= 200 && beresp.status < 300) {
508 | if (req.request == "HEAD") {
509 | # if the BE response said OK, change the request type back to GET and restart
510 | set req.request = "GET";
511 | restart;
512 | }
513 | } else {
514 | # In any other case (authentication 302 most likely), just pass the response to the client
515 | # Don't forget to set the content-length, as the HEAD response doesn't have any (and the client will hang)
516 | if (req.request == "HEAD") {
517 | set beresp.http.content-length = "0";
518 | }
519 |
520 | return (pass);
521 | }
522 |
523 | if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
524 | unset beresp.http.Surrogate-Control;
525 | // varnish < 3.0:
526 | esi;
527 | // varnish 3.0 and later:
528 | // set beresp.do_esi = true;
529 | }
530 | }
531 | ```
532 |
533 | Flash message listener
534 | ======================
535 |
536 | The Response flash message listener moves all flash messages currently set into
537 | a cookie. This way it becomes possible to better handle flash messages in
538 | combination with ESI. The ESI configuration will need to ignore the configured
539 | cookie. It will then be up to the client to read out the cookie, display the
540 | flash message and remove the flash message via javascript.
541 |
542 | ``` yaml
543 | # app/config.yml
544 | liip_cache_control:
545 | flash_message_listener:
546 | name: flashes
547 | path: /
548 | host: null
549 | secure: false
550 | httpOnly: true
551 | ```
552 |
553 | If you do not want the flash message listener, you can disable it:
554 |
555 | ``` yaml
556 | # app/config.yml
557 | liip_cache_control:
558 | flash_message_listener:
559 | enabled: false
560 | ```
561 |
562 |
563 |
--------------------------------------------------------------------------------