diff --git a/src/module-elasticsuite-virtual-category/Model/Rule.php b/src/module-elasticsuite-virtual-category/Model/Rule.php index 2fb96dbb4..a29ef5919 100644 --- a/src/module-elasticsuite-virtual-category/Model/Rule.php +++ b/src/module-elasticsuite-virtual-category/Model/Rule.php @@ -14,6 +14,8 @@ namespace Smile\ElasticsuiteVirtualCategory\Model; use Magento\Catalog\Model\Category; +use Magento\Customer\Model\Session; +use Magento\Framework\App\CacheInterface; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Model\StoreManagerInterface; use Smile\ElasticsuiteCatalogRule\Model\Data\ConditionFactory as ConditionDataFactory ; @@ -75,11 +77,26 @@ class Rule extends \Smile\ElasticsuiteCatalogRule\Model\Rule implements VirtualR */ private $storeManager; + /** + * @var Session + */ + private $customerSession; + + /** + * @var CacheInterface + */ + private $sharedCache; + /** * @var Category[] */ protected $instances = []; + /** + * @var array + */ + protected static $localCache = []; + /** * Constructor. * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -96,22 +113,26 @@ class Rule extends \Smile\ElasticsuiteCatalogRule\Model\Rule implements VirtualR * @param CollectionFactory $categoryCollectionFactory Virtual categories collection factory. * @param QueryBuilder $queryBuilder Search rule query builder. * @param StoreManagerInterface $storeManagerInterface Store Manager + * @param Session $customerSession Customer session. + * @param CacheInterface $cache Cache. * @param array $data Additional data. */ public function __construct( Context $context, Registry $registry, - FormFactory $formFactory, - TimezoneInterface $localeDate, + FormFactory $formFactory, + TimezoneInterface $localeDate, CombineConditionFactory $combineConditionsFactory, - ConditionDataFactory $conditionDataFactory, + ConditionDataFactory $conditionDataFactory, ProductConditionFactory $productConditionsFactory, - QueryFactory $queryFactory, - CategoryFactory $categoryFactory, - CollectionFactory $categoryCollectionFactory, - QueryBuilder $queryBuilder, - StoreManagerInterface $storeManagerInterface, - array $data = [] + QueryFactory $queryFactory, + CategoryFactory $categoryFactory, + CollectionFactory $categoryCollectionFactory, + QueryBuilder $queryBuilder, + StoreManagerInterface $storeManagerInterface, + Session $customerSession, + CacheInterface $cache, + array $data = [] ) { $this->queryFactory = $queryFactory; $this->productConditionsFactory = $productConditionsFactory; @@ -119,6 +140,8 @@ public function __construct( $this->categoryCollectionFactory = $categoryCollectionFactory; $this->queryBuilder = $queryBuilder; $this->storeManager = $storeManagerInterface; + $this->customerSession = $customerSession; + $this->sharedCache = $cache; parent::__construct($context, $registry, $formFactory, $localeDate, $combineConditionsFactory, $conditionDataFactory, $data); } @@ -138,33 +161,69 @@ public function __toString(): string /** * Build search query by category. * - * @param CategoryInterface $category Search category. - * @param array $excludedCategories Categories that should not be used into search query building. - * Used to avoid infinite recursion while building virtual categories rules. + * @param CategoryInterface|int $category Search category. + * @param array $excludedCategories Categories that should not be used into search query building. + * Used to avoid infinite recursion while building virtual categories rules. + * + * @TODO: manage cache in this file for getSearchQueriesByChildren, + * remove the \Smile\ElasticsuiteVirtualCategory\Helper\Rule class, + * do not use the $excludedCategories parameters to check if the category rule has been calculated, but use the local cache. * * @return QueryInterface|null */ public function getCategorySearchQuery($category, $excludedCategories = []): ?QueryInterface { - $query = null; - - if (!is_object($category)) { - $category = $this->categoryFactory->create()->setStoreId($this->getStoreId())->load($category); + \Magento\Framework\Profiler::start('ES:Virtual Rule ' . __FUNCTION__); + $categoryId = !is_object($category) ? $category : $category->getId(); + $cacheKey = implode( + '|', + [ + __FUNCTION__, + !is_object($category) ? $this->getStoreId() : $category->getStoreId(), + $categoryId, + $this->customerSession->getCustomerGroupId(), + ] + ); + + $query = self::$localCache[$categoryId] ?? false; + + // If the category is not an object, it can't be in a "draft" mode. + if ($query === false && (!is_object($category) || !$category->getHasDraftVirtualRule())) { + // Due to the fact we serialize/unserialize completely pre-built queries as object. + // We cannot use any implementation of SerializerInterface. + $query = $this->sharedCache->load($cacheKey); + $query = $query ? unserialize($query) : false; } - if (!in_array($category->getId(), $excludedCategories)) { - $excludedCategories[] = $category->getId(); + if ($query === false) { + $query = null; - if ((bool) $category->getIsVirtualCategory() && $category->getIsActive()) { - $query = $this->getVirtualCategoryQuery($category, $excludedCategories); - } elseif ($category->getId() && $category->getIsActive()) { - $query = $this->getStandardCategoryQuery($category, $excludedCategories); + if (!is_object($category)) { + $category = $this->categoryFactory->create()->setStoreId($this->getStoreId())->load($category); } - if ($query && $category->hasChildren()) { - $query = $this->addChildrenQueries($query, $category, $excludedCategories); + + if (!in_array($category->getId(), $excludedCategories)) { + $excludedCategories[] = $category->getId(); + + if ((bool) $category->getIsVirtualCategory() && $category->getIsActive()) { + $query = $this->getVirtualCategoryQuery($category, $excludedCategories); + } elseif ($category->getId() && $category->getIsActive()) { + $query = $this->getStandardCategoryQuery($category, $excludedCategories); + } + if ($query && $category->hasChildren()) { + $query = $this->addChildrenQueries($query, $category, $excludedCategories); + } + } + + if (!$category->getHasDraftVirtualRule()) { + $cacheData = serialize($query); + $this->sharedCache->save($cacheData, $cacheKey, $category->getCacheTags()); } } + self::$localCache[$categoryId] = $query; + \Magento\Framework\Profiler::stop('ES:Virtual Rule ' . __FUNCTION__); + return $query; } diff --git a/src/module-elasticsuite-virtual-category/Plugin/CleanRuleCacheAfterSave.php b/src/module-elasticsuite-virtual-category/Plugin/CleanRuleCacheAfterSave.php new file mode 100644 index 000000000..d1cf667ca --- /dev/null +++ b/src/module-elasticsuite-virtual-category/Plugin/CleanRuleCacheAfterSave.php @@ -0,0 +1,135 @@ + + * @copyright 2024 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteVirtualCategory\Plugin; + +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\ResourceModel\Category as ResourceCategory; +use Magento\Framework\App\CacheInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Model\AbstractModel; +use Smile\ElasticsuiteVirtualCategory\Model\ResourceModel\VirtualCategory\CollectionFactory; + +/** + * Clean rule cache on category save. + * + * @category Smile + * @package Smile\ElasticsuiteVirtualCategory + * @author Pierre Gauthier + */ +class CleanRuleCacheAfterSave +{ + /** + * @var CacheInterface + */ + private $cache; + + /** + * @var CollectionFactory + */ + private $categoryCollectionFactory; + + /** + * Constructor. + * + * @param CacheInterface $cache Cache. + * @param CollectionFactory $categoryCollectionFactory Category collection factory. + */ + public function __construct( + CacheInterface $cache, + CollectionFactory $categoryCollectionFactory + ) { + $this->cache = $cache; + $this->categoryCollectionFactory = $categoryCollectionFactory; + } + + /** + * Clean category search rule cache on category save. + * + * @param ResourceCategory $subject Category resource model. + * @param ResourceCategory $resourceCategory Category resource model return by original method. + * @param AbstractModel $category Saved category. + * @return ResourceCategory + */ + public function afterSave(ResourceCategory $subject, ResourceCategory $resourceCategory, AbstractModel $category) + { + if ($this->hasVirtualDataChange($category)) { + $tagsToClean = $this->getAffectedCategories([$category]); + $this->cache->clean($tagsToClean); + } + return $resourceCategory; + } + + /** + * Check if saved category has some update in its virtual category data. + * + * @param AbstractModel $category Category. + * @return bool + */ + private function hasVirtualDataChange(AbstractModel $category): bool + { + $originalData = [ + 'is_virtual_category' => $category->getOrigData('is_virtual_category'), + 'virtual_rule' => "{$category->getOrigData('virtual_rule')}", + 'virtual_category_root' => $category->getOrigData('virtual_category_root'), + ]; + $newData = [ + 'is_virtual_category' => $category->getData('is_virtual_category'), + 'virtual_rule' => "{$category->getData('virtual_rule')}", + 'virtual_category_root' => $category->getData('virtual_category_root'), + ]; + + return !empty(array_diff($originalData, $newData)); + } + + /** + * Get all category ids affected by the given category rules. + * + * @param array $categories Category list to check. + * @param array $tagList Current calculated tag list to clean. + * @return array + * @throws LocalizedException + */ + private function getAffectedCategories(array $categories, array $tagList = []) + { + $parentCategoryIds = []; + /** @var Category $category */ + foreach ($categories as $category) { + // We need the root and the category of level 1 as they are the root of the website. + $parentCategoryIds = array_merge($parentCategoryIds, array_slice($category->getPathIds(), 2)); + } + + // Add the parent category in the list of the category rule to flush. + foreach ($parentCategoryIds as $categoryId) { + $tagList[$categoryId] = Category::CACHE_TAG . '_' . $categoryId; + } + + // Search for virtual category that use the cleaned category as root category. + $affectedCategories = $this->categoryCollectionFactory + ->create() + ->addAttributeToFilter('level', ['gt' => 1]) + ->addAttributeToFilter('is_virtual_category', ['eq' => true]) + ->addAttributeToFilter('virtual_category_root', ['in' => $parentCategoryIds]) + ->addAttributeToFilter('entity_id', ['nin' => array_keys($tagList)]); + + foreach ($affectedCategories as $category) { + $tagList[$category->getId()] = Category::CACHE_TAG . '_' . $category->getId(); + } + + // If cleaned categories are used as virtual category root, re-run this algo with these categories. + if ($affectedCategories->count()) { + $tagList = $this->getAffectedCategories($affectedCategories->getItems(), $tagList); + } + + return $tagList; + } +} diff --git a/src/module-elasticsuite-virtual-category/etc/di.xml b/src/module-elasticsuite-virtual-category/etc/di.xml index 5b8ea8f26..035839fd0 100644 --- a/src/module-elasticsuite-virtual-category/etc/di.xml +++ b/src/module-elasticsuite-virtual-category/etc/di.xml @@ -227,4 +227,14 @@ Magento\Customer\Model\Session\Proxy + + + Magento\Customer\Model\Session\Proxy + + + + + + +