diff --git a/CHANGELOG-8.0.md b/CHANGELOG-8.0.md index aa3c1df81d..f470862e7a 100644 --- a/CHANGELOG-8.0.md +++ b/CHANGELOG-8.0.md @@ -8,6 +8,11 @@ - Support PSR caching interface, related functionalities and applied them on module cache. - Registration of environment variables via Symfony Dotenv Component - Interface for storing Symfony Service Container parameters in configuration +- Support Symfony caching interface with tags + +### Deprecated + +- `Utils` methods for managing cache will be replaced by using Symfony cache directly ### Changed @@ -25,6 +30,8 @@ All corresponding command-line parameters were removed - Updated list of Search Engines (formerly `aRobots` configuration) - Browser-based application setup was discontinued. Only console-based setup is available +- Replace file caching in `Utils` with Symfony cache +- Removed $includePermanentCache parameter from `oxResetFileCache` method, all cache files are now cleared without exclusions. ### Removed @@ -56,4 +63,5 @@ container. - Deprecated `handleDatabaseException` functionality - Dependency on `oxideshop-facts` component -- `FileCache` and `SubShopSpecificFileCache` classes. Use `ContextInterface::getCurrentShopId()` instead \ No newline at end of file +- `FileCache` and `SubShopSpecificFileCache` classes. Use `ContextInterface::getCurrentShopId()` instead +- Legacy file-based caching methods from `Utils` class \ No newline at end of file diff --git a/source/Application/Controller/Admin/NavigationTree.php b/source/Application/Controller/Admin/NavigationTree.php index f9118a44d8..8579d4c106 100644 --- a/source/Application/Controller/Admin/NavigationTree.php +++ b/source/Application/Controller/Admin/NavigationTree.php @@ -14,6 +14,8 @@ use OxidEsales\Eshop\Core\Str; use OxidEsales\EshopCommunity\Core\Di\ContainerFacade; use stdClass; +use Symfony\Contracts\Cache\TagAwareCacheInterface; +use Symfony\Contracts\Cache\ItemInterface; class NavigationTree extends Base { @@ -393,60 +395,38 @@ protected function processCachedFile($cacheContents) return $cacheContents; } - /** - * get initial dom, not modified by init method - * - * @return DOMDocument - */ protected function getInitialDom() { - if ($this->_oInitialDom === null) { - $myOxUtlis = \OxidEsales\Eshop\Core\Registry::getUtils(); - - if (is_array($filesToLoad = $this->getMenuFiles())) { - // now checking if xml files are newer than cached file - $reload = false; - $templateLanguageCode = $this->getTemplateLanguageCode(); - - $shopId = \OxidEsales\Eshop\Core\Registry::getConfig()->getActiveShop()->getShopId(); - $cacheName = 'menu_' . $templateLanguageCode . $shopId . '_xml'; - $cacheFile = $myOxUtlis->getCacheFilePath($cacheName); - $cacheContents = $myOxUtlis->fromFileCache($cacheName); - if ($cacheContents && file_exists($cacheFile) && ($cacheModTime = filemtime($cacheFile))) { - foreach ($filesToLoad as $dynPath) { - if ($cacheModTime < filemtime($dynPath)) { - $reload = true; - } - } - } else { - $reload = true; - } + if ($this->_oInitialDom !== null) { + return $this->_oInitialDom; + } - $this->_oInitialDom = new DOMDocument(); - if ($reload) { - // fully reloading and building pathes - $this->_oInitialDom->appendChild(new DOMElement('OX')); + $filesToLoad = $this->getMenuFiles(); + if (!is_array($filesToLoad)) { + return null; + } - foreach ($filesToLoad as $dynPath) { - $this->loadFromFile($dynPath, $this->_oInitialDom); - } + $templateLanguageCode = $this->getTemplateLanguageCode(); + $cacheName = 'shop_menu_cache_' . $templateLanguageCode; + $cache = ContainerFacade::get(TagAwareCacheInterface::class); - // adds links to menu items - $this->addLinks($this->_oInitialDom); + if ($this->isMenuCacheOutdated($cache, $cacheName, $filesToLoad)) { + $cache->delete($cacheName); + } - // writing to cache - $myOxUtlis->toFileCache($cacheName, $this->_oInitialDom->saveXML()); - } else { - $cacheContents = $this->processCachedFile($cacheContents); - // loading from cached file - $this->_oInitialDom->preserveWhiteSpace = false; - $this->_oInitialDom->loadXML($cacheContents); - } + $cacheContents = $cache->get($cacheName, function (ItemInterface $item) use ($filesToLoad): array { + $item->tag('oxid_esales.cache.menu'); + return [ + 'creation_time' => time(), + 'menu_dom' => $this->generateInitialMenuDomXml($filesToLoad) + ]; + }); - // add session params - $this->sessionizeLocalUrls($this->_oInitialDom); - } - } + $this->_oInitialDom = new DOMDocument(); + $this->_oInitialDom->preserveWhiteSpace = false; + $this->_oInitialDom->loadXML($cacheContents['menu_dom']); + + $this->sessionizeLocalUrls($this->_oInitialDom); return $this->_oInitialDom; } @@ -643,4 +623,36 @@ protected function getTemplateLanguageCode() protected function onGettingDomXml() { } + + private function isMenuCacheOutdated($cache, string $cacheName, array $filesToLoad): bool + { + $cacheItem = $cache->getItem($cacheName); + + if (!$cacheItem->isHit()) { + return true; + } + + $cacheCreationTime = $cacheItem->get()['creation_time']; + foreach ($filesToLoad as $filePath) { + if ($cacheCreationTime < filemtime($filePath)) { + return true; + } + } + + return false; + } + + private function generateInitialMenuDomXml(array $filesToLoad): string + { + $initialDom = new DOMDocument(); + $initialDom->appendChild(new DOMElement('OX')); + + foreach ($filesToLoad as $filePath) { + $this->loadFromFile($filePath, $initialDom); + } + + $this->addLinks($initialDom); + + return $initialDom->saveXML(); + } } diff --git a/source/Application/Controller/OxidStartController.php b/source/Application/Controller/OxidStartController.php index 3ab6470f2a..21c1d3fedf 100644 --- a/source/Application/Controller/OxidStartController.php +++ b/source/Application/Controller/OxidStartController.php @@ -74,9 +74,6 @@ public function pageClose() if (isset($session)) { $session->freeze(); } - - //commit file cache - \OxidEsales\Eshop\Core\Registry::getUtils()->commitFileCache(); } /** diff --git a/source/Core/Config.php b/source/Core/Config.php index 35d679d6a4..083a408d34 100644 --- a/source/Core/Config.php +++ b/source/Core/Config.php @@ -1020,7 +1020,6 @@ public function getDir($file, $dir, $admin, $lang = null, $shop = null, $theme = // TODO: implement logic to log missing paths - // to cache Registry::getUtils()->toStaticCache($cacheKey, $return); return $return; diff --git a/source/Core/Language.php b/source/Core/Language.php index f81aa19649..3372df2601 100644 --- a/source/Core/Language.php +++ b/source/Core/Language.php @@ -15,6 +15,8 @@ use OxidEsales\Eshop\Core\Registry; use OxidEsales\Eshop\Core\Str; use stdClass; +use Symfony\Contracts\Cache\ItemInterface; +use Symfony\Contracts\Cache\TagAwareCacheInterface; /** * Language related utility class @@ -724,7 +726,7 @@ protected function getCustomThemeLanguageFiles($language) $aLangFiles = []; if ($sCustomTheme) { - $customThemePath = $sAppDir . 'views/' . $sCustomTheme .'/'; + $customThemePath = $sAppDir . 'views/' . $sCustomTheme . '/'; $aLangFiles = array_merge($aLangFiles, $this->getThemeLanguageFiles($customThemePath, $sLang)); } @@ -893,29 +895,20 @@ protected function getLangFileCacheName($blAdmin, $iLang, $aLangFiles = null) return "langcache_" . ((int) $blAdmin) . "_{$iLang}_" . $myConfig->getShopId() . "_" . $myConfig->getConfigParam('sTheme') . $sLangFilesIdent; } - /** - * Returns language cache array - * - * @param bool $blAdmin admin or not [optional] - * @param int $iLang current language id [optional] - * @param array $aLangFiles language files to load [optional] - * - * @return array - */ protected function getLanguageFileData($blAdmin = false, $iLang = 0, $aLangFiles = null) { - $myUtils = Registry::getUtils(); - $sCacheName = $this->getLangFileCacheName($blAdmin, $iLang, $aLangFiles); - $aLangCache = $myUtils->getLangCache($sCacheName); - if (!$aLangCache && $aLangFiles === null) { - if ($blAdmin) { - $aLangFiles = $this->getAdminLangFilesPathArray($iLang); - } else { - $aLangFiles = $this->getLangFilesPathArray($iLang); + + $cache = ContainerFacade::get(TagAwareCacheInterface::class); + $aLangCache = $cache->get($sCacheName, function (ItemInterface $item) use ($aLangFiles, $blAdmin, $iLang): array { + $item->tag('oxid_esales.cache.language'); + if ($aLangFiles === null) { + if ($blAdmin) { + $aLangFiles = $this->getAdminLangFilesPathArray($iLang); + } else { + $aLangFiles = $this->getLangFilesPathArray($iLang); + } } - } - if (!$aLangCache && $aLangFiles) { $aLangCache = []; $sBaseCharset = $this->getTranslationsExpectedEncoding(); $aLang = []; @@ -941,9 +934,8 @@ protected function getLanguageFileData($blAdmin = false, $iLang = 0, $aLangFiles // special character replacement list $aLangCache['_aSeoReplaceChars'] = $aLangSeoReplaceChars; - //save to cache - $myUtils->setLangCache($sCacheName, $aLangCache); - } + return $aLangCache; + }); return $aLangCache; } diff --git a/source/Core/Utils.php b/source/Core/Utils.php index 0d0336d4e3..49d656ef4e 100644 --- a/source/Core/Utils.php +++ b/source/Core/Utils.php @@ -12,7 +12,10 @@ use OxidEsales\Eshop\Core\TableViewNameGenerator; use OxidEsales\EshopCommunity\Core\Di\ContainerFacade; use OxidEsales\EshopCommunity\Internal\Transition\ShopEvents\ApplicationExitEvent; +use Psr\Cache\CacheItemPoolInterface; use stdClass; +use Symfony\Contracts\Cache\ItemInterface; +use Symfony\Contracts\Cache\TagAwareCacheInterface; use Symfony\Component\Filesystem\Path; use function is_array; @@ -383,66 +386,9 @@ public function cleanStaticCache($sCacheName = null) } /** - * Generates php file, which could later be loaded as include instead of parsed data. - * Currently this method supports simple arrays only. + * @deprecated will be removed in next major version * - * @param string $sKey Cache key - * @param mixed $mContents Cache contents. At this moment only simple array type is supported. - */ - public function toPhpFileCache($sKey, $mContents) - { - //only simple arrays are supported - if (is_array($mContents) && ($sCachePath = $this->getCacheFilePath($sKey, false, 'php'))) { - // setting meta - $this->setCacheMeta($sKey, ["serialize" => false, "cachepath" => $sCachePath]); - - // caching.. - $this->toFileCache($sKey, $mContents); - } - } - - /** - * Includes cached php file and loads stored contents. - * - * @param string $sKey Cache key. - * - * @return null - */ - public function fromPhpFileCache($sKey) - { - // setting meta - $this->setCacheMeta($sKey, ["include" => true, "cachepath" => $this->getCacheFilePath($sKey, false, 'php')]); - - return $this->fromFileCache($sKey); - } - - /** - * If available returns cache meta data array - * - * @param string $sKey meta data/cache key - * - * @return mixed - */ - public function getCacheMeta($sKey) - { - return isset($this->_aFileCacheMeta[$sKey]) ? $this->_aFileCacheMeta[$sKey] : false; - } - - /** - * Saves cache meta data (information) - * - * @param string $sKey meta data/cache key - * @param array $aMeta meta data array - */ - public function setCacheMeta($sKey, $aMeta) - { - // cache meta data - $this->_aFileCacheMeta[$sKey] = $aMeta; - } - - /** * Adds contents to cache contents by given key. Returns true on success. - * All file caches are supposed to be written once by commitFileCache() method. * * @param string $sKey Cache key * @param mixed $mContents Contents to cache @@ -452,21 +398,19 @@ public function setCacheMeta($sKey, $aMeta) */ public function toFileCache($sKey, $mContents, $iTtl = 0) { - $aCacheData['content'] = $mContents; - $aMeta = $this->getCacheMeta($sKey); + $cache = ContainerFacade::get(CacheItemPoolInterface::class); + $cacheItem = $cache->getItem($sKey)->set($mContents); if ($iTtl) { - $aCacheData['ttl'] = $iTtl; - $aCacheData['timestamp'] = Registry::getUtilsDate()->getTime(); + $cacheItem->expiresAfter($iTtl); } - $this->_aFileCacheContents[$sKey] = $aCacheData; + $cache->save($cacheItem); - // looking for cache meta - $sCachePath = isset($aMeta["cachepath"]) ? $aMeta["cachepath"] : $this->getCacheFilePath($sKey); - - return (bool) $this->lockFile($sCachePath, $sKey); + return true; } /** + * @deprecated will be removed in next major version + * * Fetches contents from file cache. * * @param string $sKey Cache key @@ -475,244 +419,45 @@ public function toFileCache($sKey, $mContents, $iTtl = 0) */ public function fromFileCache($sKey) { - if (!array_key_exists($sKey, $this->_aFileCacheContents)) { - $aMeta = $this->getCacheMeta($sKey); - $sCachePath = isset($aMeta["cachepath"]) ? $aMeta["cachepath"] : $this->getCacheFilePath($sKey); - - clearstatcache(); - if (is_readable($sCachePath)) { - $this->lockFile($sCachePath, $sKey, LOCK_SH); - - $blInclude = isset($aMeta["include"]) ? $aMeta["include"] : false; - $sRes = $blInclude ? $this->includeFile($sCachePath) : $this->readFile($sCachePath); - - if (isset($sRes['ttl']) && $sRes['ttl'] != 0) { - $iTimestamp = $sRes['timestamp']; - $iTtl = $sRes['ttl']; - - $iTime = Registry::getUtilsDate()->getTime(); - if ($iTime > $iTimestamp + $iTtl) { - return null; - } - } - - $this->_aFileCacheContents[$sKey] = $sRes; - - $this->releaseFile($sKey, LOCK_SH); - } - } - - return isset($this->_aFileCacheContents[$sKey]) ? $this->_aFileCacheContents[$sKey]['content'] : null; - } - - /** - * Reads and returns cache file contents - * - * @param string $sFilePath cache fiel path - * - * @return string - */ - protected function readFile($sFilePath) - { - $sRes = file_get_contents($sFilePath); - - return $sRes ? unserialize($sRes) : null; - } - - /** - * Includes cache file - * - * @param string $sFilePath cache file path - * - * @return mixed - */ - protected function includeFile($sFilePath) - { - $_aCacheContents = null; - include $sFilePath; - - return $_aCacheContents; - } - - /** - * Serializes or writes php array for class file cache - * - * @param string $sKey cache key - * @param mixed $mContents cache data - * - * @return mixed - */ - protected function processCache($sKey, $mContents) - { - // looking for cache meta - $aCacheMeta = $this->getCacheMeta($sKey); - $blSerialize = isset($aCacheMeta["serialize"]) ? $aCacheMeta["serialize"] : true; - - if ($blSerialize) { - $mContents = serialize($mContents); - } else { - $mContents = ""; + $cache = ContainerFacade::get(CacheItemPoolInterface::class); + if ($cache->hasItem($sKey)) { + $cacheItem = $cache->getItem($sKey); + return $cacheItem->get(); } - return $mContents; - } - - /** - * Writes all cache contents to file at once. This method was introduced due to possible - * race conditions. Cache is cleaned up after commit - */ - public function commitFileCache() - { - if (!empty($this->_aLockedFileHandles[LOCK_EX])) { - startProfile("!__SAVING CACHE__! (warning)"); - foreach ($this->_aLockedFileHandles[LOCK_EX] as $sKey => $rHandle) { - if ($rHandle !== false && isset($this->_aFileCacheContents[$sKey])) { - // #0002931A truncate file once more before writing - ftruncate($rHandle, 0); - - // writing cache - fwrite($rHandle, $this->processCache($sKey, $this->_aFileCacheContents[$sKey])); - - // releasing locks - $this->releaseFile($sKey); - } - } - - stopProfile("!__SAVING CACHE__! (warning)"); - - //empty buffer - $this->_aFileCacheContents = []; - } + return null; } /** - * Locks cache file and returns its handle on success or false on failure - * - * @param string $sFilePath name of file to lock - * @param string $sIdent lock identifier - * @param int $iLockMode lock mode - LOCK_EX/LOCK_SH - * - * @return mixed lock file resource or false on error + * @deprecated will be removed in next major version */ - protected function lockFile($sFilePath, $sIdent, $iLockMode = LOCK_EX) + public function oxResetFileCache() { - $rHandle = isset($this->_aLockedFileHandles[$iLockMode][$sIdent]) ? $this->_aLockedFileHandles[$iLockMode][$sIdent] : null; - if ($rHandle === null) { - $blLocked = false; - $rHandle = @fopen($sFilePath, "a+"); - - if ($rHandle !== false) { - if (flock($rHandle, $iLockMode | LOCK_NB)) { - if ($iLockMode === LOCK_EX) { - // truncate file - $blLocked = ftruncate($rHandle, 0); - } else { - // move to a start position - $blLocked = fseek($rHandle, 0) === 0; - } - } - - // on failure - closing and setting false.. - if (!$blLocked) { - fclose($rHandle); - $rHandle = false; - } - } - - // in case system does not support file locking - if (!$blLocked && $iLockMode === LOCK_EX) { - // clearing on first call - if (count($this->_aLockedFileHandles) == 0) { - clearstatcache(); - } - - // start a blank file to inform other processes we are dealing with it. - if (!(file_exists($sFilePath) && !filesize($sFilePath) && abs(time() - filectime($sFilePath) < 40))) { - $rHandle = @fopen($sFilePath, "w"); - } - } - - $this->_aLockedFileHandles[$iLockMode][$sIdent] = $rHandle; - } - - return $rHandle; + $cache = ContainerFacade::get(CacheItemPoolInterface::class); + $cache->clear(); } /** - * Releases file lock and returns release state + * @deprecated will be removed in next major version * - * @param string $sIdent lock ident - * @param int $iLockMode lock mode - * - * @return bool - */ - protected function releaseFile($sIdent, $iLockMode = LOCK_EX) - { - $blSuccess = true; - if ( - isset($this->_aLockedFileHandles[$iLockMode][$sIdent]) && - $this->_aLockedFileHandles[$iLockMode][$sIdent] !== false - ) { - // release the lock and close file - $blSuccess = flock($this->_aLockedFileHandles[$iLockMode][$sIdent], LOCK_UN) && - fclose($this->_aLockedFileHandles[$iLockMode][$sIdent]); - unset($this->_aLockedFileHandles[$iLockMode][$sIdent]); - } - - return $blSuccess; - } - - /** - * Removes most files stored in cache (default 'tmp') folder. Some files - * e.g. table files names description, are left. Excluded cache file name - * patterns are defined in \OxidEsales\Eshop\Core\Utils::_sPermanentCachePattern parameter - */ - public function oxResetFileCache(bool $includePermanentCache = false) - { - $cacheFilePath = $this->getCacheFilePath(null, true); - $files = $cacheFilePath ?? glob($cacheFilePath . '*'); - if (is_array($files)) { - if (!$includePermanentCache) { - // delete all the files, except cached tables field names - $files = preg_grep($this->_sPermanentCachePattern, $files, PREG_GREP_INVERT); - } - - foreach ($files as $file) { - @unlink($file); - } - } - } - - /** * Removes language constant cache */ public function resetLanguageCache() { - $aFiles = glob($this->getCacheFilePath(null, true) . '*'); - if (is_array($aFiles)) { - // delete all language cache files - $sPattern = $this->_sLanguageCachePattern; - $aFiles = preg_grep($sPattern, $aFiles); - foreach ($aFiles as $sFile) { - @unlink($sFile); - } - } + + $cache = ContainerFacade::get(TagAwareCacheInterface::class); + $cache->invalidateTags(['oxid_esales.cache.language']); } /** + * @deprecated will be removed in next major version + * * Removes admin menu cache */ public function resetMenuCache() { - $aFiles = glob($this->getCacheFilePath(null, true) . '*'); - if (is_array($aFiles)) { - // delete all menu cache files - $sPattern = $this->_sMenuCachePattern; - $aFiles = preg_grep($sPattern, $aFiles); - foreach ($aFiles as $sFile) { - @unlink($sFile); - } - } + $cache = ContainerFacade::get(TagAwareCacheInterface::class); + $cache->invalidateTags(['oxid_esales.cache.menu']); } /** @@ -988,7 +733,6 @@ protected function prepareToExit() { $session = Registry::getSession(); $session->freeze(); - $this->commitFileCache(); ContainerFacade::dispatch(new ApplicationExitEvent()); @@ -1179,76 +923,33 @@ public function oxMimeContentType($sFileName) } /** - * Returns full path (including file name) to cache file - * - * @param string $sCacheName cache file name - * @param bool $blPathOnly if TRUE, name parameter will be ignored and only cache folder will be returned (default FALSE) - * @param string $sExtension cache file extension - * - * @return string - */ - public function getCacheFilePath($sCacheName, $blPathOnly = false, $sExtension = 'txt') - { - $versionPrefix = $this->getEditionCacheFilePrefix(); - - $sPath = realpath(ContainerFacade::getParameter('oxid_esales.build_directory')); - - if (!$sPath) { - return false; - } - - return $blPathOnly ? "{$sPath}/" : "{$sPath}/ox{$versionPrefix}c_{$sCacheName}." . $sExtension; - } - - /** - * Get current edition prefix - * @return string - */ - public function getEditionCacheFilePrefix() - { - return ''; - } - - /** - * Tries to load lang cache array from cache file - * - * @param string $sCacheName cache file name + * @deprecated will be removed in next major version * * @return array */ - public function getLangCache($sCacheName) + public function getLangCache($cacheName) { - $aLangCache = null; - $sFilePath = $this->getCacheFilePath($sCacheName); - if (file_exists($sFilePath) && is_readable($sFilePath)) { - include $sFilePath; + $cache = ContainerFacade::get(CacheItemPoolInterface::class); + if (!$cache->hasItem($cacheName)) { + return null; } - return $aLangCache; + return $cache->getItem($cacheName)->get(); } /** - * Writes language array to file cache - * - * @param string $sCacheName name of cache file - * @param array $aLangCache language array - * - * @return null + * @deprecated will be removed in next major version */ - public function setLangCache($sCacheName, $aLangCache) + public function setLangCache($cacheName, $langCache) { - $sCache = ""; - $sFileName = $this->getCacheFilePath($sCacheName); - - $tmpFile = Path::join( - ContainerFacade::getParameter('oxid_esales.build_directory'), - basename($sFileName) . uniqid('.temp', true) . '.txt' - ); - $blRes = file_put_contents($tmpFile, $sCache, LOCK_EX); + $cache = ContainerFacade::get(TagAwareCacheInterface::class); + $cache->get($cacheName, function (ItemInterface $item) use ($langCache) { + $item->tag('oxid_esales.cache.language'); - rename($tmpFile, $sFileName); + return $langCache; + }); - return $blRes; + return true; } /** diff --git a/source/Internal/Framework/Cache/Pool/FilesystemCacheItemPoolFactory.php b/source/Internal/Framework/Cache/Adapter/FilesystemTagAwareAdapterFactory.php similarity index 54% rename from source/Internal/Framework/Cache/Pool/FilesystemCacheItemPoolFactory.php rename to source/Internal/Framework/Cache/Adapter/FilesystemTagAwareAdapterFactory.php index 34817e9c06..e0c60773b0 100644 --- a/source/Internal/Framework/Cache/Pool/FilesystemCacheItemPoolFactory.php +++ b/source/Internal/Framework/Cache/Adapter/FilesystemTagAwareAdapterFactory.php @@ -7,24 +7,24 @@ declare(strict_types=1); -namespace OxidEsales\EshopCommunity\Internal\Framework\Cache\Pool; +namespace OxidEsales\EshopCommunity\Internal\Framework\Cache\Adapter; use OxidEsales\EshopCommunity\Internal\Transition\Utility\ContextInterface; -use Psr\Cache\CacheItemPoolInterface; -use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\Adapter\FilesystemTagAwareAdapter; +use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface; use Symfony\Component\Filesystem\Path; -class FilesystemCacheItemPoolFactory implements CacheItemPoolFactoryInterface +class FilesystemTagAwareAdapterFactory implements TagAwareAdapterFactoryInterface { public function __construct(private readonly ContextInterface $context) { } - public function create(int $shopId): CacheItemPoolInterface + public function create(int $shopId): TagAwareAdapterInterface { - return new FilesystemAdapter( + return new FilesystemTagAwareAdapter( namespace: "cache_items_shop_$shopId", - directory: Path::join($this->context->getCacheDirectory(), 'pool',) + directory: Path::join($this->context->getCacheDirectory(), 'pool') ); } } diff --git a/source/Internal/Framework/Cache/Adapter/TagAwareAdapterFactoryInterface.php b/source/Internal/Framework/Cache/Adapter/TagAwareAdapterFactoryInterface.php new file mode 100644 index 0000000000..8bc411db78 --- /dev/null +++ b/source/Internal/Framework/Cache/Adapter/TagAwareAdapterFactoryInterface.php @@ -0,0 +1,15 @@ +shopAdapter->invalidateModulesCache(); $this->templateCacheService->invalidateCache($shopId); - $this->cacheItemPoolFactory->create($shopId)->clear(); + $this->tagAwareAdapterFactory->create($shopId)->clear(); } public function clearAll(): void @@ -36,7 +36,7 @@ public function clearAll(): void $this->shopAdapter->invalidateModulesCache(); $this->templateCacheService->invalidateAllShopsCache(); foreach ($this->context->getAllShopIds() as $shopId) { - $this->cacheItemPoolFactory->create($shopId)->clear(); + $this->tagAwareAdapterFactory->create($shopId)->clear(); } } } diff --git a/source/Internal/Framework/Cache/services.yaml b/source/Internal/Framework/Cache/services.yaml index f364560720..c143db591b 100644 --- a/source/Internal/Framework/Cache/services.yaml +++ b/source/Internal/Framework/Cache/services.yaml @@ -1,5 +1,5 @@ imports: - - { resource: Pool/services.yaml } + - { resource: Adapter/services.yaml } - { resource: Command/services.yaml } services: diff --git a/source/bootstrap.php b/source/bootstrap.php index cf39a64e55..b37fc37e78 100644 --- a/source/bootstrap.php +++ b/source/bootstrap.php @@ -77,4 +77,4 @@ static function () { (new DotenvLoader(INSTALLATION_ROOT_PATH))->loadEnvironmentVariables(); -date_default_timezone_set(getenv('OXID_DEFAULT_TIMEZONE')); +date_default_timezone_set(getenv('OXID_DEFAULT_TIMEZONE') ?: 'Europe/Berlin'); diff --git a/tests/Integration/Application/Model/DiagnosticsOutputTest.php b/tests/Integration/Application/Model/DiagnosticsOutputTest.php index 8bdbd665cd..b476f4abf9 100644 --- a/tests/Integration/Application/Model/DiagnosticsOutputTest.php +++ b/tests/Integration/Application/Model/DiagnosticsOutputTest.php @@ -26,11 +26,7 @@ public function testDownloadResultFileWillSetCorrectContentLengthHeader(): void $contentLength = strlen($content); $utils = $this->getUtils(); - $cacheFilePath = $utils->getCacheFilePath($this->key); - file_put_contents( - $cacheFilePath, - serialize(['content' => $content]) - ); + $utils->toFileCache($this->key, $content); $diagnostics = new DiagnosticsOutput(); ob_start(); $diagnostics->downloadResultFile($this->key); @@ -73,7 +69,7 @@ public function testDownloadResultFilePrintsOutput(): void { $utils = $this->getUtils(); $content = 'some-content-123'; - file_put_contents($utils->getCacheFilePath($this->key), serialize(['content' => $content])); + $utils->toFileCache($this->key, $content); $this->expectOutputString($content); diff --git a/tests/Integration/Core/LanguageTest.php b/tests/Integration/Core/LanguageTest.php new file mode 100644 index 0000000000..29ff585399 --- /dev/null +++ b/tests/Integration/Core/LanguageTest.php @@ -0,0 +1,62 @@ +createMock(LoggerInterface::class); + Registry::set('logger', $logger); + + $config = Registry::getConfig(); + + $logger->expects($this->once()) + ->method('warning') + ->with(sprintf('translation for %s not found', $stringToTranslate), $this->anything()); + + $language = new Language(); + $translatedString = $language->translateString($stringToTranslate, $language->getBaseLanguage()); + + $this->assertEquals($stringToTranslate, $translatedString); + + $cache->invalidateTags(['oxid_esales.cache.language']); + + $langCacheName = sprintf( + 'langcache_%d_%s_%d_%s_default', + $config->isAdmin(), + $language->getBaseLanguage(), + $config->getShopId(), + $config->getConfigParam('sTheme') + ); + + $cache->get($langCacheName, function (ItemInterface $item) use ($stringToTranslate) { + $item->tag('oxid_esales.cache.language'); + return [$stringToTranslate => 'translated value']; + }); + + $language = new Language(); + $translatedString = $language->translateString($stringToTranslate, $language->getBaseLanguage()); + + $this->assertEquals('translated value', $translatedString); + + $cache->invalidateTags(['oxid_esales.cache.language']); + } +} diff --git a/tests/Integration/Core/UtilsTest.php b/tests/Integration/Core/UtilsTest.php new file mode 100644 index 0000000000..80f61f1d80 --- /dev/null +++ b/tests/Integration/Core/UtilsTest.php @@ -0,0 +1,106 @@ +toFileCache($key, $value); + + $this->assertEquals($value, $utils->fromFileCache($key)); + } + + public function testToFileCacheOverrideValue(): void + { + $utils = Registry::getUtils(); + $Key = "testCacheKey"; + $value1 = "testCacheFirstValue"; + $value2 = "testCacheSecondValue"; + + $utils->toFileCache($Key, $value1); + + $this->assertEquals($value1, $utils->fromFileCache($Key)); + + $utils->toFileCache($Key, $value2); + + $this->assertEquals($value2, $utils->fromFileCache($Key)); + } + + public function testLangCache(): void + { + $utils = Registry::getUtils(); + $langCache = ['TEST' => 'test value']; + $cacheName = 'testCacheName'; + + $utils->setLangCache($cacheName, $langCache); + + $this->assertEquals($langCache, $utils->getLangCache($cacheName)); + } + + public function testDeleteLanguageCache(): void + { + $utils = Registry::getUtils(); + $keyLang1 = 'lang_1_0_0'; + $keyLang2 = 'lang_1_0_0'; + $testLang = ['key1' => 'testVal1', 'key2' => 'testVal2']; + + $utils->setLangCache($keyLang1, $testLang); + $utils->setLangCache($keyLang2, $testLang); + + $utils->resetLanguageCache(); + + $this->assertEmpty($utils->fromFileCache($keyLang1)); + $this->assertEmpty($utils->fromFileCache($keyLang2)); + } + + public function testDeleteMenuCache(): void + { + $utils = Registry::getUtils(); + $keyLang1 = 'lang_1_0_0'; + $keyLang2 = 'lang_1_0_0'; + $testLang = ['key1' => 'testVal1', 'key2' => 'testVal2']; + + $utils->setLangCache($keyLang1, $testLang); + $utils->setLangCache($keyLang2, $testLang); + + $utils->resetLanguageCache(); + + $this->assertEmpty($utils->fromFileCache($keyLang1)); + $this->assertEmpty($utils->fromFileCache($keyLang2)); + } + + public function testCacheResetShouldNotRemoveCacheFilesFromSubdirectories(): void + { + $utils = Registry::getUtils(); + $context = ContainerFacade::get(ContextInterface::class); + + $cachedTestPhpFile = Path::join($context->getCacheDirectory(), 'myTestSubCacheDir', 'test_cache_file.php'); + $cachedTestTxtFile = Path::join($context->getCacheDirectory(), 'myTestSubCacheDir2', 'test_cache_file.txt'); + $filesystem = ContainerFacade::get('oxid_esales.symfony.file_system'); + + $filesystem->dumpFile($cachedTestPhpFile, ''); + $filesystem->dumpFile($cachedTestTxtFile, ''); + $utils->oxResetFileCache(); + + $this->assertFileExists($cachedTestPhpFile); + $this->assertFileExists($cachedTestTxtFile); + } +} diff --git a/tests/Integration/Legacy/Application/Controller/Admin/NavigationTree/NavigationTreeTest.php b/tests/Integration/Legacy/Application/Controller/Admin/NavigationTree/NavigationTreeTest.php index 386741c86a..82998e5aca 100644 --- a/tests/Integration/Legacy/Application/Controller/Admin/NavigationTree/NavigationTreeTest.php +++ b/tests/Integration/Legacy/Application/Controller/Admin/NavigationTree/NavigationTreeTest.php @@ -13,6 +13,7 @@ use DOMXPath; use oxfield; use OxidEsales\EshopCommunity\Application\Controller\Admin\NavigationTree; +use OxidEsales\EshopCommunity\Core\Di\ContainerFacade; use OxidEsales\EshopCommunity\Core\Registry; use OxidEsales\EshopCommunity\Internal\Container\ContainerFactory; use OxidEsales\EshopCommunity\Internal\Framework\Module\Install\DataObject\OxidEshopPackage; @@ -21,6 +22,8 @@ use OxidEsales\EshopCommunity\Internal\Transition\Utility\BasicContext; use OxidEsales\EshopCommunity\Tests\Integration\IntegrationTestCase; use Psr\Container\ContainerInterface; +use Symfony\Contracts\Cache\TagAwareCacheInterface; +use Symfony\Contracts\Cache\ItemInterface; final class NavigationTreeTest extends IntegrationTestCase { @@ -115,11 +118,52 @@ public function testGetDomXmlWillApplyCorrectLoadOrderAndOverwriteShopsValue(): $this->installModuleFixture('module1'); $this->activateModule('module1'); - $attributeValue = $this->getTestedAttributeValue($this->getNavigationTree() ->getDomXml()); + $attributeValue = $this->getTestedAttributeValue($this->getNavigationTree()->getDomXml()); $this->assertSame(self::EXISTING_XML_ELEMENTS_ATTRIBUTE_VALUE_CHANGED, $attributeValue); } + public function testGetDomXml(): void + { + $utils = Registry::getUtils(); + $language = Registry::getLang(); + $cache = ContainerFacade::get(TagAwareCacheInterface::class); + $navigationTree = $this->getNavigationTree(); + + $templateLanguageCode = $language->getLanguageArray()[$language->getTplLanguage()]->abbr; + $cacheName = 'shop_menu_cache_' . $templateLanguageCode; + + $this->assertEmpty($cache->get($cacheName, function (ItemInterface $item): ?string { + return $item->get(); + })); + $cache->delete($cacheName); + + $navigationTree->getDomXml(); + + $this->assertNotEmpty( + $cache->get( + $cacheName, + fn(ItemInterface $item): array => $item->get() + ) + ); + + $cache->delete($cacheName); + $testXml = 'Test'; + $cache->get($cacheName, function () use ($testXml): array { + return [ + 'creation_time' => time(), + 'menu_dom' => $testXml + ]; + }); + + $navigationTree = $this->getNavigationTree(); + $cachedMenuContent = $navigationTree->getDomXml(); + + $this->assertXmlStringEqualsXmlString($testXml, $cachedMenuContent->saveXML()); + + $cache->delete($cacheName); + } + protected function get(string $class) { if (!$this->container) {