Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cache on search query building #3193

Merged
merged 1 commit into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ private function loadCategory()
*/
private function addVirtualCategoryData(CategoryInterface $category)
{
$originalData = [
'is_virtual_category' => (bool) $category->getOrigData('is_virtual_category'),
'virtual_rule' => "{$category->getOrigData('virtual_rule')}",
'virtual_category_root' => $category->getOrigData('virtual_category_root'),
];

$isVirtualCategory = (bool) $this->getRequest()->getParam('is_virtual_category');
$category->setIsVirtualCategory($isVirtualCategory);

Expand All @@ -145,6 +151,14 @@ private function addVirtualCategoryData(CategoryInterface $category)
$category->setVirtualCategoryRoot($this->getRequest()->getParam('virtual_category_root', null));
}

$newData = [
'is_virtual_category' => $category->getData('is_virtual_category'),
'virtual_rule' => "{$category->getData('virtual_rule')}",
'virtual_category_root' => $category->getData('virtual_category_root'),
];

$category->setData('has_draft_virtual_rule', !empty(array_diff($originalData, $newData)));

return $this;
}

Expand Down
129 changes: 103 additions & 26 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 @@ -136,35 +159,57 @@ public function __toString(): string
}

/**
* Build search query by category.
* Get search query by category from cache or build it.
*
* @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.
*
* @codingStandardsIgnoreStart
* @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.
* @codingStandardsIgnoreEnd
* @SuppressWarnings(PHPMD.StaticAccess)
*
* @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 = $this->buildCategorySearchQuery($category, $excludedCategories);

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 Expand Up @@ -239,6 +284,38 @@ public function mergeCategoryQueries(array $categories)
return $this->queryFactory->create(QueryInterface::TYPE_BOOL, ['must' => $queries]);
}

/**
* Build search query by category.
*
* @param CategoryInterface|int $category Search category.
* @param array $excludedCategories Categories that should not be used into search query building.
*
* @return QueryInterface|null
*/
private function buildCategorySearchQuery($category, $excludedCategories = []): ?QueryInterface
{
$query = null;

if (!is_object($category)) {
$category = $this->categoryFactory->create()->setStoreId($this->getStoreId())->load($category);
}

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);
}
}

return $query;
}

/**
* Load the root category used for a virtual category.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?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.
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*
* @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>
Loading