├── .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 | [![Build Status](https://secure.travis-ci.org/liip/LiipCacheControlBundle.png)](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 | --------------------------------------------------------------------------------