From 7014fdfed6a89fd7277506dfdab5503f3f1ceb82 Mon Sep 17 00:00:00 2001 From: Oliver Stark Date: Tue, 12 Jul 2022 12:53:58 +0200 Subject: [PATCH 1/3] Test setup and composer version bump --- .gitignore | 41 ++++++------------------- CHANGELOG.md | 4 +++ composer.json | 24 ++++++++++++--- phpstan.neon | 11 +++++++ phpunit.xml.dist | 38 +++++++++++++++++++++++ psalm.xml | 28 ----------------- tests/Feature/ExampleTest.php | 17 +++++++++++ tests/Pest.php | 39 ++++++++++++++++++++++++ tests/bootstrap.php | 57 ++++++++++++++++++++++++++++++----- 9 files changed, 188 insertions(+), 71 deletions(-) create mode 100644 phpstan.neon create mode 100644 phpunit.xml.dist delete mode 100644 psalm.xml create mode 100644 tests/Feature/ExampleTest.php create mode 100644 tests/Pest.php diff --git a/.gitignore b/.gitignore index a24a7ee..99454f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,33 +1,12 @@ -# CRAFT ENVIRONMENT -.env.php -.env.sh -.env - -# COMPOSER -/vendor +.idea +.php_cs +.php_cs.cache +.php-cs-fixer.cache +.phpunit.result.cache +build composer.lock +coverage +vendor +*.log +/tests/_craft/storage/ -# BUILD FILES -/bower_components/* -/node_modules/* -/build/* -/yarn-error.log - -# MISC FILES -.cache -.DS_Store -.idea -.project -.settings -*.esproj -*.sublime-workspace -*.sublime-project -*.tmproj -*.tmproject -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -config.codekit3 -prepros-6.config diff --git a/CHANGELOG.md b/CHANGELOG.md index 407c653..efb08a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [2.0.0] - Unreleased + + + ## [1.9.2] - 2022-03-26 - Fix issues with Supertable / exclude SuperTableBlockElement diff --git a/composer.json b/composer.json index 913f2a1..d660a47 100644 --- a/composer.json +++ b/composer.json @@ -24,11 +24,14 @@ } ], "require": { - "craftcms/cms": "^3.2.0", + "craftcms/cms": "^4.1.0", "guzzlehttp/guzzle": "^6.5.5|^7.2.0" }, "require-dev": { - "vimeo/psalm": "^4.4" + "craftcms/phpstan": "*", + "friendsofphp/php-cs-fixer": "^3.0", + "pestphp/pest": "^1.2", + "pestphp/pest-plugin-parallel": "^1.0" }, "autoload": { "psr-4": { @@ -36,8 +39,8 @@ } }, "scripts": { - "ps": "phpstan analyse src --level=5 -c phpstan.neon", - "stan": "@ps" + "phpstan": "vendor/bin/phpstan analyse", + "test": "vendor/bin/pest" }, "extra": { "name": "Upper", @@ -45,5 +48,16 @@ "hasCpSettings": false, "hasCpSection": false, "changelogUrl": "https://raw.githubusercontent.com/ostark/upper/master/CHANGELOG.md" - } + }, + "config": { + "allow-plugins": { + "yiisoft/yii2-composer": true, + "composer/package-versions-deprecated": true, + "craftcms/plugin-installer": true, + "pestphp/pest-plugin": true + } + }, + + "prefer-stable": true, + "minimum-stability": "dev" } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..be0a203 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,11 @@ +includes: + - vendor/craftcms/phpstan/phpstan.neon +parameters: + level: 6 + paths: + - src + - tests + tmpDir: build/phpstan + checkMissingIterableValueType: false + ignoreErrors: + - "#Unable to resolve the template type T in call to method static method#" diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..fea0d2f --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,38 @@ + + + + + tests + + + + + ./src + + + + + + + + + + + diff --git a/psalm.xml b/psalm.xml deleted file mode 100644 index 5509c48..0000000 --- a/psalm.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php new file mode 100644 index 0000000..f7deeab --- /dev/null +++ b/tests/Feature/ExampleTest.php @@ -0,0 +1,17 @@ +set('foo', new \my\FooDummy()); +}); + +it('is always true', function () { + + $someResult = true; + + // Assert + expect($someResult)->toBeTrue(); +}); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..6d4256c --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,39 @@ +in('Feature'); + +/* +|-------------------------------------------------------------------------- +| Expectations +|-------------------------------------------------------------------------- +| +| When you're writing tests, you often need to check that values meet certain conditions. The +| "expect()" function gives you access to a set of "expectations" methods that you can use +| to assert different things. Of course, you may extend the Expectation API at any time. +| +*/ + + +/* +|-------------------------------------------------------------------------- +| Functions +|-------------------------------------------------------------------------- +| +| While Pest is very powerful out-of-the-box, you may have some testing code specific to your +| project that you don't want to repeat in every file. Here you can also expose helpers as +| global functions to help you to reduce the number of lines of code in your test files. +| +*/ diff --git a/tests/bootstrap.php b/tests/bootstrap.php index bfe6e85..9ed0f43 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,13 +1,56 @@ env = $environment; +$configService->configDir = CRAFT_CONFIG_PATH; +$configService->appDefaultsDir = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'defaults'; +$generalConfig = $configService->getConfigFromFile('general'); + +$config = \craft\helpers\ArrayHelper::merge( + [ + 'vendorPath' => CRAFT_VENDOR_PATH, + 'env' => $environment, + 'components' => [ + 'config' => $configService, + ], + 'id' => 'test', + 'basePath' => __DIR__, + 'class' => craft\console\Application::class, + ], + require 'vendor/craftcms/cms/src/config/app.php', + require 'vendor/craftcms/cms/src/config/app.console.php', + $configService->getConfigFromFile('app'), +); + +// Initialize the application +/** @var \craft\web\Application|craft\console\Application $app */ +$app = Craft::createObject($config); + + +// Load and run Craft +/** @var craft\console\Application $app */ +\Craft::$app = $app; From 7b67f493847382cd5f433299a0a7257c60c2c972 Mon Sep 17 00:00:00 2001 From: Oliver Stark Date: Tue, 12 Jul 2022 13:14:11 +0200 Subject: [PATCH 2/3] Namespace changes --- .gitignore | 3 +-- composer.json | 2 +- phpunit.xml.dist | 8 +++--- src/CacheResponse.php | 4 +-- src/{drivers => Drivers}/AbstractPurger.php | 6 ++--- .../CachePurgeInterface.php | 4 +-- src/{drivers => Drivers}/Cloudflare.php | 10 +++---- src/{drivers => Drivers}/Dummy.php | 4 +-- src/{drivers => Drivers}/Fastly.php | 8 +++--- src/{drivers => Drivers}/Keycdn.php | 8 +++--- src/{drivers => Drivers}/Varnish.php | 4 +-- src/EventRegistrar.php | 10 +++---- src/{events => Events}/CacheResponseEvent.php | 4 +-- src/{events => Events}/PurgeEvent.php | 4 +-- .../CloudflareApiException.php | 2 +- .../FastlyApiException.php | 2 +- .../KeycdnApiException.php | 2 +- src/{jobs => Jobs}/PurgeCacheJob.php | 6 ++--- src/{models => Models}/Settings.php | 2 +- src/Plugin.php | 26 +++++++++---------- src/PurgerFactory.php | 6 ++--- src/TagCollection.php | 4 +-- src/TwigExtension.php | 2 +- src/behaviors/CacheControlBehavior.php | 4 +-- src/behaviors/TagHeaderBehavior.php | 4 +-- src/migrations/Install.php | 4 +-- src/migrations/m180618_120307_url_index.php | 4 +-- 27 files changed, 72 insertions(+), 75 deletions(-) rename src/{drivers => Drivers}/AbstractPurger.php (96%) rename src/{drivers => Drivers}/CachePurgeInterface.php (84%) rename src/{drivers => Drivers}/Cloudflare.php (93%) rename src/{drivers => Drivers}/Dummy.php (94%) rename src/{drivers => Drivers}/Fastly.php (94%) rename src/{drivers => Drivers}/Keycdn.php (91%) rename src/{drivers => Drivers}/Varnish.php (97%) rename src/{events => Events}/CacheResponseEvent.php (86%) rename src/{events => Events}/PurgeEvent.php (77%) rename src/{exceptions => Exceptions}/CloudflareApiException.php (97%) rename src/{exceptions => Exceptions}/FastlyApiException.php (96%) rename src/{exceptions => Exceptions}/KeycdnApiException.php (96%) rename src/{jobs => Jobs}/PurgeCacheJob.php (87%) rename src/{models => Models}/Settings.php (98%) diff --git a/.gitignore b/.gitignore index 99454f7..76d83e2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,8 @@ .php_cs.cache .php-cs-fixer.cache .phpunit.result.cache -build +/build composer.lock -coverage vendor *.log /tests/_craft/storage/ diff --git a/composer.json b/composer.json index d660a47..c5691b5 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,7 @@ }, "autoload": { "psr-4": { - "ostark\\upper\\": "src/" + "ostark\\Upper\\": "src/" } }, "scripts": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index fea0d2f..9203aad 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -27,12 +27,12 @@ ./src - - - + + + - + diff --git a/src/CacheResponse.php b/src/CacheResponse.php index 71d3309..f6df701 100644 --- a/src/CacheResponse.php +++ b/src/CacheResponse.php @@ -1,11 +1,11 @@ -get('tagCollection'); $collection->setKeyPrefix($this->getSettings()->getKeyPrefix()); @@ -109,12 +109,10 @@ public function getTagCollection(): TagCollection /** * Creates and returns the model used to store the plugin’s settings. - * - * @return \craft\base\Model|null */ - protected function createSettingsModel() + protected function createSettingsModel(): PluginSettings { - return new Settings(); + return new PluginSettings(); } diff --git a/src/PurgerFactory.php b/src/PurgerFactory.php index 6f9dfb2..0456838 100644 --- a/src/PurgerFactory.php +++ b/src/PurgerFactory.php @@ -1,16 +1,16 @@ - Date: Tue, 12 Jul 2022 17:46:40 +0200 Subject: [PATCH 3/3] Refactor huge EventRegistrar to multiple invokeable handler classes --- composer.json | 5 +- src/EventRegistrar.php | 279 ------------------ src/Handlers/AddCacheResponse.php | 65 ++++ src/Handlers/CollectTagsFromElementQuery.php | 34 +++ src/Handlers/CollectTagsFromTemplateCache.php | 17 ++ src/Handlers/InvalidateCache.php | 89 ++++++ src/Handlers/RegisterCacheCheckbox.php | 32 ++ src/Handlers/StoreTagUrlRelation.php | 55 ++++ .../{Settings.php => PluginSettings.php} | 2 +- src/Plugin.php | 137 ++++++--- src/helpers.php | 13 + 11 files changed, 411 insertions(+), 317 deletions(-) delete mode 100644 src/EventRegistrar.php create mode 100644 src/Handlers/AddCacheResponse.php create mode 100644 src/Handlers/CollectTagsFromElementQuery.php create mode 100644 src/Handlers/CollectTagsFromTemplateCache.php create mode 100644 src/Handlers/InvalidateCache.php create mode 100644 src/Handlers/RegisterCacheCheckbox.php create mode 100644 src/Handlers/StoreTagUrlRelation.php rename src/Models/{Settings.php => PluginSettings.php} (98%) create mode 100644 src/helpers.php diff --git a/composer.json b/composer.json index c5691b5..f2314ad 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,10 @@ "autoload": { "psr-4": { "ostark\\Upper\\": "src/" - } + }, + "files": [ + "src/helpers.php" + ] }, "scripts": { "phpstan": "vendor/bin/phpstan analyse", diff --git a/src/EventRegistrar.php b/src/EventRegistrar.php deleted file mode 100644 index 416236d..0000000 --- a/src/EventRegistrar.php +++ /dev/null @@ -1,279 +0,0 @@ -getRequest(); - - // Don't cache CP, LivePreview, Action, Non-GET requests - if ($request->getIsCpRequest() || - $request->getIsLivePreview() || - $request->getIsActionRequest() || - !$request->getIsGet() - ) { - $response = \Craft::$app->getResponse(); - $response->addCacheControlDirective('private'); - $response->addCacheControlDirective('no-cache'); - - return false; - } - - // Collect tags - Event::on(ElementQuery::class, ElementQuery::EVENT_AFTER_POPULATE_ELEMENT, function (PopulateElementEvent $event) { - - // Don't collect MatrixBlock and User elements for now - if (!Plugin::getInstance()->getSettings()->isCachableElement(get_class($event->element))) { - return; - } - - // Tag with GlobalSet handle - if ($event->element instanceof \craft\elements\GlobalSet) { - Plugin::getInstance()->getTagCollection()->add($event->element->handle); - } - - // Add to collection - Plugin::getInstance()->getTagCollection()->addTagsFromElement($event->row); - - }); - - // Add the tags to the response header - Event::on(View::class, View::EVENT_AFTER_RENDER_PAGE_TEMPLATE, function (TemplateEvent $event) { - - /** @var \yii\web\Response $response */ - $response = \Craft::$app->getResponse(); - $plugin = Plugin::getInstance(); - $tagCollection = $plugin->getTagCollection(); - $tags = $plugin->getTagCollection()->getAll(); - $settings = $plugin->getSettings(); - $headers = $response->getHeaders(); - - // Make existing cache-control headers accessible - $response->setCacheControlDirectiveFromString($headers->get('cache-control')); - - // Don't cache if private | no-cache set already - if ($response->hasCacheControlDirective('private') || $response->hasCacheControlDirective('no-cache')) { - $headers->set(Plugin::INFO_HEADER_NAME, 'BYPASS'); - - return; - } - - // MaxAge or defaultMaxAge? - $maxAge = $response->getMaxAge() ?? $settings->defaultMaxAge; - - // Set Headers - $maxBytes = $settings->maxBytesForCacheTagHeader; - $maxedTags = $tagCollection->getUntilMaxBytes($maxBytes); - $response->setTagHeader($settings->getTagHeaderName(), $maxedTags, $settings->getHeaderTagDelimiter()); - - // Flag truncation - if (count($tags) > count($maxedTags)) { - $headers->set(Plugin::TRUNCATED_HEADER_NAME, count($tags) - count($maxedTags)); - } - - $response->setSharedMaxAge($maxAge); - $headers->set(Plugin::INFO_HEADER_NAME, "CACHED: " . date(\DateTime::ISO8601)); - - $plugin->trigger($plugin::EVENT_AFTER_SET_TAG_HEADER, new CacheResponseEvent([ - 'tags' => $tags, - 'maxAge' => $maxAge, - 'requestUrl' => \Craft::$app->getRequest()->getUrl(), - 'headers' => $response->getHeaders()->toArray() - ] - )); - }); - - } - - - public static function registerCpEvents() - { - // Register cache purge checkbox - Event::on( - ClearCaches::class, - ClearCaches::EVENT_REGISTER_CACHE_OPTIONS, - function (RegisterCacheOptionsEvent $event) { - $driver = ucfirst(Plugin::getInstance()->getSettings()->driver); - $event->options[] = [ - 'key' => 'upper-purge-all', - 'label' => \Craft::t('upper', 'Upper ({driver})', ['driver' => $driver]), - 'action' => function () { - Plugin::getInstance()->getPurger()->purgeAll(); - }, - ]; - } - ); - } - - - public static function registerFallback() - { - - Event::on(Plugin::class, Plugin::EVENT_AFTER_SET_TAG_HEADER, function (CacheResponseEvent $event) { - - // not tagged? - if (0 == count($event->tags)) { - return; - } - - // fulltext or array - $tags = \Craft::$app->getDb()->getIsMysql() - ? implode(" ", $event->tags) - : str_replace(['[', ']'], ['{', '}'], json_encode($event->tags) ?: '[]'); - - // in order to have a unique (collitions are possible) identifier by url with a fixed length - $urlHash = md5($event->requestUrl); - - try { - // Insert item - \Craft::$app->getDb()->createCommand() - ->upsert( - // Table - Plugin::CACHE_TABLE, - - // Identifier - ['urlHash' => $urlHash], - - // Data - [ - 'urlHash' => $urlHash, - 'url' => $event->requestUrl, - 'tags' => $tags, - 'headers' => json_encode($event->headers), - 'siteId' => \Craft::$app->getSites()->currentSite->id - ] - ) - ->execute(); - } catch (\Exception $e) { - \Craft::warning("Failed to register fallback.", "upper"); - } - - }); - - } - - - /** - * @param \yii\base\Event $event - */ - protected static function handleUpdateEvent(Event $event) - { - $tags = []; - - - if ($event instanceof ElementEvent) { - - if (!Plugin::getInstance()->getSettings()->isCachableElement(get_class($event->element))) { - return; - } - - // Prevent purge on updates of drafts or revisions - if (ElementHelper::isDraftOrRevision($event->element)) { - return; - } - - // Prevent purge on resaving - if (property_exists($event->element, 'resaving') && $event->element->resaving === true) { - return; - } - - if ($event->element instanceof \craft\elements\GlobalSet && is_string($event->element->handle)) { - $tags[] = $event->element->handle; - } elseif ($event->element instanceof \craft\elements\Asset && $event->isNew) { - $tags[] = (string)$event->element->volumeId; - } else { - if (isset($event->element->sectionId)) { - $tags[] = Plugin::TAG_PREFIX_SECTION . $event->element->sectionId; - } - if (!$event->isNew) { - $tags[] = Plugin::TAG_PREFIX_ELEMENT . $event->element->getId(); - } - } - } - - if ($event instanceof SectionEvent) { - $tags[] = Plugin::TAG_PREFIX_SECTION . $event->section->id; - } - - if ($event instanceof MoveElementEvent or $event instanceof ElementStructureEvent) { - $tags[] = Plugin::TAG_PREFIX_STRUCTURE . $event->structureId; - } - - if (count($tags) === 0) { - return; - } - - foreach ($tags as $tag) { - $tag = Plugin::getInstance()->getTagCollection()->prepareTag($tag); - - $purgeEvent = new PurgeEvent([ - 'tag' => $tag, - ]); - - Plugin::getInstance()->trigger(Plugin::EVENT_BEFORE_PURGE, $purgeEvent); - - // Push to queue - \Craft::$app->getQueue()->push(new PurgeCacheJob([ - 'tag' => $purgeEvent->tag - ] - )); - - Plugin::getInstance()->trigger(Plugin::EVENT_AFTER_PURGE, $purgeEvent); - } - - } - -} diff --git a/src/Handlers/AddCacheResponse.php b/src/Handlers/AddCacheResponse.php new file mode 100644 index 0000000..b0ba10c --- /dev/null +++ b/src/Handlers/AddCacheResponse.php @@ -0,0 +1,65 @@ +settings = $settings; + $this->tags = $tags; + } + + public function __invoke(TemplateEvent $event): void + { + /** @var \yii\web\Response $response */ + $response = \Craft::$app->getResponse(); + $plugin = Plugin::getInstance(); + $tagCollection = $this->tags; + $tags = $this->tags->getAll(); + $headers = $response->getHeaders(); + + // Make existing cache-control headers accessible + $response->setCacheControlDirectiveFromString($headers->get('cache-control')); + + // Don't cache if private | no-cache set already + if ($response->hasCacheControlDirective('private') || $response->hasCacheControlDirective('no-cache')) { + $headers->set(Plugin::INFO_HEADER_NAME, 'BYPASS'); + + return; + } + + // MaxAge or defaultMaxAge? + $maxAge = $response->getMaxAge() ?? $this->settings->defaultMaxAge; + + // Set Headers + $maxBytes = $this->settings->maxBytesForCacheTagHeader; + $maxedTags = $tagCollection->getUntilMaxBytes($maxBytes); + $response->setTagHeader($this->settings->getTagHeaderName(), $maxedTags, $this->settings->getHeaderTagDelimiter()); + + // Flag truncation + if (count($tags) > count($maxedTags)) { + $headers->set(Plugin::TRUNCATED_HEADER_NAME, count($tags) - count($maxedTags)); + } + + $response->setSharedMaxAge($maxAge); + $headers->set(Plugin::INFO_HEADER_NAME, "CACHED: " . date(\DateTime::ISO8601)); + + $plugin->trigger($plugin::EVENT_AFTER_SET_TAG_HEADER, new CacheResponseEvent([ + 'tags' => $tags, + 'maxAge' => $maxAge, + 'requestUrl' => \Craft::$app->getRequest()->getUrl(), + 'headers' => $response->getHeaders()->toArray() + ] + )); + + } +} diff --git a/src/Handlers/CollectTagsFromElementQuery.php b/src/Handlers/CollectTagsFromElementQuery.php new file mode 100644 index 0000000..4d5574d --- /dev/null +++ b/src/Handlers/CollectTagsFromElementQuery.php @@ -0,0 +1,34 @@ +settings = $settings; + $this->tags = $tags; + } + + public function __invoke(PopulateElementEvent $event): void + { + // Don't collect MatrixBlock and User elements for now + if (!$this->settings->isCachableElement(get_class($event->element))) { + return; + } + + // Tag with GlobalSet handle + if ($event->element instanceof \craft\elements\GlobalSet) { + $this->tags->add($event->element->handle); + } + + // Add to collection + $this->tags->addTagsFromElement($event->row); + } +} diff --git a/src/Handlers/CollectTagsFromTemplateCache.php b/src/Handlers/CollectTagsFromTemplateCache.php new file mode 100644 index 0000000..d0232dc --- /dev/null +++ b/src/Handlers/CollectTagsFromTemplateCache.php @@ -0,0 +1,17 @@ +settings = $settings; + } + + public function __invoke(): void + { + } +} diff --git a/src/Handlers/InvalidateCache.php b/src/Handlers/InvalidateCache.php new file mode 100644 index 0000000..44ffc34 --- /dev/null +++ b/src/Handlers/InvalidateCache.php @@ -0,0 +1,89 @@ +settings = $settings; + } + + public function __invoke(Event $event): void + { + + $tags = []; + + if ($event instanceof ElementEvent) { + + if (!$this->settings->isCachableElement(get_class($event->element))) { + return; + } + + // Prevent purge on updates of drafts or revisions + if (ElementHelper::isDraftOrRevision($event->element)) { + return; + } + + // Prevent purge on resaving + if (property_exists($event->element, 'resaving') && $event->element->resaving === true) { + return; + } + + if ($event->element instanceof \craft\elements\GlobalSet && is_string($event->element->handle)) { + $tags[] = $event->element->handle; + } elseif ($event->element instanceof \craft\elements\Asset && $event->isNew) { + $tags[] = (string)$event->element->volumeId; + } else { + if (isset($event->element->sectionId)) { + $tags[] = Plugin::TAG_PREFIX_SECTION . $event->element->sectionId; + } + if (!$event->isNew) { + $tags[] = Plugin::TAG_PREFIX_ELEMENT . $event->element->getId(); + } + } + } + + if ($event instanceof SectionEvent) { + $tags[] = Plugin::TAG_PREFIX_SECTION . $event->section->id; + } + + if ($event instanceof MoveElementEvent or $event instanceof ElementStructureEvent) { + $tags[] = Plugin::TAG_PREFIX_STRUCTURE . $event->structureId; + } + + if (count($tags) === 0) { + return; + } + + foreach ($tags as $tag) { + $tag = Plugin::getInstance()->getTagCollection()->prepareTag($tag); + + $purgeEvent = new PurgeEvent([ + 'tag' => $tag, + ]); + + Plugin::getInstance()->trigger(Plugin::EVENT_BEFORE_PURGE, $purgeEvent); + + // Push to queue + \Craft::$app->getQueue()->push(new PurgeCacheJob([ + 'tag' => $purgeEvent->tag + ] + )); + + Plugin::getInstance()->trigger(Plugin::EVENT_AFTER_PURGE, $purgeEvent); + } + + } +} diff --git a/src/Handlers/RegisterCacheCheckbox.php b/src/Handlers/RegisterCacheCheckbox.php new file mode 100644 index 0000000..a236f16 --- /dev/null +++ b/src/Handlers/RegisterCacheCheckbox.php @@ -0,0 +1,32 @@ +settings = $settings; + $this->purger = $purger; + } + + public function __invoke(RegisterCacheOptionsEvent $event, CachePurgeInterface $purger): void + { + $driver = ucfirst($this->settings->driver); + $event->options[] = [ + 'key' => 'upper-purge-all', + 'label' => \Craft::t('upper', 'Upper ({driver})', ['driver' => $driver]), + 'action' => function () { + $this->purger->purgeAll(); + }, + ]; + + + } +} diff --git a/src/Handlers/StoreTagUrlRelation.php b/src/Handlers/StoreTagUrlRelation.php new file mode 100644 index 0000000..0714007 --- /dev/null +++ b/src/Handlers/StoreTagUrlRelation.php @@ -0,0 +1,55 @@ +tags)) { + return; + } + + // fulltext or array + $tags = \Craft::$app->getDb()->getIsMysql() + ? implode(" ", $event->tags) + : str_replace(['[', ']'], ['{', '}'], json_encode($event->tags) ?: '[]'); + + // in order to have a unique (collitions are possible) identifier by url with a fixed length + $urlHash = md5($event->requestUrl); + + try { + // Insert item + \Craft::$app->getDb()->createCommand() + ->upsert( + // Table + Plugin::CACHE_TABLE, + + // Identifier + ['urlHash' => $urlHash], + + // Data + [ + 'urlHash' => $urlHash, + 'url' => $event->requestUrl, + 'tags' => $tags, + 'headers' => json_encode($event->headers), + 'siteId' => \Craft::$app->getSites()->currentSite->id + ] + ) + ->execute(); + } catch (\Exception $e) { + \Craft::warning("Failed to register fallback.", "upper"); + } + + } +} diff --git a/src/Models/Settings.php b/src/Models/PluginSettings.php similarity index 98% rename from src/Models/Settings.php rename to src/Models/PluginSettings.php index 854c565..9c4b356 100644 --- a/src/Models/Settings.php +++ b/src/Models/PluginSettings.php @@ -18,7 +18,7 @@ * @package Upper * @since 1.0.0 */ -class Settings extends Model +class PluginSettings extends Model { // Public Properties // ========================================================================= diff --git a/src/Plugin.php b/src/Plugin.php index 5675796..b455b2a 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -2,18 +2,32 @@ use Craft; +use craft\base\Element; use craft\base\Plugin as BasePlugin; +use craft\elements\db\ElementQuery; +use craft\services\Elements; +use craft\services\Sections; +use craft\services\Structures; +use craft\utilities\ClearCaches; +use craft\web\View; use ostark\Upper\behaviors\CacheControlBehavior; use ostark\Upper\behaviors\TagHeaderBehavior; use ostark\Upper\Drivers\CachePurgeInterface; -use ostark\Upper\Models\Settings as PluginSettings; +use ostark\Upper\Handlers\AddCacheResponse; +use ostark\Upper\Handlers\CollectTagsFromElementQuery; +use ostark\Upper\Handlers\CollectTagsFromTemplateCache; +use ostark\Upper\Handlers\InvalidateCache; +use ostark\Upper\Handlers\RegisterCacheCheckbox; +use ostark\Upper\Handlers\StoreTagUrlRelation; +use ostark\Upper\Models\PluginSettings; +use yii\base\Event; /** * Class Plugin * * @package ostark\Upper * - * @method Models\Settings getSettings() + * @method Models\PluginSettings getSettings() */ class Plugin extends BasePlugin { @@ -41,7 +55,7 @@ class Plugin extends BasePlugin const INFO_HEADER_NAME = 'X-UPPER-CACHE'; const TRUNCATED_HEADER_NAME = 'X-UPPER-CACHE-TRUNCATED'; - public $schemaVersion = '1.0.1'; + public string $schemaVersion = '1.0.1'; /** @@ -56,56 +70,108 @@ public function init() return false; } - // Register plugin components - $this->setComponents([ - 'purger' => PurgerFactory::create($this->getSettings()->toArray()), - 'tagCollection' => TagCollection::class - ]); + // Register TagCollection in container + Craft::$container->setSingleton(TagCollection::class, function () { + $collection = new TagCollection(); + $collection->setKeyPrefix($this->getSettings()->getKeyPrefix()); + return $collection; + }); + + // Register Purger in container + Craft::$container->set(CachePurgeInterface::class , function () { + return PurgerFactory::create($this->getSettings()->toArray()); + }); // Attach Behaviors - \Craft::$app->getResponse()->attachBehavior('cache-control', CacheControlBehavior::class); - \Craft::$app->getResponse()->attachBehavior('tag-header', TagHeaderBehavior::class); + // TODO -> different implementation + // \Craft::$app->getResponse()->attachBehavior('cache-control', CacheControlBehavior::class); + // \Craft::$app->getResponse()->attachBehavior('tag-header', TagHeaderBehavior::class); // Register event handlers - EventRegistrar::registerFrontendEvents(); - EventRegistrar::registerCpEvents(); - EventRegistrar::registerUpdateEvents(); - - if ($this->getSettings()->useLocalTags) { - EventRegistrar::registerFallback(); - } + $this->registerFrontendEventHandlers(); + $this->registerCpEventHandlers(); + $this->registerUpdateEventHandlers(); // Register Twig extension \Craft::$app->getView()->registerTwigExtension(new TwigExtension); } - // ServiceLocators - // ========================================================================= - /** - * @return \ostark\Upper\Drivers\CachePurgeInterface - */ - public function getPurger(): CachePurgeInterface + private function registerFrontendEventHandlers(): void { - return $this->get('purger'); + if ($this->isNotCacheable()) { + // TODO + // $response = \Craft::$app->getResponse(); + // $response->addCacheControlDirective('private'); + //$response->addCacheControlDirective('no-cache'); + return; + } + + Event::on( + ElementQuery::class, + ElementQuery::EVENT_AFTER_POPULATE_ELEMENT, + new CollectTagsFromElementQuery($this->getSettings(), tags()) + ); + + Event::on( + ElementQuery::class, + ElementQuery::EVENT_DEFINE_CACHE_TAGS, + new CollectTagsFromTemplateCache($this->getSettings()) + ); + + Event::on( + View::class, + View::EVENT_AFTER_RENDER_PAGE_TEMPLATE, + new AddCacheResponse($this->getSettings(), tags()) + ); + + Event::on( + Plugin::class, + Plugin::EVENT_AFTER_SET_TAG_HEADER, + new StoreTagUrlRelation() + ); + } + private function registerCpEventHandlers(): void + { + Event::on( + ClearCaches::class, + ClearCaches::EVENT_REGISTER_CACHE_OPTIONS, + new RegisterCacheCheckbox($this->getSettings(), purger()) + ); + } - /** - * @return \ostark\Upper\TagCollection - */ - public function getTagCollection(): TagCollection + private function registerUpdateEventHandlers(): void { - /* @var \ostark\Upper\TagCollection $collection */ - $collection = $this->get('tagCollection'); - $collection->setKeyPrefix($this->getSettings()->getKeyPrefix()); + $handler = new InvalidateCache($this->getSettings()); - return $collection; + Event::on(Elements::class, Elements::EVENT_AFTER_SAVE_ELEMENT, $handler); + Event::on(Element::class, Element::EVENT_AFTER_MOVE_IN_STRUCTURE, $handler); + Event::on(Elements::class, Elements::EVENT_AFTER_DELETE_ELEMENT, $handler); + Event::on(Structures::class, Structures::EVENT_AFTER_MOVE_ELEMENT, $handler); + Event::on(Sections::class, Sections::EVENT_AFTER_SAVE_SECTION, $handler); } - // Protected Methods - // ========================================================================= + private function isNotCacheable(): bool + { + if (\Craft::$app instanceof \craft\console\Application) { + return true; + } + + $request = \Craft::$app->getRequest(); + + if ($request->getIsCpRequest() || + $request->getIsLivePreview() || + $request->getIsActionRequest() || + !$request->getIsGet() + ) { + return true; + } + return false; + } + /** * Creates and returns the model used to store the plugin’s settings. @@ -120,7 +186,7 @@ protected function createSettingsModel(): PluginSettings * Is called after the plugin is installed. * Copies example config to project's config folder */ - protected function afterInstall() + protected function afterInstall():void { $configSourceFile = __DIR__ . DIRECTORY_SEPARATOR . 'config.example.php'; $configTargetFile = \Craft::$app->getConfig()->configDir . DIRECTORY_SEPARATOR . $this->handle . '.php'; @@ -129,5 +195,4 @@ protected function afterInstall() copy($configSourceFile, $configTargetFile); } } - } diff --git a/src/helpers.php b/src/helpers.php new file mode 100644 index 0000000..ad1da5a --- /dev/null +++ b/src/helpers.php @@ -0,0 +1,13 @@ +