diff --git a/Block/Adminhtml/Form/Renderer/Config/ScheduleEveryFieldComment.php b/Block/Adminhtml/Form/Renderer/Config/ScheduleEveryFieldComment.php new file mode 100644 index 0000000..5dc0ef1 --- /dev/null +++ b/Block/Adminhtml/Form/Renderer/Config/ScheduleEveryFieldComment.php @@ -0,0 +1,38 @@ +config = $config; + + parent::__construct($context); + } + + /** + * @param $elementValue + * @return string + */ + public function getCommentText($elementValue) + { + return 'Cron Expression: none'; + } +} diff --git a/Block/Adminhtml/Form/Renderer/Config/ScheduleEveryFieldConfig.php b/Block/Adminhtml/Form/Renderer/Config/ScheduleEveryFieldConfig.php new file mode 100644 index 0000000..2d192ad --- /dev/null +++ b/Block/Adminhtml/Form/Renderer/Config/ScheduleEveryFieldConfig.php @@ -0,0 +1,57 @@ +select = $select; + + parent::__construct($context); + } + + /** + * @param AbstractElement $element + * @return string + */ + protected function _getElementHtml(AbstractElement $element) { + $name = $element->getName(); + + $element->setStyle('width: 100px; margin-right: 20px;')->setName($name . '[]'); + + if ($element->getValue()) { + $values = explode(',', $element->getValue()); + } else { + $values = []; + } + + $field = $element->setValue(isset($values[0]) ? $values[0] : null)->getElementHtml(); + + $units = $this->select->setName($name . '[]') + ->setStyle('width: 142px;') + ->setForm($element->getForm()) + ->setValues([ + ['value' => 'min', 'label' => 'minute(s)'], + ['value' => 'hour', 'label' => 'hour(s)'] + ]) + ->setId('webscale_varnish_flush_cache_schedule_time') + ->setValue(isset($values[1]) ? $values[1] : null) + ->getElementHtml(); + + return '
' . $field . '
' + . '
' . $units . '
' + . '
'; + } +} diff --git a/Block/System/Config/Scheduled.php b/Block/System/Config/Scheduled.php new file mode 100644 index 0000000..2d9d675 --- /dev/null +++ b/Block/System/Config/Scheduled.php @@ -0,0 +1,39 @@ +getCacheConfigMessage() . $this->getScheduledConfigMessage(); + } + + /** + * Check if account and application is configured + * + * @return string + */ + private function getScheduledConfigMessage(): string + { + return $this->getMessageWrapper( + __('Enabling this feature will disable partial cache invalidation.' . + ' Full varnish cache flush will be executed instead, according to the cron expression configured below.' + ), 'warning' + ); + } +} diff --git a/Block/System/Config/Settings.php b/Block/System/Config/Settings.php index f415f37..339481b 100644 --- a/Block/System/Config/Settings.php +++ b/Block/System/Config/Settings.php @@ -6,54 +6,17 @@ namespace Webscale\Varnish\Block\System\Config; -use Webscale\Varnish\Helper\Config; -use Magento\Backend\Block\Context; -use Magento\Backend\Model\Auth\Session; -use Magento\Config\Block\System\Config\Form\Fieldset; -use Magento\Framework\View\Helper\Js; -use Magento\Backend\Model\UrlInterface; use Magento\PageCache\Model\Config as CacheConfig; -use Magento\Framework\Data\Form\Element\AbstractElement; /** * @SuppressWarnings(PHPMD.CamelCaseMethodName) */ -class Settings extends Fieldset +class Settings extends \Webscale\Varnish\Block\System\Config\SettingsAbstract { - /** - * @var Config $config - */ - protected $config; - - /** - * @param Context $context - * @param Session $authSession - * @param Js $jsHelper - * @param Config $config - * @param UrlInterface $urlBuilder - * @param CacheConfig $cacheConfig - * @param array $data - */ - public function __construct( - Context $context, - Session $authSession, - Js $jsHelper, - Config $config, - UrlInterface $urlBuilder, - CacheConfig $cacheConfig, - array $data = [] - ) { - $this->config = $config; - $this->urlBuilder = $urlBuilder; - $this->cacheConfig = $cacheConfig; - - parent::__construct($context, $authSession, $jsHelper, $data); - } - /** * Return header comment part of html for fieldset * - * @param AbstractElement $element + * @param \Magento\Framework\Data\Form\Element\AbstractElement $element * @return string * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -105,22 +68,4 @@ private function getApplicationConfigMessage(): string return ''; } - - /** - * Get message wrapper - * - * @param string $message - * @param string $severity - * @return string - */ - private function getMessageWrapper(string $message = '', string $severity = 'notice'): string - { - $html = '
'; - $html .= '
'; - $html .= '
'; - $html .= $message; - $html .= '
'; - - return $html; - } } diff --git a/Block/System/Config/SettingsAbstract.php b/Block/System/Config/SettingsAbstract.php new file mode 100644 index 0000000..03cfa0a --- /dev/null +++ b/Block/System/Config/SettingsAbstract.php @@ -0,0 +1,66 @@ +config = $config; + $this->urlBuilder = $urlBuilder; + $this->cacheConfig = $cacheConfig; + + parent::__construct($context, $authSession, $jsHelper); + } + + /** + * Get message wrapper + * + * @param string $message + * @param string $severity + * @return string + */ + public function getMessageWrapper(string $message = '', string $severity = 'notice'): string + { + $html = '
'; + $html .= '
'; + $html .= '
'; + $html .= $message; + $html .= '
'; + + return $html; + } +} diff --git a/Cron/CacheFlushScheduled.php b/Cron/CacheFlushScheduled.php new file mode 100644 index 0000000..b48d2de --- /dev/null +++ b/Cron/CacheFlushScheduled.php @@ -0,0 +1,58 @@ +config = $config; + $this->cacheConfig = $cacheConfig; + $this->purgeCache = $purgeCache; + } + + /** + * @return void + */ + public function execute() { + try { + if ( + $this->cacheConfig->getType() == CacheConfig::VARNISH + && $this->config->isAvailable() + && !empty($this->config->getCronExpression()) + ) { + $this->purgeCache->sendPurgeRequest(['tagsPattern' => ['.*'], 'event' => self::EVENT_NAME]); + $this->config->log('Executed scheduled varnish cache flush.'); + } + } catch (\Exception $e) { + $this->config->log('Unable to execute scheduled varnish cache flush.'); + $this->config->log($e->getMessage() . PHP_EOL . $e->getTraceAsString(), 'critical'); + } + + } +} diff --git a/Documentation/cache-events.png b/Documentation/cache-events.png new file mode 100644 index 0000000..3cf0b91 Binary files /dev/null and b/Documentation/cache-events.png differ diff --git a/Documentation/scheduled-full-cache-flush.png b/Documentation/scheduled-full-cache-flush.png new file mode 100644 index 0000000..3d4215d Binary files /dev/null and b/Documentation/scheduled-full-cache-flush.png differ diff --git a/Helper/Config.php b/Helper/Config.php index ccc898d..a2da7b3 100644 --- a/Helper/Config.php +++ b/Helper/Config.php @@ -6,12 +6,12 @@ namespace Webscale\Varnish\Helper; +use Webscale\Varnish\Model\Config\Source\Cron\Frequency; +use Webscale\Varnish\Model\Config\Cron\ScheduleFrequency; use Magento\Framework\App\Config\Storage\WriterInterface; -use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Helper\AbstractHelper; use Magento\Framework\App\Helper\Context; use Magento\Framework\Module\ModuleListInterface; -use Webscale\Varnish\Service\Api; use Webscale\Varnish\Logger\Logger; class Config extends AbstractHelper @@ -28,6 +28,9 @@ class Config extends AbstractHelper public const XML_PATH_EVENTS_PARTIAL = 'webscale_varnish/cache_events/partial_invalidate_events'; + public const XML_PATH_CACHE_SCHEDULE_EVERY = 'webscale_varnish/flush_cache_schedule/every'; + + public const DEFAULT_CRON_EXPRESSION = ['*', '*', '*', '*', '*']; /** @var ModuleListInterface $moduleList */ private $moduleList; @@ -108,33 +111,48 @@ public function getApplicationId(): string } /** - * Retrieve flush all events array + * Retrieve cache scheduled every * * @return array */ - public function getEventsFlushAll(): array + public function getCacheScheduleEvery(): array { - return $this->getEventList(self::XML_PATH_EVENTS_ALL); + $every = $this->scopeConfig->getValue(self::XML_PATH_CACHE_SCHEDULE_EVERY); + + return is_array($every) ? $every : explode(',', $every); } /** - * Retrieve partial invalidate events array + * Retrieve current value for cron expression + * + * @return string + */ + public function getCronExpression(): string + { + return (string) $this->scopeConfig->getValue(ScheduleFrequency::CRON_STRING_PATH); + } + + /** + * Retrieve flush all events array * * @return array */ - public function getEventsPartialInvalidate(): array + public function getEventsFlushAll(): array { - return $this->getEventList(self::XML_PATH_EVENTS_PARTIAL); + $events = $this->scopeConfig->getValue(self::XML_PATH_EVENTS_ALL); + + return is_array($events) ? $events : explode(',', $events); } /** - * @param string $scope + * Retrieve partial invalidate events array + * * @return array */ - public function getEventList($scope): array + public function getEventsPartialInvalidate(): array { - $events = $this->scopeConfig->getValue($scope); - if (empty($events)) $events = []; + $events = $this->scopeConfig->getValue(self::XML_PATH_EVENTS_PARTIAL); + return is_array($events) ? $events : explode(',', $events); } @@ -156,7 +174,7 @@ public function getCacheUri(): string */ public function generateCacheParams(array $purge = []): array { - return [ + $params = [ 'json' => [ 'type' => 'invalidate-cache', 'target' => '/v2/applications/' . $this->getApplicationId(), @@ -166,6 +184,8 @@ public function generateCacheParams(array $purge = []): array ] ], ]; + + return $params; } /** @@ -186,6 +206,48 @@ public function getVersion(): string return $version; } + /** + * @param string $value + * @return string + */ + public function getCronExpressionByValue($value, $data = ['every' => '', 'hours' => 0, 'minutes' => 0]) + { + switch($value) { + case Frequency::CRON_HOURLY: + $result = array_replace(self::DEFAULT_CRON_EXPRESSION, ['0']); + break; + case Frequency::CRON_DAILY: + $result = array_replace( self::DEFAULT_CRON_EXPRESSION, [$data['minutes'], $data['hours']]); + break; + case Frequency::CRON_CUSTOM: + $result = $this->getCustomCronExpression($data['every']); + break; + default: + $result = []; + } + + return $result; + } + + /** + * @param array $every + * @return array + */ + private function getCustomCronExpression($every = []): array + { + $result = []; + + if (is_array($every) && !empty($every[0]) && !empty($every[1])) { + if ($every[1] == 'hour') { + $result = array_replace( self::DEFAULT_CRON_EXPRESSION, ['0', '*/' . $every[0]]); + } else if ($every[1] == 'min') { + $result = array_replace( self::DEFAULT_CRON_EXPRESSION, ['*/' . $every[0]]); + } + } + + return $result; + } + /** * Write message to custom log * diff --git a/Model/Config/Cron/ScheduleFrequency.php b/Model/Config/Cron/ScheduleFrequency.php new file mode 100644 index 0000000..9f6932f --- /dev/null +++ b/Model/Config/Cron/ScheduleFrequency.php @@ -0,0 +1,137 @@ +_runModelPath = $runModelPath; + $this->_configValueFactory = $configValueFactory; + $this->config = $config; + parent::__construct($context, $registry, $scopeConfig, $cacheTypeList, $resource, $resourceCollection, $data); + } + + /** + * @return ScheduleFrequency + * @throws \Exception + */ + public function afterSave() + { + $time = $this->getData('groups/flush_cache_schedule/fields/start_time/value'); + $frequency = $this->getData('groups/flush_cache_schedule/fields/frequency/value'); + $every = $this->getData('groups/flush_cache_schedule/fields/every/value'); + + $cronExprArray = $this->getCronExpressionArray($frequency, $every, $time); + $cronExprString = join(' ', $cronExprArray); + + try { + $this->_configValueFactory->create()->load( + self::CRON_STRING_PATH, + 'path' + )->setValue( + $cronExprString + )->setPath( + self::CRON_STRING_PATH + )->save(); + + $this->_configValueFactory->create()->load( + self::CRON_MODEL_PATH, + 'path' + )->setValue( + $this->_runModelPath + )->setPath( + self::CRON_MODEL_PATH + )->save(); + } catch (\Exception $e) { + throw new \Exception(__('Can\'t save the cron expression.')); + } + + return parent::afterSave(); + } + + /** + * @param string $frequency + * @return array + */ + protected function getCronExpressionArray($frequency = '', $every = '', $time = []) + { + switch ($frequency) { + case Frequency::CRON_HOURLY: + $cronExprArray = $this->config->getCronExpressionByValue(Frequency::CRON_HOURLY); + break; + case Frequency::CRON_DAILY: + $cronExprArray = $this->config->getCronExpressionByValue(Frequency::CRON_DAILY, [ + 'hours' => intval($time[0]), + 'minutes' => intval($time[1]) + ]); + break; + case Frequency::CRON_CUSTOM: + $cronExprArray = $this->config->getCronExpressionByValue(Frequency::CRON_CUSTOM, [ + 'every' => $every + ]); + break; + default: + $cronExprArray = []; + break; + } + + return $cronExprArray; + } +} diff --git a/Model/Config/Source/Cron/Frequency.php b/Model/Config/Source/Cron/Frequency.php new file mode 100644 index 0000000..e1ea6b9 --- /dev/null +++ b/Model/Config/Source/Cron/Frequency.php @@ -0,0 +1,41 @@ + __('Disabled'), 'value' => static::CRON_DISABLED], + ['label' => __('Hourly'), 'value' => static::CRON_HOURLY], + ['label' => __('Daily'), 'value' => static::CRON_DAILY], + ['label' => __('Custom'), 'value' => static::CRON_CUSTOM], + ]; + } + return self::$options; + } +} diff --git a/Observer/FlushAllCacheObserver.php b/Observer/FlushAllCacheObserver.php index c3423ff..3756b7b 100644 --- a/Observer/FlushAllCacheObserver.php +++ b/Observer/FlushAllCacheObserver.php @@ -54,6 +54,7 @@ public function execute(Observer $observer): void if ($this->cacheConfig->getType() == CacheConfig::VARNISH && $this->config->isAvailable() && in_array($event->getName(), $events) + && empty($this->config->getCronExpression()) ) { $this->purgeCache->sendPurgeRequest(['tagsPattern' => ['.*'], 'event' => $event->getName()]); } diff --git a/Observer/InvalidateVarnishObserver.php b/Observer/InvalidateVarnishObserver.php index b85a100..a344da8 100644 --- a/Observer/InvalidateVarnishObserver.php +++ b/Observer/InvalidateVarnishObserver.php @@ -62,6 +62,7 @@ public function execute(Observer $observer): void if ($this->cacheConfig->getType() == CacheConfig::VARNISH && $this->config->isAvailable() && in_array($event->getName(), $events) + && empty($this->config->getCronExpression()) ) { $object = $event->getObject(); $tags = []; diff --git a/README.md b/README.md index 280f103..56dce29 100644 --- a/README.md +++ b/README.md @@ -40,23 +40,39 @@ Stores > Configuration > Webscale > Varnish Enable the module by switching `Enabled` to `Yes` under `General Configuration` section and enter `API token` and `Application Id`: -![Webacale Varnish Configuration](Documentation/enable-extension2.png "Webacale Varnish Configuration Page") +![Webscale Varnish Configuration](Documentation/enable-extension2.png "Webscale Varnish Configuration Page") Save the configuration. After setting up API token and Application Id navigate to `Stores > Configuration > Advanced > System`, open `Full Page Cache` section and select `Varnish` in `Caching Application` field: -![Webacale Varnish Configuration](Documentation/caching-application.png "Caching Application") +![Webscale Varnish Configuration](Documentation/caching-application.png "Caching Application") -### Optional +## Optional -You can also select `Enable Debug` under `Developer` section - this option will enable more detailed server logs: +### Debug Mode -![Webacale Varnish Configuration](Documentation/debug-logs.png "Debug Logging") +You can also select `Enable Debug` under `Developer` section - this will result to more detailed server logs: + +![Webscale Varnish Configuration](Documentation/debug-logs.png "Debug Logging") Log file can be found at `MAGENTO_ROOT/var/log/webscale.log`. +### Cache Events + +In this section all the magento observing events listed that is triggering varnish cache flush. By default - all selected, but it can be configured by selecting/deselecting items from list: + +![Webscale Varnish Configuration](Documentation/cache-events.png "Cache Flush by Events") + +### Scheduled Full Cache Flush + +> Enabling this feature will disable partial cache invalidation. Full varnish cache flush will be executed instead, according to the cron expression configured. + +![Webscale Varnish Configuration](Documentation/scheduled-full-cache-flush.png "Scheduled Full Cache Flush") + +Choose one of the three frequency modes: hourly, daily or custom. Cron expression helper will display configured cron expression to validate. + ## Managing Varnish Cache Webscale varnish cache will be flushed by default with all Magento native cache events, partial by tags or full cache flush. To flush specifically Webscale varnish cache navigate to `System > Tools > Cache Management` and click `Flush Varnish Cache` button under `Additional Cache Management` section: -![Webacale Varnish Configuration](Documentation/flush-cache.png "Flush Cache") +![Webscale Varnish Configuration](Documentation/flush-cache.png "Flush Cache") diff --git a/composer.json b/composer.json index 3199850..5c36be9 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "license": [ "MIT" ], - "version": "1.1.1", + "version": "1.2.0", "require": { "php": "^7.2 || ^8.1", "magento/magento2-base": "2.3.* || 2.4.*" diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index 6d980af..db8b100 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -54,6 +54,30 @@ 1 + + + Webscale\Varnish\Block\System\Config\Scheduled + + + Webscale\Varnish\Model\Config\Source\Cron\Frequency + Webscale\Varnish\Model\Config\Cron\ScheduleFrequency + + + + + Webscale\Varnish\Block\Adminhtml\Form\Renderer\Config\ScheduleEveryFieldConfig + required-entry validate-digits + + custom + + + + + + D + + + diff --git a/etc/config.xml b/etc/config.xml index fbc0ec6..04dfbd3 100644 --- a/etc/config.xml +++ b/etc/config.xml @@ -19,9 +19,28 @@ + + + + + 0 + + + + + + + + + + + + + + diff --git a/etc/crontab.xml b/etc/crontab.xml new file mode 100644 index 0000000..f7f0b6b --- /dev/null +++ b/etc/crontab.xml @@ -0,0 +1,14 @@ + + + + + + crontab/default/jobs/webscale_varnish_cache_flush_scheduled/schedule/cron_expr + + + diff --git a/etc/module.xml b/etc/module.xml index b62182d..98bae2d 100644 --- a/etc/module.xml +++ b/etc/module.xml @@ -7,5 +7,5 @@ --> - + diff --git a/i18n/en_US.csv b/i18n/en_US.csv index becc579..a85b055 100644 --- a/i18n/en_US.csv +++ b/i18n/en_US.csv @@ -1,8 +1,18 @@ +"Enabling this feature will disable partial cache invalidation. Full varnish cache flush will be executed instead, according to the cron expression configured below.","Enabling this feature will disable partial cache invalidation. Full varnish cache flush will be executed instead, according to the cron expression configured below." "Magento is configured to use the built-in Full Page Cache. To use Webscale varnish caching please change ""Caching Application"" to ""Varnish Cache"" under the ""Full Page Cache"" tab in System Configuration","Magento is configured to use the built-in Full Page Cache. To use Webscale varnish caching please change ""Caching Application"" to ""Varnish Cache"" under the ""Full Page Cache"" tab in System Configuration" -"Please configure API Token.","Please configure API Token." +"Please configure API Token and Application Id.","Please configure API Token and Application Id." "To be able to use Webscale varnish cache please configure ""Application Id"".","To be able to use Webscale varnish cache please configure ""Application Id""." "Varnish cache flushed successfully.","Varnish cache flushed successfully." "There is error occurred while trying to purge varnish cache. Please refer to logs for more information.","There is error occurred while trying to purge varnish cache. Please refer to logs for more information." +"Can't save the cron expression.","Can't save the cron expression." +Disabled,Disabled +Hourly,Hourly +Daily,Daily +Custom,Custom +Minute,Minute +Hour,Hour +Day,Day +"--Please Select--","--Please Select--" "Flush Varnish Cache","Flush Varnish Cache" "Webscale varnish cache.","Webscale varnish cache." Webscale,Webscale @@ -12,5 +22,12 @@ Enabled,Enabled Application,Application "API Token","API Token" "Application Id","Application Id" +"Cache Events","Cache Events" +"Flush All Events","Flush All Events" +"Partial Invalidate Events","Partial Invalidate Events" +"Scheduled Full Cache Flush","Scheduled Full Cache Flush" +Frequency,Frequency +Every,Every +Time,Time Developer,Developer "Enable Debug","Enable Debug" diff --git a/view/adminhtml/requirejs-config.js b/view/adminhtml/requirejs-config.js new file mode 100644 index 0000000..379689b --- /dev/null +++ b/view/adminhtml/requirejs-config.js @@ -0,0 +1,9 @@ +/** + * Copyright © Webscale. All rights reserved. + * See LICENSE for license details. + */ +var config = { + deps: [ + "Webscale_Varnish/js/cronexpression" + ] +}; diff --git a/view/adminhtml/web/css/source/_module.less b/view/adminhtml/web/css/source/_module.less index a11db74..70cf8a1 100644 --- a/view/adminhtml/web/css/source/_module.less +++ b/view/adminhtml/web/css/source/_module.less @@ -26,3 +26,9 @@ } } } + +/** Scheduled Cache Flush */ +#row_webscale_varnish_flush_cache_schedule_start_time td select:nth-of-type(3), +#row_webscale_varnish_flush_cache_schedule_start_time td span:nth-of-type(2) { + display: none; +} diff --git a/view/adminhtml/web/js/cronexpression.js b/view/adminhtml/web/js/cronexpression.js new file mode 100644 index 0000000..c9ae30a --- /dev/null +++ b/view/adminhtml/web/js/cronexpression.js @@ -0,0 +1,72 @@ +/** + * Copyright © Webscale. All rights reserved. + * See LICENSE for license details. + */ + +define(["jquery"], function($) { + "use strict"; + + let note = $('#row_webscale_varnish_flush_cache_schedule_frequency .note span') + let items = { + 'frequency': '#webscale_varnish_flush_cache_schedule_frequency', + 'everytext': '#webscale_varnish_flush_cache_schedule_every', + 'everytime': '#webscale_varnish_flush_cache_schedule_time', + 'timehour': '[data-ui-id="time-groups-flush-cache-schedule-fields-start-time-value-hour"]', + 'timeminute': '[data-ui-id="time-groups-flush-cache-schedule-fields-start-time-value-minute"]', + 'timesecond': '[data-ui-id="time-groups-flush-cache-schedule-fields-start-time-value-second"]' + }; + + let init = function () { + if (note && note.length) { + // $(items.timesecond).prop('disabled', 'disabled'); + return bind(); + } + } + + let bind = function() { + for (const key in items) { + const instance = $(items[key]); + instance.on('change', function (e) { + return change(e, instance); + }.bind(instance)); + } + + $(items.frequency).trigger('change'); + } + + /** + * Trigger change event and combine cron expression + */ + let change = function(e, element) { + let value = '', + frequency = $(items.frequency), + timeMinute = $(items.timeminute), + timeHour = $(items.timehour), + everyText = $(items.everytext), + everyTime = $(items.everytime); + + switch(frequency.val()) { + case 'H': + value = '0 * * * *'; + break; + case 'D': + value = parseInt($(timeMinute).val(), 10) + ' ' + parseInt($(timeHour).val(), 10) + ' * * *'; + break; + case 'custom': + if (everyText.val() !== "undefined" && everyText.val() !== '') { + let txtValue = everyText.val(); + if (everyTime.val() == 'hour') { + value = '0 */' + txtValue + ' * * *'; + } else if (everyTime.val() == 'min') { + value = '*/' + txtValue + ' * * * *'; + } + } + break; + + } + + note.html('Cron Expression: ' + (value ? '' + value + '' : 'none')); + } + + return init(); +});