diff --git a/src/module-elasticsuite-catalog-optimizer/Controller/Adminhtml/Optimizer/Save.php b/src/module-elasticsuite-catalog-optimizer/Controller/Adminhtml/Optimizer/Save.php index d207332f3..923243e26 100644 --- a/src/module-elasticsuite-catalog-optimizer/Controller/Adminhtml/Optimizer/Save.php +++ b/src/module-elasticsuite-catalog-optimizer/Controller/Adminhtml/Optimizer/Save.php @@ -13,6 +13,7 @@ namespace Smile\ElasticsuiteCatalogOptimizer\Controller\Adminhtml\Optimizer; +use Smile\ElasticsuiteCatalogOptimizer\Api\Data\OptimizerInterface; use Smile\ElasticsuiteCatalogOptimizer\Controller\Adminhtml\AbstractOptimizer as OptimizerController; /** @@ -36,7 +37,7 @@ public function execute() $redirectBack = $this->getRequest()->getParam('back', false); if ($data) { - $identifier = $this->getRequest()->getParam('id'); + $identifier = $this->getRequest()->getParam(OptimizerInterface::OPTIMIZER_ID); $model = $this->optimizerFactory->create(); if ($identifier) { diff --git a/src/module-elasticsuite-catalog-optimizer/Model/Optimizer.php b/src/module-elasticsuite-catalog-optimizer/Model/Optimizer.php index 5164025bb..4a2ba03f2 100644 --- a/src/module-elasticsuite-catalog-optimizer/Model/Optimizer.php +++ b/src/module-elasticsuite-catalog-optimizer/Model/Optimizer.php @@ -12,6 +12,7 @@ */ namespace Smile\ElasticsuiteCatalogOptimizer\Model; +use Magento\Framework\DataObject\IdentityInterface; use Smile\ElasticsuiteCatalogOptimizer\Api\Data\OptimizerInterface; /** @@ -23,7 +24,7 @@ * @package Smile\ElasticsuiteCatalogOptimizer * @author Fanny DECLERCK */ -class Optimizer extends \Magento\Framework\Model\AbstractModel implements OptimizerInterface +class Optimizer extends \Magento\Framework\Model\AbstractModel implements OptimizerInterface, IdentityInterface { /** * @var string @@ -45,6 +46,12 @@ class Optimizer extends \Magento\Framework\Model\AbstractModel implements Optimi */ private $serializer; + /** + * @var \Smile\ElasticsuiteCatalogOptimizer\Model\Optimizer\Limitation\IdentitiesFactory + */ + private $limitationIdentitiesFactory; + + /** /** * @var string */ @@ -53,14 +60,15 @@ class Optimizer extends \Magento\Framework\Model\AbstractModel implements Optimi /** * Class constructor * - * @param \Magento\Framework\Model\Context $context Context. - * @param \Magento\Framework\Registry $registry Registry. - * @param \Smile\ElasticsuiteCatalogRule\Model\RuleFactory $ruleFactory Rule factory. - * @param \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter Date Filter. - * @param \Magento\Framework\Serialize\SerializerInterface $serializer Serializer. - * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource Resource. - * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection Resource collection. - * @param array $data Data. + * @param \Magento\Framework\Model\Context $context Context. + * @param \Magento\Framework\Registry $registry Registry. + * @param \Smile\ElasticsuiteCatalogRule\Model\RuleFactory $ruleFactory Rule factory. + * @param \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter Date Filter. + * @param \Magento\Framework\Serialize\SerializerInterface $serializer Serializer. + * @param Optimizer\Limitation\IdentitiesFactory $limitationIdentitiesFactory Limitation Identities. + * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource Resource. + * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection Resource collection. + * @param array $data Data. */ public function __construct( \Magento\Framework\Model\Context $context, @@ -68,14 +76,16 @@ public function __construct( \Smile\ElasticsuiteCatalogRule\Model\RuleFactory $ruleFactory, \Magento\Framework\Stdlib\DateTime\Filter\Date $dateFilter, \Magento\Framework\Serialize\SerializerInterface $serializer, + Optimizer\Limitation\IdentitiesFactory $limitationIdentitiesFactory, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [] ) { parent::__construct($context, $registry, $resource, $resourceCollection, $data); - $this->ruleFactory = $ruleFactory; - $this->dateFilter = $dateFilter; - $this->serializer = $serializer; + $this->ruleFactory = $ruleFactory; + $this->dateFilter = $dateFilter; + $this->serializer = $serializer; + $this->limitationIdentitiesFactory = $limitationIdentitiesFactory; } /** @@ -304,6 +314,17 @@ public function validateData(\Magento\Framework\DataObject $dataObject) return !empty($result) ? $result : true; } + /** + * {@inheritdoc} + */ + public function getIdentities() + { + $limitationIdentities = $this->limitationIdentitiesFactory->create(['optimizer' => $this]); + $identities = array_merge($this->getCacheTags(), $limitationIdentities->get()); + + return $identities; + } + /** * @SuppressWarnings(PHPMD.CamelCaseMethodName) * diff --git a/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/Limitation/Identities.php b/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/Limitation/Identities.php new file mode 100644 index 000000000..c3f59dd0d --- /dev/null +++ b/src/module-elasticsuite-catalog-optimizer/Model/Optimizer/Limitation/Identities.php @@ -0,0 +1,194 @@ + + * @copyright 2018 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteCatalogOptimizer\Model\Optimizer\Limitation; + +use Magento\Search\Model\PopularSearchTerms; +use Smile\ElasticsuiteCatalogOptimizer\Api\Data\OptimizerInterface; + +/** + * Identities Provider for optimizer limitations. + * + * @category Smile + * @package Smile\ElasticsuiteCatalogOptimizer + * @author Romain Ruaud + */ +class Identities +{ + /** + * Scope configuration + * + * @var \Magento\Framework\App\Config\ScopeConfigInterface + */ + private $scopeConfig; + + /** + * Catalog search data + * + * @var \Magento\Search\Model\ResourceModel\Query\Collection + */ + private $queryCollection; + + /** + * @var OptimizerInterface + */ + private $optimizer; + + /** + * Limitation Identities Constructor. + * + * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig Scope Config + * @param \Magento\Search\Model\ResourceModel\Query\Collection $queryCollection Search Queries Collection + * @param \Smile\ElasticsuiteCatalogOptimizer\Api\Data\OptimizerInterface $optimizer The Optimizer + */ + public function __construct( + \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, + \Magento\Search\Model\ResourceModel\Query\Collection $queryCollection, + OptimizerInterface $optimizer + ) { + $this->scopeConfig = $scopeConfig; + $this->queryCollection = $queryCollection; + $this->optimizer = $optimizer; + } + + /** + * Get Limitation identities for the current optimizer. + * + * @return array + */ + public function get() + { + $identities = []; + $origData = $this->optimizer->getOrigData(); + $containers = $this->optimizer->getData('search_container') ?? []; + + if (!$this->optimizer->isObjectNew()) { + $containers = array_unique( + array_keys(array_merge($this->optimizer->getSearchContainers(), $origData['search_containers'] ?? [])) + ); + } + + if (in_array('quick_search_container', $containers)) { + $identities = array_merge($identities, $this->getSearchQueryIdentities()); + } + + if (in_array('catalog_view_container', $containers)) { + $identities = array_merge($identities, $this->getCategoryIdentities()); + } + + return $identities; + } + + /** + * Get search queries identities related to current optimizer. + * + * @return array + */ + private function getSearchQueryIdentities() + { + $identities = []; + $queryIds = []; + $origData = $this->optimizer->getOrigData(); + $data = $this->optimizer->getData(); + + // If optimizer was previously assigned to all queries, or is now set to all queries. + $isAppliedToAllQueries = empty($data['quick_search_container']) + || (bool) $data['quick_search_container']['apply_to'] === false; + $wasAppliedToAllQueries = empty($origData['quick_search_container']['query_ids']); + + if (!empty($origData['quick_search_container']['query_ids'])) { + $queryIds = array_merge($queryIds, $origData['quick_search_container']['query_ids']); + } + + if (!empty($data['quick_search_container']['query_ids'])) { + foreach ($data['quick_search_container']['query_ids'] as $query) { + $queryIds[] = $query['id']; + } + } + + $queryIds = array_unique(array_filter($queryIds)); + + if ($wasAppliedToAllQueries || $isAppliedToAllQueries) { + $identities[] = \Smile\ElasticsuiteCatalog\Block\CatalogSearch\Result\Cache::POPULAR_SEARCH_CACHE_TAG; + } elseif (!empty($queryIds)) { + $popularQueryIds = $this->queryCollection + ->setPopularQueryFilter($this->optimizer->getStoreId()) + ->setPageSize($this->getMaxCountCacheableSearchTerms($this->optimizer->getStoreId())) + ->load() + ->getColumnValues('query_id'); + + if (!empty(array_intersect($queryIds, $popularQueryIds))) { + $identities[] = \Smile\ElasticsuiteCatalog\Block\CatalogSearch\Result\Cache::POPULAR_SEARCH_CACHE_TAG; + } + } + + return $identities; + } + + /** + * Get category identities related to current optimizer. + * + * @return array + */ + private function getCategoryIdentities() + { + $identities = []; + $categoryIds = []; + $origData = $this->optimizer->getOrigData(); + $data = $this->optimizer->getData(); + + // If optimizer was previously assigned to all categories, or is now set to all categories. + $isAppliedToAllCategories = empty($data['catalog_view_container']) + || (bool) $data['catalog_view_container']['apply_to'] === false; + + $wasAppliedToAllCategories = empty($origData['catalog_view_container']['category_ids']); + + if ($isAppliedToAllCategories || $wasAppliedToAllCategories) { + $identities[] = \Magento\Catalog\Model\Category::CACHE_TAG; + } + + if (!empty($data['catalog_view_container']['category_ids'])) { + $categoryIds = array_merge($categoryIds, $data['catalog_view_container']['category_ids']); + } + + if (!empty($origData['catalog_view_container']['category_ids'])) { + $categoryIds = array_merge($categoryIds, $origData['catalog_view_container']['category_ids']); + } + + $categoryIds = array_filter(array_unique($categoryIds)); + if (!empty($categoryIds)) { + $categoryTags = array_map(function ($categoryId) { + return \Magento\Catalog\Model\Category::CACHE_TAG . '_' . $categoryId; + }, $categoryIds); + + $identities = array_merge($identities, $categoryTags); + } + + return $identities; + } + + /** + * Retrieve maximum count cacheable search terms by Store. + * + * @param int $storeId Store Id + * + * @return int + */ + private function getMaxCountCacheableSearchTerms(int $storeId) + { + return $this->scopeConfig->getValue( + PopularSearchTerms::XML_PATH_MAX_COUNT_CACHEABLE_SEARCH_TERMS, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $storeId + ); + } +} diff --git a/src/module-elasticsuite-catalog/Block/CatalogSearch/Result/Cache.php b/src/module-elasticsuite-catalog/Block/CatalogSearch/Result/Cache.php new file mode 100644 index 000000000..3de3428a4 --- /dev/null +++ b/src/module-elasticsuite-catalog/Block/CatalogSearch/Result/Cache.php @@ -0,0 +1,83 @@ + + * @copyright 2018 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteCatalog\Block\CatalogSearch\Result; + +use Magento\Framework\DataObject\IdentityInterface; +use Magento\Framework\View\Element\AbstractBlock; + +/** + * Block to handle search results cache tags. + * + * @category Smile + * @package Smile\ElasticsuiteCatalog + * @author Romain Ruaud + */ +class Cache extends AbstractBlock implements IdentityInterface +{ + /** + * Cache tag that will be applied to popular search results (they are cached by Magento). + */ + const POPULAR_SEARCH_CACHE_TAG = 'es_pop'; // Short name style like Magento does (cat_c, cat_p, etc...). + + /** + * @var \Magento\Framework\App\ResponseInterface + */ + private $response; + + /** + * Cache constructor. + * + * @param \Magento\Framework\View\Element\Context $context Block Context + * @param \Magento\Framework\App\ResponseInterface $response HTTP Response + * @param array $data Data + */ + public function __construct( + \Magento\Framework\View\Element\Context $context, + \Magento\Framework\App\ResponseInterface $response, + array $data = [] + ) { + $this->response = $response; + parent::__construct($context, $data); + } + + /** + * {@inheritdoc} + */ + public function getIdentities() + { + $identities = []; + + if ($this->isPageCacheable()) { + $identities[] = self::POPULAR_SEARCH_CACHE_TAG; + } + + return $identities; + } + + /** + * Check if current page is cacheable + * + * @return bool + */ + public function isPageCacheable() + { + $result = false; + $pragma = $this->response->getHeader('pragma'); + + if ($pragma) { + $result = $pragma->getFieldValue() === 'cache'; + } + + return $result; + } +} diff --git a/src/module-elasticsuite-catalog/Controller/Adminhtml/Term/Merchandiser/Save.php b/src/module-elasticsuite-catalog/Controller/Adminhtml/Term/Merchandiser/Save.php index 2a879c19d..bb47d9972 100644 --- a/src/module-elasticsuite-catalog/Controller/Adminhtml/Term/Merchandiser/Save.php +++ b/src/module-elasticsuite-catalog/Controller/Adminhtml/Term/Merchandiser/Save.php @@ -29,24 +29,23 @@ class Save extends \Magento\Search\Controller\Adminhtml\Term private $jsonHelper; /** - * @var \Smile\ElasticsuiteCatalog\Model\ResourceModel\Product\Search\Position + * @var \Smile\ElasticsuiteCatalog\Model\Product\Search\Position */ - private $resourceModel; + private $positionModel; /** - * - * @param \Magento\Backend\App\Action\Context $context Context. - * @param \Magento\Framework\Json\Helper\Data $jsonHelper JSON helper. - * @param \Smile\ElasticsuiteCatalog\Model\ResourceModel\Product\Search\Position $resourceModel Resource model. + * @param \Magento\Backend\App\Action\Context $context Context. + * @param \Magento\Framework\Json\Helper\Data $jsonHelper JSON helper. + * @param \Smile\ElasticsuiteCatalog\Model\Product\Search\Position $positionModel Position model. */ public function __construct( \Magento\Backend\App\Action\Context $context, \Magento\Framework\Json\Helper\Data $jsonHelper, - \Smile\ElasticsuiteCatalog\Model\ResourceModel\Product\Search\Position $resourceModel + \Smile\ElasticsuiteCatalog\Model\Product\Search\Position $positionModel ) { parent::__construct($context); - $this->jsonHelper = $jsonHelper; - $this->resourceModel = $resourceModel; + $this->jsonHelper = $jsonHelper; + $this->positionModel = $positionModel; } /** @@ -70,7 +69,7 @@ public function execute() ->setPath('*/term/edit', ['id' => $queryId]); try { - $this->resourceModel->saveProductPositions($queryId, $sortedProducts, $blacklistedProducts); + $this->positionModel->saveProductPositions($queryId, $sortedProducts, $blacklistedProducts); if ($this->getRequest()->getParam('back') == "edit") { $result->setPath('*/*/edit', ['id' => $queryId]); diff --git a/src/module-elasticsuite-catalog/Model/Product/Search/Position.php b/src/module-elasticsuite-catalog/Model/Product/Search/Position.php new file mode 100644 index 000000000..69ddb0f65 --- /dev/null +++ b/src/module-elasticsuite-catalog/Model/Product/Search/Position.php @@ -0,0 +1,143 @@ + + * @copyright 2018 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteCatalog\Model\Product\Search; + +use Magento\Search\Model\PopularSearchTerms; + +/** + * Model that handle custom product position for a given search term. + * + * @category Smile + * @package Smile\ElasticsuiteCatalog + * @author Romain Ruaud + */ +class Position +{ + /** + * @var \Smile\ElasticsuiteCatalog\Model\ResourceModel\Product\Search\Position + */ + private $resourceModel; + + /** + * @var \Magento\Store\Model\StoreManagerInterface + */ + private $storeManager; + + /** + * @var \Magento\Framework\App\Cache\Type\FrontendPool + */ + private $frontendCachePool; + + /** + * @var \Magento\Framework\App\Config\ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var \Magento\Search\Model\ResourceModel\Query\Collection + */ + private $queryCollection; + + /** + * Position constructor. + * + * @param \Smile\ElasticsuiteCatalog\Model\ResourceModel\Product\Search\Position $resourceModel Resource + * @param \Magento\Store\Model\StoreManagerInterface $storeManagerInterface Store Manager + * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig Scope Config + * @param \Magento\Search\Model\ResourceModel\Query\Collection $queryCollection Query Collection + * @param \Magento\Framework\App\Cache\Type\FrontendPool $frontendCachePool Frontend Cache + */ + public function __construct( + \Smile\ElasticsuiteCatalog\Model\ResourceModel\Product\Search\Position $resourceModel, + \Magento\Store\Model\StoreManagerInterface $storeManagerInterface, + \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, + \Magento\Search\Model\ResourceModel\Query\Collection $queryCollection, + \Magento\Framework\App\Cache\Type\FrontendPool $frontendCachePool + ) { + $this->resourceModel = $resourceModel; + $this->storeManager = $storeManagerInterface; + $this->frontendCachePool = $frontendCachePool; + $this->scopeConfig = $scopeConfig; + $this->queryCollection = $queryCollection; + } + + /** + * Save the product positions. + * + * @param int $queryId Query id. + * @param array $newProductPositions Product positions. + * @param array $blacklistedProducts Blacklisted product ids. + * + * @return \Smile\ElasticsuiteCatalog\Model\ResourceModel\Product\Search\Position + */ + public function saveProductPositions($queryId, $newProductPositions, $blacklistedProducts = []) + { + $this->resourceModel->saveProductPositions($queryId, $newProductPositions, $blacklistedProducts); + + foreach ($this->storeManager->getStores() as $store) { + if ($this->isPopularQuery($queryId, $store->getId())) { + $this->cleanPopularSearchCache(); + } + } + } + + /** + * Check if a query Id is among the popular query list for a given store. + * + * @param int $queryId The Query Id + * @param int $storeId The Store Id + * + * @return bool + */ + private function isPopularQuery($queryId, $storeId) + { + $popularQueryIds = $this->queryCollection + ->setPopularQueryFilter($storeId) + ->setPageSize($this->getMaxCountCacheableSearchTerms($storeId)) + ->load() + ->getColumnValues('query_id'); + + return count(array_intersect([$queryId], $popularQueryIds)) > 0 ; + } + + /** + * Cleanup popular searches cache tag. + */ + private function cleanPopularSearchCache() + { + try { + $this->frontendCachePool->get(\Magento\PageCache\Model\Cache\Type::TYPE_IDENTIFIER)->clean( + \Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, + [\Smile\ElasticsuiteCatalog\Block\CatalogSearch\Result\Cache::POPULAR_SEARCH_CACHE_TAG] + ); + } catch (\InvalidArgumentException $exception) { + ; + } + } + + /** + * Retrieve maximum count cacheable search terms by Store. + * + * @param int $storeId Store Id + * + * @return int + */ + private function getMaxCountCacheableSearchTerms(int $storeId) + { + return $this->scopeConfig->getValue( + PopularSearchTerms::XML_PATH_MAX_COUNT_CACHEABLE_SEARCH_TERMS, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE, + $storeId + ); + } +} diff --git a/src/module-elasticsuite-catalog/Plugin/CatalogSearch/Indexer/FulltextPlugin.php b/src/module-elasticsuite-catalog/Plugin/CatalogSearch/Indexer/FulltextPlugin.php new file mode 100644 index 000000000..c9a6e7d4f --- /dev/null +++ b/src/module-elasticsuite-catalog/Plugin/CatalogSearch/Indexer/FulltextPlugin.php @@ -0,0 +1,60 @@ + + * @copyright 2018 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteCatalog\Plugin\CatalogSearch\Indexer; + +/** + * Plugin that will cleanup popular searches cache after a full reindex. + * + * @category Smile + * @package Smile\ElasticsuiteCatalog + * @author Romain Ruaud + */ +class FulltextPlugin +{ + /** + * @var \Magento\Framework\App\Cache\Type\FrontendPool + */ + private $frontendCachePool; + + /** + * FulltextPlugin constructor. + * + * @param \Magento\Framework\App\Cache\Type\FrontendPool $frontendCachePool Frontend Cache Pool + */ + public function __construct(\Magento\Framework\App\Cache\Type\FrontendPool $frontendCachePool) + { + $this->frontendCachePool = $frontendCachePool; + } + + /** + * After a full reindex of catalogsearch_fulltext index : + * - cleanup the cache for items matching the popular search results tag. + * + * @param \Magento\CatalogSearch\Model\Indexer\Fulltext $subject Catalog product fulltext indexer + * @param void $result Void result + * + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExecuteFull(\Magento\CatalogSearch\Model\Indexer\Fulltext $subject, $result) + { + try { + $this->frontendCachePool->get(\Magento\PageCache\Model\Cache\Type::TYPE_IDENTIFIER)->clean( + \Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, + [\Smile\ElasticsuiteCatalog\Block\CatalogSearch\Result\Cache::POPULAR_SEARCH_CACHE_TAG] + ); + } catch (\InvalidArgumentException $exception) { + ; + } + } +} diff --git a/src/module-elasticsuite-catalog/etc/di.xml b/src/module-elasticsuite-catalog/etc/di.xml index 8bf5cd8dc..5803476b0 100644 --- a/src/module-elasticsuite-catalog/etc/di.xml +++ b/src/module-elasticsuite-catalog/etc/di.xml @@ -223,4 +223,12 @@ + + + + + diff --git a/src/module-elasticsuite-catalog/view/frontend/layout/catalogsearch_result_index.xml b/src/module-elasticsuite-catalog/view/frontend/layout/catalogsearch_result_index.xml index 5a4fea7ad..7bb1ba562 100644 --- a/src/module-elasticsuite-catalog/view/frontend/layout/catalogsearch_result_index.xml +++ b/src/module-elasticsuite-catalog/view/frontend/layout/catalogsearch_result_index.xml @@ -41,5 +41,12 @@ template="Smile_ElasticsuiteCatalog::layer/filter/slider.phtml" /> + + + + diff --git a/src/module-elasticsuite-virtual-category/Plugin/CatalogSearch/Indexer/FulltextPlugin.php b/src/module-elasticsuite-virtual-category/Plugin/CatalogSearch/Indexer/FulltextPlugin.php new file mode 100644 index 000000000..430287b05 --- /dev/null +++ b/src/module-elasticsuite-virtual-category/Plugin/CatalogSearch/Indexer/FulltextPlugin.php @@ -0,0 +1,108 @@ + + * @copyright 2018 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteVirtualCategory\Plugin\CatalogSearch\Indexer; + +/** + * Plugin that will cleanup virtual categories cache after a full reindex. + * + * @category Smile + * @package Smile\ElasticsuiteVirtualCategory + * @author Romain Ruaud + */ +class FulltextPlugin +{ + /** + * @var \Magento\Framework\Event\ManagerInterface + */ + private $eventManager; + + /** + * @var \Magento\Framework\Indexer\CacheContext + */ + private $cacheContext; + + /** + * @var \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory + */ + private $categoryCollectionFactory; + + /** + * FulltextPlugin constructor. + * + * @param \Magento\Framework\Event\ManagerInterface $eventManager Event Manager + * @param \Magento\Framework\Indexer\CacheContext $cacheContext Cache Context + * @param \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollectionFactory Category Collection Factory + * + * @internal param \Magento\Framework\App\Cache\Type\FrontendPool $frontendCachePool Frontend Cache Pool + */ + public function __construct( + \Magento\Framework\Event\ManagerInterface $eventManager, + \Magento\Framework\Indexer\CacheContext $cacheContext, + \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $categoryCollectionFactory + ) { + $this->eventManager = $eventManager; + $this->cacheContext = $cacheContext; + $this->categoryCollectionFactory = $categoryCollectionFactory; + } + + /** + * After a full reindex of catalogsearch_fulltext index : + * - cleanup the cache tag of each virtual category and their parents. + * + * @param \Magento\CatalogSearch\Model\Indexer\Fulltext $subject Catalog product fulltext indexer + * @param void $result Void result + * + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExecuteFull(\Magento\CatalogSearch\Model\Indexer\Fulltext $subject, $result) + { + /** @var \Magento\Catalog\Model\ResourceModel\Category\Collection $categories */ + $categories = $this->categoryCollectionFactory->create(); + + // Can occur during setup:install. + if (false === $categories->getEntity()->getAttribute('is_virtual_category')) { + return; + } + + $categories->addAttributeToSelect(['is_virtual_category']) + ->addIsActiveFilter() + ->addAttributeToFilter('is_virtual_category', 1); + + $categoryIds = []; + + // Foreach virtual Category, compute list of it's ancestors to purge their cache. + foreach ($categories as $category) { + $categoryIds = array_merge( + $categoryIds, + [$category->getId()], + array_slice($category->getPathIds(), 2) // Exclude default root (1) and Store Root. + ); + } + + $categoryIds = array_unique($categoryIds); + + $this->cleanCategoryCacheByIds($categoryIds); + } + + /** + * Clean cache of frontend pages based on category ids. + * + * @param array $categoryIds Category Ids + */ + private function cleanCategoryCacheByIds(array $categoryIds) + { + $this->cacheContext->registerEntities(\Magento\Catalog\Model\Category::CACHE_TAG, $categoryIds); + $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]); + } +} diff --git a/src/module-elasticsuite-virtual-category/etc/di.xml b/src/module-elasticsuite-virtual-category/etc/di.xml index 770e7b295..3c356622c 100644 --- a/src/module-elasticsuite-virtual-category/etc/di.xml +++ b/src/module-elasticsuite-virtual-category/etc/di.xml @@ -97,4 +97,12 @@ + + + + +