├── varnishpurge ├── config.php ├── elementactions │ └── Varnishpurge_PurgeCacheElementAction.php ├── variables │ └── VarnishpurgeVariable.php ├── tasks │ └── Varnishpurge_PurgeTask.php ├── VarnishpurgePlugin.php └── services │ └── VarnishpurgeService.php ├── LICENSE ├── releases.json └── README.md /varnishpurge/config.php: -------------------------------------------------------------------------------- 1 | craft()->getSiteUrl(), 6 | 'purgeEnabled' => isset($_SERVER['HTTP_X_VARNISH']), 7 | 'purgeRelated' => true, 8 | 'logAll' => 0, 9 | 'purgeUrlMap' => [], 10 | ); 11 | -------------------------------------------------------------------------------- /varnishpurge/elementactions/Varnishpurge_PurgeCacheElementAction.php: -------------------------------------------------------------------------------- 1 | varnishpurge->getSetting('purgeEnabled')) { 19 | $elements = $criteria->find(); 20 | craft()->varnishpurge->purgeElements($elements, false); 21 | $this->setMessage(Craft::t('Varnish cache was purged.')); 22 | return true; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /varnishpurge/variables/VarnishpurgeVariable.php: -------------------------------------------------------------------------------- 1 | getSettings()->urls; 18 | $this->_locale = $this->getSettings()->locale; 19 | 20 | $this->_urls = array(); 21 | $this->_urls = array_chunk($urls, 20); 22 | 23 | return count($this->_urls); 24 | } 25 | 26 | public function runStep($step) 27 | { 28 | VarnishpurgePlugin::log('Varnish purge task run step: ' . $step, LogLevel::Info, craft()->varnishpurge->getSetting('logAll')); 29 | 30 | $batch = \Guzzle\Batch\BatchBuilder::factory() 31 | ->transferRequests(20) 32 | ->bufferExceptions() 33 | ->build(); 34 | 35 | $client = new \Guzzle\Http\Client(); 36 | $client->setDefaultOption('headers/Accept', '*/*'); 37 | 38 | foreach ($this->_urls[$step] as $url) { 39 | VarnishpurgePlugin::log('Adding url to purge: ' . $url, LogLevel::Info, craft()->varnishpurge->getSetting('logAll')); 40 | 41 | $request = $client->createRequest('PURGE', $url); 42 | $batch->add($request); 43 | } 44 | 45 | $requests = $batch->flush(); 46 | 47 | foreach ($batch->getExceptions() as $e) { 48 | VarnishpurgePlugin::log('An exception occurred: ' . $e->getMessage(), LogLevel::Error); 49 | } 50 | 51 | $batch->clearExceptions(); 52 | 53 | return true; 54 | } 55 | 56 | protected function defineSettings() 57 | { 58 | return array( 59 | 'urls' => AttributeType::Mixed, 60 | 'locale' => AttributeType::String 61 | ); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /varnishpurge/VarnishpurgePlugin.php: -------------------------------------------------------------------------------- 1 | _name); 22 | } 23 | 24 | public function getUrl() 25 | { 26 | return $this->_url; 27 | } 28 | 29 | public function getVersion() 30 | { 31 | return $this->_version; 32 | } 33 | 34 | public function getDeveloper() 35 | { 36 | return $this->_developer; 37 | } 38 | 39 | public function getDeveloperUrl() 40 | { 41 | return $this->_developerUrl; 42 | } 43 | 44 | public function getDescription() 45 | { 46 | return $this->_description; 47 | } 48 | 49 | public function getDocumentationUrl() 50 | { 51 | return $this->_documentationUrl; 52 | } 53 | 54 | public function getSchemaVersion() 55 | { 56 | return $this->_schemaVersion; 57 | } 58 | 59 | public function getReleaseFeedUrl() 60 | { 61 | return $this->_releaseFeedUrl; 62 | } 63 | 64 | public function getCraftRequiredVersion() 65 | { 66 | return $this->_minVersion; 67 | } 68 | 69 | 70 | public function init() 71 | { 72 | parent::init(); 73 | 74 | if (craft()->varnishpurge->getSetting('purgeEnabled')) { 75 | 76 | $purgeRelated = craft()->varnishpurge->getSetting('purgeRelated'); 77 | 78 | craft()->on('elements.onSaveElement', function (Event $event) use($purgeRelated) { // element saved 79 | craft()->varnishpurge->purgeElement($event->params['element'], $purgeRelated); 80 | }); 81 | 82 | craft()->on('entries.onDeleteEntry', function (Event $event) use($purgeRelated) { //entry deleted 83 | craft()->varnishpurge->purgeElement($event->params['entry'], $purgeRelated); 84 | }); 85 | 86 | craft()->on('elements.onBeforePerformAction', function(Event $event) use($purgeRelated) { //entry deleted via element action 87 | $action = $event->params['action']->classHandle; 88 | if ($action == 'Delete') { 89 | $elements = $event->params['criteria']->find(); 90 | foreach ($elements as $element) { 91 | if ($element->elementType !== 'Entry') { return; } 92 | craft()->varnishpurge->purgeElement($element, $purgeRelated); 93 | } 94 | } 95 | }); 96 | } 97 | } 98 | 99 | public function addEntryActions() 100 | { 101 | $actions = array(); 102 | 103 | if (craft()->varnishpurge->getSetting('purgeEnabled')) { 104 | $purgeAction = craft()->elements->getAction('Varnishpurge_PurgeCache'); 105 | 106 | $purgeAction->setParams(array( 107 | 'label' => Craft::t('Purge cache'), 108 | )); 109 | 110 | $actions[] = $purgeAction; 111 | } 112 | 113 | return $actions; 114 | } 115 | 116 | public function addCategoryActions() 117 | { 118 | $actions = array(); 119 | 120 | if (craft()->varnishpurge->getSetting('purgeEnabled')) { 121 | $purgeAction = craft()->elements->getAction('Varnishpurge_PurgeCache'); 122 | 123 | $purgeAction->setParams(array( 124 | 'label' => Craft::t('Purge cache'), 125 | )); 126 | 127 | $actions[] = $purgeAction; 128 | } 129 | 130 | return $actions; 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Varnish Purge for Craft 2 | ===== 3 | Craft plugin for purging Varnish when elements are saved. 4 | 5 | Installation 6 | --- 7 | 1. Download and extract the contents of the zip. Copy the /varnishpurge folder to your Craft plugin folder. 8 | 2. Enable the Varnish Purge plugin in Craft (Settings > Plugins). 9 | 3. Override default configuration if necessary (see "Configuration" below). 10 | 4. Let the purge commence. 11 | 12 | Configuration 13 | --- 14 | To configure Varnish Purge, create a new `varnishpurge.php` config file in your config folder, and override settings 15 | as needed. The following settings are the default (found in `config.php` in the plugin folder): 16 | 17 | 'varnishUrl' => craft()->getSiteUrl(), 18 | 'purgeEnabled' => isset($_SERVER['HTTP_X_VARNISH']), 19 | 'purgeRelated' => true, 20 | 'logAll' => 0, 21 | 'purgeUrlMap' => [], 22 | 23 | The `varnishUrl` setting can also be an array if you are running a multi language site: 24 | 25 | 'varnishUrl' => array( 26 | 'no' => 'http://your-varnish-server.com/no/', 27 | 'en' => 'http://your-varnish-server.com/en/', 28 | ), 29 | 30 | ####varnishUrl 31 | The url to your varnish server. Usually this is your site url, but it could be different if you don't purge 32 | through a private connection, or if you use the IP directly to bypass CloudFlare or similar services. If your 33 | site url and the varnish url is different, make sure you handle this in your VCL file. 34 | 35 | ####purgeEnabled 36 | Enables or disables the Varnish Purge plugin. You'd normally want to disable it in your dev environments and 37 | enable it in your prod environment. 38 | 39 | ####purgeRelated 40 | Enables or disables purging of related urls when an element is saved. This should normally be enabled to make sure 41 | that all relevant urls are updated, but could be disabled on high traffic websites to make sure the cache stays as warm 42 | as possible. 43 | 44 | ####logAll 45 | When set to `1` some additional logging is forced even if devMode is disabled. Useful for debugging in production 46 | environments without having to enable devMode. 47 | 48 | ####purgeUrlMap 49 | A lookup map for purging additional urls that needs it when a given url is purged. 50 | 51 | 52 | How it works 53 | --- 54 | When an element is saved, the plugin collects the urls *that it thinks need to be updated*, and creates a new task 55 | that sends purge requests to the Varnish server for each url. 56 | 57 | If `purgeRelated` is disabled, only the url for the element itself, or the owners url if the element is 58 | a Matrix block, is purged. 59 | 60 | If `purgeRelated` is enabled, the urls for all related elements are also purged. If the saved element is 61 | an entry, all directly related entry urls, all related category urls, and all urls for elements related through 62 | an entries Matrix blocks, is purged. If the saved element is an asset, all urls for elements related to that 63 | asset, either directly or through a Matrix block, is purged. And so on. The element types taken into account is 64 | entries, categories, matrix blocks and assets. 65 | 66 | The plugin also adds a new element action to entries and categories for purging individual elements manually. 67 | When doing this, related elements are not purged, only the selected elements. 68 | 69 | Alternatives 70 | --- 71 | A good alternative to this plugin is [Josh Angell's CacheMonster plugin](https://github.com/supercool/Cache-Monster/). It takes 72 | a different approach, using the result of the {% cache %} tag to find the urls that needs to be purged, and also provides a feature to 73 | warm the cache. 74 | 75 | This is a great solution if you're using {% cache %}, but most of the sites I'm using Varnish on is very content 76 | heavy and I've opted not to use it. When you have tens of thousands of element criterias that needs to be updated when content is 77 | updated, it's really a problem. 78 | 79 | Setting HTTP headers in your templates 80 | --- 81 | Varnish uses the HTTP headers sent by Craft to determine if/how to cache a request. You can configure this in 82 | your webserver, but I find it more flexible to do it in my templates. I usually have a config variable 83 | named `addExpiryHeaders` in my config, which is enabled only in production (or you'll get issues with the browser caching 84 | your pages unnecessarily), and the following code in my layout template: 85 | 86 | {% if craft.config.addExpiryHeaders %} 87 | {% if expiryTime is not defined %}{% set expiryTime = '+1 day' %}{% endif %} 88 | 89 | {% set expires = now | date_modify(expiryTime) %} 90 | {% header "Cache-Control: max-age=" ~ (expires.timestamp - now.timestamp) %} 91 | {% header "Pragma: cache" %} 92 | {% header "Expires: " ~ expires.rfc1123() %} 93 | {% header "X-Remove-Cache-Control: 1" %} 94 | {% endif %} 95 | 96 | In your individual page templates, you can then override the expiry time of page types like this: 97 | 98 | {% set expiryTime = '+60 mins' %} 99 | 100 | If you want to cache static assets with Varnish, you need to set the appropriate cache headers in your webserver. 101 | 102 | Configuring Varnish 103 | --- 104 | If you plan to run a Varnish server, you really need to get your hands dirty and learn 105 | [VCL](https://www.varnish-cache.org/docs/trunk/users-guide/vcl.html). There're no shortcuts 106 | unfortunately. :) Have a look at [this gist](https://gist.github.com/aelvan/eba03969f91c1bd51c40) for an example VCL file. 107 | It's based on the [Varnish 4.0 template made by Mattias Geniar](https://github.com/mattiasgeniar/varnish-4.0-configuration-templates) 108 | with some adjustments for Craft. 109 | 110 | **Note: Remember to add your server ip to the purge acl** 111 | 112 | Also, read [this thread on the Craft CMS StackExchange](http://craftcms.stackexchange.com/questions/2716/varnish-4-x-and-craft/) and 113 | [Josh Angell's blogpost over at SuperCool](https://supercool.github.io/2015/06/08/making-craft-sing-with-varnish-and-nginx.html). 114 | 115 | Price, license and support 116 | --- 117 | The plugin is released under the MIT license, meaning you can do what ever you want with it as long as you don't 118 | blame me. **It's free**, which means there is absolutely no support included, but you might get it anyway. 119 | Just post an issue here on github if you have one, and I'll see what I can do. :) 120 | -------------------------------------------------------------------------------- /varnishpurge/services/VarnishpurgeService.php: -------------------------------------------------------------------------------- 1 | purgeElements(array($element), $purgeRelated); 16 | } 17 | 18 | /** 19 | * Purge an array of elements 20 | * 21 | * @param mixed $event 22 | */ 23 | public function purgeElements($elements, $purgeRelated = true) 24 | { 25 | if (count($elements) > 0) { 26 | 27 | // Assume that we only want to purge elements in one locale. 28 | // May not be the case if other thirdparty plugins sends elements. 29 | $locale = $elements[0]->locale; 30 | 31 | $uris = array(); 32 | 33 | foreach ($elements as $element) { 34 | $uris = array_merge($uris, $this->_getElementUris($element, $locale, $purgeRelated)); 35 | } 36 | 37 | $urls = $this->_generateUrls($uris, $locale); 38 | $urls = array_merge($urls, $this->_getMappedUrls($urls)); 39 | 40 | if (count($urls) > 0) { 41 | $this->_makeTask('Varnishpurge_Purge', $urls, $locale); 42 | } 43 | 44 | } 45 | } 46 | 47 | /** 48 | * Get URIs to purge from $element in $locale. 49 | * 50 | * Adds the URI of the $element, and all related elements 51 | * 52 | * @param $element 53 | * @param $locale 54 | * @return array 55 | */ 56 | private function _getElementUris($element, $locale, $getRelated = true) 57 | { 58 | $uris = array(); 59 | 60 | // Get elements own uri 61 | if ($element->uri != '') { 62 | $uris[] = $element->uri; 63 | } 64 | 65 | // If this is a matrix block, get the uri of matrix block owner 66 | if ($element->getElementType() == ElementType::MatrixBlock) { 67 | if ($element->owner->uri != '') { 68 | $uris[] = $element->owner->uri; 69 | } 70 | } 71 | 72 | // Get related elements and their uris 73 | if ($getRelated) { 74 | 75 | // get directly related entries 76 | $relatedEntries = $this->_getRelatedElementsOfType($element, $locale, ElementType::Entry); 77 | foreach ($relatedEntries as $related) { 78 | if ($related->uri != '') { 79 | $uris[] = $related->uri; 80 | } 81 | } 82 | unset($relatedEntries); 83 | 84 | // get directly related categories 85 | $relatedCategories = $this->_getRelatedElementsOfType($element, $locale, ElementType::Category); 86 | foreach ($relatedCategories as $related) { 87 | if ($related->uri != '') { 88 | $uris[] = $related->uri; 89 | } 90 | } 91 | unset($relatedCategories); 92 | 93 | // get directly related matrix block and its owners uri 94 | $relatedMatrixes = $this->_getRelatedElementsOfType($element, $locale, ElementType::MatrixBlock); 95 | foreach ($relatedMatrixes as $relatedMatrixBlock) { 96 | if ($relatedMatrixBlock->owner->uri != '') { 97 | $uris[] = $relatedMatrixBlock->owner->uri; 98 | } 99 | } 100 | unset($relatedMatrixes); 101 | 102 | // get directly related categories 103 | $relatedCategories = $this->_getRelatedElementsOfType($element, $locale, ElementType::Category); 104 | foreach ($relatedCategories as $related) { 105 | if ($related->uri != '') { 106 | $uris[] = $related->uri; 107 | } 108 | } 109 | unset($relatedCategories); 110 | 111 | // get directly Commerce products 112 | $relatedProducts = $this->_getRelatedElementsOfType($element, $locale, 'Commerce_Product'); 113 | foreach ($relatedProducts as $related) { 114 | if ($related->uri != '') { 115 | $uris[] = $related->uri; 116 | } 117 | } 118 | unset($relatedProducts); 119 | } 120 | 121 | 122 | $uris = array_unique($uris); 123 | 124 | foreach (craft()->plugins->call('varnishPurgeTransformElementUris', [$element, $uris]) as $plugin => $pluginUris) { 125 | if ($pluginUris !== null) { 126 | $uris = $pluginUris; 127 | } 128 | } 129 | 130 | return $uris; 131 | 132 | } 133 | 134 | 135 | /** 136 | * Gets elements of type $elementType related to $element in $locale 137 | * 138 | * @param $element 139 | * @param $locale 140 | * @param $elementType 141 | * @return mixed 142 | */ 143 | private function _getRelatedElementsOfType($element, $locale, $elementType) 144 | { 145 | $elementTypeExists = craft()->elements->getElementType($elementType); 146 | if(!$elementTypeExists) { return array(); } 147 | 148 | $criteria = craft()->elements->getCriteria($elementType); 149 | $criteria->relatedTo = $element; 150 | $criteria->locale = $locale; 151 | return $criteria->find(); 152 | } 153 | 154 | /** 155 | * 156 | * 157 | * @param $uris 158 | * @param $locale 159 | * @return array 160 | */ 161 | private function _generateUrls($uris, $locale) 162 | { 163 | $urls = array(); 164 | $varnishUrlSetting = craft()->varnishpurge->getSetting('varnishUrl'); 165 | 166 | if (is_array($varnishUrlSetting)) { 167 | $varnishUrl = $varnishUrlSetting[$locale]; 168 | } else { 169 | $varnishUrl = $varnishUrlSetting; 170 | } 171 | 172 | if (!$varnishUrl) { 173 | VarnishpurgePlugin::log('Varnish URL could not be found', LogLevel::Error); 174 | return $urls; 175 | } 176 | 177 | foreach ($uris as $uri) { 178 | $path = $uri == '__home__' ? '' : $uri; 179 | $url = rtrim($varnishUrl, '/') . '/' . trim($path, '/'); 180 | 181 | if ($path && craft()->config->get('addTrailingSlashesToUrls')) { 182 | $url .= '/'; 183 | } 184 | 185 | array_push($urls, $url); 186 | } 187 | 188 | return $urls; 189 | } 190 | 191 | /** 192 | * 193 | * 194 | * @param $uris 195 | * @return array 196 | */ 197 | private function _getMappedUrls($urls) 198 | { 199 | $mappedUrls = array(); 200 | $map = $this->getSetting('purgeUrlMap'); 201 | 202 | if (is_array($map)) { 203 | foreach ($urls as $url) { 204 | if (isset($map[$url])) { 205 | $mappedVal = $map[$url]; 206 | 207 | if (is_array($mappedVal)) { 208 | $mappedUrls = array_merge($mappedUrls, $mappedVal); 209 | } else { 210 | array_push($mappedUrls, $mappedVal); 211 | } 212 | } 213 | } 214 | } 215 | 216 | return $mappedUrls; 217 | } 218 | 219 | /** 220 | * Create task for purging urls 221 | * 222 | * @param $taskName 223 | * @param $uris 224 | * @param $locale 225 | */ 226 | 227 | private function _makeTask($taskName, $urls, $locale) 228 | { 229 | $urls = array_unique($urls); 230 | 231 | VarnishpurgePlugin::log('Creating task (' . $taskName . ', ' . implode(',', $urls) . ', ' . $locale . ')', LogLevel::Info, craft()->varnishpurge->getSetting('logAll')); 232 | 233 | // If there are any pending tasks, just append the paths to it 234 | $task = craft()->tasks->getNextPendingTask($taskName); 235 | 236 | if ($task && is_array($task->settings)) { 237 | $settings = $task->settings; 238 | 239 | if (!is_array($settings['urls'])) { 240 | $settings['urls'] = array($settings['urls']); 241 | } 242 | 243 | if (is_array($urls)) { 244 | $settings['urls'] = array_merge($settings['urls'], $urls); 245 | } else { 246 | $settings['urls'][] = $urls; 247 | } 248 | 249 | // Make sure there aren't any duplicate paths 250 | $settings['urls'] = array_unique($settings['urls']); 251 | 252 | // Set the new settings and save the task 253 | $task->settings = $settings; 254 | craft()->tasks->saveTask($task, false); 255 | } else { 256 | craft()->tasks->createTask($taskName, null, array( 257 | 'urls' => $urls, 258 | 'locale' => $locale 259 | )); 260 | } 261 | 262 | } 263 | 264 | 265 | /** 266 | * Gets a plugin setting 267 | * 268 | * @param $name String Setting name 269 | * @return mixed Setting value 270 | * @author André Elvan 271 | */ 272 | public function getSetting($name) 273 | { 274 | return craft()->config->get($name, 'varnishpurge'); 275 | } 276 | 277 | } 278 | --------------------------------------------------------------------------------