├── assets └── heart.png ├── src ├── jobs │ └── InvalidateJob.php ├── services │ ├── CacheTags.php │ └── FastPurgeApi.php ├── models │ └── Settings.php └── AkamaiInvalidator.php ├── LICENSE.md └── composer.json /assets/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fork/craft-akamai-invalidator/main/assets/heart.png -------------------------------------------------------------------------------- /src/jobs/InvalidateJob.php: -------------------------------------------------------------------------------- 1 | fastPurgeApi->invalidateTags($this->tags); 17 | } 18 | 19 | protected function defaultDescription(): string 20 | { 21 | return Craft::t('akamai-invalidator', 'Invalidate Akamai cache tags'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/services/CacheTags.php: -------------------------------------------------------------------------------- 1 | cacheTags)) { 23 | array_push($this->cacheTags, $tag); 24 | } 25 | } 26 | 27 | /** 28 | * Get and format cache tags for the Edge-Cache-Tag header 29 | * 30 | * @return string The cache tags, delimited by commas 31 | */ 32 | public function getCacheTagHeader(): string 33 | { 34 | return collect($this->cacheTags)->join(', '); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Fork Unstable Media GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/models/Settings.php: -------------------------------------------------------------------------------- 1 | invalidateOnSave); 23 | } 24 | 25 | public function getEnableInvalidateAll(): bool 26 | { 27 | return App::parseBooleanEnv($this->enableInvalidateAll); 28 | } 29 | 30 | public function getNetwork(): string 31 | { 32 | return App::parseEnv($this->network); 33 | } 34 | 35 | public function getEdgeRcSection(): string 36 | { 37 | return App::parseEnv($this->edgeRcSection); 38 | } 39 | 40 | public function getEdgeRcPath(): string 41 | { 42 | return App::parseEnv($this->edgeRcPath); 43 | } 44 | 45 | public function defineRules(): array 46 | { 47 | return [ 48 | ['invalidateOnSave', 'boolean'], 49 | ['enableInvalidateAll', 'boolean'], 50 | ['network', 'string'], 51 | ['edgeRcSection', 'string'], 52 | ['edgeRcPath', 'string'], 53 | ]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/services/FastPurgeApi.php: -------------------------------------------------------------------------------- 1 | getSettings(); 27 | 28 | $client = \Akamai\Open\EdgeGrid\Client::createFromEdgeRcFile( 29 | $settings->getEdgeRcSection(), 30 | $settings->getEdgeRcPath() 31 | ); 32 | 33 | $url = '/ccu/v3/invalidate/tag/' . $settings->getNetwork(); 34 | 35 | $response = $client->request('POST', $url, [ 36 | 'json' => [ 37 | 'objects' => $tags, 38 | ], 39 | 'headers' => [ 40 | 'accept' => 'application/json', 41 | ], 42 | ]); 43 | 44 | return $response; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fork/craft-akamai-invalidator", 3 | "description": "Assign cache tags to pages and invalidate them on save.", 4 | "type": "craft-plugin", 5 | "version": "1.4.1", 6 | "license": "MIT", 7 | "keywords": [ 8 | "craft", 9 | "cms", 10 | "craftcms", 11 | "craft-plugin", 12 | "akamai", 13 | "cache" 14 | ], 15 | "authors": [ 16 | { 17 | "name": "Fork Unstable Media GmbH", 18 | "homepage": "https://www.fork.de/" 19 | } 20 | ], 21 | "support": { 22 | "email": "obj@fork.de", 23 | "issues": "https://github.com/fork/craft-akamai-invalidator/issues?state=open", 24 | "source": "https://github.com/fork/craft-akamai-invalidator", 25 | "docs": "https://github.com/fork/craft-akamai-invalidator", 26 | "rss": "https://github.com/fork/craft-akamai-invalidator/releases.atom" 27 | }, 28 | "require": { 29 | "php": ">=8.1", 30 | "akamai-open/edgegrid-client": "^2.0", 31 | "craftcms/cms": "^4.4.7.1" 32 | }, 33 | "require-dev": { 34 | "craftcms/ecs": "dev-main", 35 | "craftcms/phpstan": "dev-main" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "fork\\akamaiinvalidator\\": "src/" 40 | } 41 | }, 42 | "extra": { 43 | "name": "Akamai Cache Invalidator", 44 | "handle": "akamai-invalidator", 45 | "class": "fork\\akamaiinvalidator\\AkamaiInvalidator" 46 | }, 47 | "scripts": { 48 | "check-cs": "ecs check --ansi", 49 | "fix-cs": "ecs check --ansi --fix", 50 | "phpstan": "phpstan --memory-limit=1G" 51 | }, 52 | "config": { 53 | "sort-packages": true, 54 | "platform": { 55 | "php": "8.1" 56 | }, 57 | "allow-plugins": { 58 | "yiisoft/yii2-composer": true, 59 | "craftcms/plugin-installer": true 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/AkamaiInvalidator.php: -------------------------------------------------------------------------------- 1 | 26 | * @copyright Fork Unstable Media GmbH 27 | * @license MIT 28 | * @property-read CacheTags $cacheTags 29 | * @property-read FastPurgeApi $fastPurgeApi 30 | */ 31 | class AkamaiInvalidator extends Plugin 32 | { 33 | /** 34 | * Whether cache tags get invalidated on entry save 35 | * 36 | * @var bool 37 | */ 38 | public bool $invalidateOnSave = true; 39 | 40 | public string $schemaVersion = '1.0.0'; 41 | 42 | public static function config(): array 43 | { 44 | return [ 45 | 'components' => [ 46 | 'cacheTags' => ['class' => CacheTags::class], 47 | 'fastPurgeApi' => ['class' => FastPurgeApi::class], 48 | ], 49 | ]; 50 | } 51 | 52 | public function init() 53 | { 54 | parent::init(); 55 | 56 | // Defer most setup tasks until Craft is fully initialized 57 | Craft::$app->onInit(function() { 58 | $this->attachEventHandlers(); 59 | }); 60 | } 61 | 62 | private function attachEventHandlers(): void 63 | { 64 | /** @var \fork\akamaiinvalidator\models\Settings */ 65 | $settings = AkamaiInvalidator::getInstance()->getSettings(); 66 | 67 | if ($settings->getEnableInvalidateAll()) { 68 | /** 69 | * Adds Craft cache option that invalidates all pages 70 | */ 71 | Event::on( 72 | ClearCaches::class, 73 | ClearCaches::EVENT_REGISTER_CACHE_OPTIONS, 74 | function(RegisterCacheOptionsEvent $event) { 75 | $event->options[] = [ 76 | 'key' => 'akamai-invalidator', 77 | 'label' => Craft::t('akamai-invalidator', 'Akamai Cache'), 78 | 'action' => function() { 79 | Queue::push(new InvalidateJob(['tags' => ['all']])); 80 | }, 81 | ]; 82 | } 83 | ); 84 | } 85 | 86 | /** 87 | * Invalidates individual entries after they are saved 88 | */ 89 | Event::on( 90 | Entry::class, 91 | Entry::EVENT_AFTER_SAVE, 92 | function(ModelEvent $event) { 93 | /** @var \fork\akamaiinvalidator\models\Settings */ 94 | $settings = AkamaiInvalidator::getInstance()->getSettings(); 95 | 96 | if (!$settings->getInvalidateOnSave()) { 97 | // Don't do anything when invalidation is disabled 98 | return; 99 | } 100 | 101 | /* @var Entry $entry */ 102 | $entry = $event->sender; 103 | 104 | if (ElementHelper::isDraftOrRevision($entry)) { 105 | // don't do anything with drafts or revisions 106 | return; 107 | } 108 | 109 | if ($entry->url == null) { 110 | // Ignore entries without URL 111 | return; 112 | } 113 | 114 | Queue::push(new InvalidateJob(['tags' => ['entry-' . $entry->id]])); 115 | } 116 | ); 117 | 118 | /** 119 | * Tasks that are run after the page finished rendering 120 | */ 121 | Event::on( 122 | View::class, 123 | View::EVENT_AFTER_RENDER_PAGE_TEMPLATE, 124 | function() { 125 | /** @var \craft\web\Application */ 126 | $app = Craft::$app; 127 | 128 | // Attach `all` cache tag that is used to invalidate all pages at once 129 | AkamaiInvalidator::getInstance()->cacheTags->addCacheTag('all'); 130 | 131 | $entry = $app->getUrlManager()->getMatchedElement(); 132 | if ($entry) { 133 | // Attach an entry-specific cache tag that is used to invalidate pages including specific entries 134 | AkamaiInvalidator::getInstance()->cacheTags->addCacheTag('entry-' . $entry->id); 135 | } 136 | 137 | // Collect and add all cache tags to the response 138 | $cacheTagsHeader = AkamaiInvalidator::getInstance()->cacheTags->getCacheTagHeader(); 139 | $app->response->headers->add('Edge-Cache-Tag', $cacheTagsHeader); 140 | } 141 | ); 142 | } 143 | 144 | protected function createSettingsModel(): ?Model 145 | { 146 | return new Settings(); 147 | } 148 | } 149 | --------------------------------------------------------------------------------