Skip to content

Commit

Permalink
Add cache on search query building
Browse files Browse the repository at this point in the history
  • Loading branch information
PierreGauthier committed Feb 23, 2024
1 parent 84e3ada commit 7ddedc7
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 24 deletions.
107 changes: 83 additions & 24 deletions src/module-elasticsuite-virtual-category/Model/Rule.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ;
Expand Down Expand Up @@ -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)
Expand All @@ -96,29 +113,35 @@ 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;
$this->categoryFactory = $categoryFactory;
$this->categoryCollectionFactory = $categoryCollectionFactory;
$this->queryBuilder = $queryBuilder;
$this->storeManager = $storeManagerInterface;
$this->customerSession = $customerSession;
$this->sharedCache = $cache;

parent::__construct($context, $registry, $formFactory, $localeDate, $combineConditionsFactory, $conditionDataFactory, $data);
}
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php
/**
* DISCLAIMER
* Do not edit or add to this file if you wish to upgrade Smile Elastic Suite to newer
* versions in the future.
*
* @category Smile
* @package Smile\ElasticsuiteVirtualCategory
* @author Pierre Gauthier <pigau@smile.fr>
* @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 <pigau@smile.fr>
*/
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;
}
}
10 changes: 10 additions & 0 deletions src/module-elasticsuite-virtual-category/etc/di.xml
Original file line number Diff line number Diff line change
Expand Up @@ -227,4 +227,14 @@
<argument name="customerSession" xsi:type="object">Magento\Customer\Model\Session\Proxy</argument>
</arguments>
</type>
<type name="Smile\ElasticsuiteVirtualCategory\Model\Rule">
<arguments>
<argument name="customerSession" xsi:type="object">Magento\Customer\Model\Session\Proxy</argument>
</arguments>
</type>

<!-- Clean rule cache on category save -->
<type name="Magento\Catalog\Model\ResourceModel\Category">
<plugin name="cleanRuleCacheAfterSave" type="Smile\ElasticsuiteVirtualCategory\Plugin\CleanRuleCacheAfterSave"/>
</type>
</config>

0 comments on commit 7ddedc7

Please sign in to comment.