Skip to content

Commit d667852

Browse files
authored
Merge pull request #1764 from algolia/feat/MAGE-1083-collection-size-handling
MAGE-1083 Collection size caching
2 parents 9b2f8d0 + 68e2d41 commit d667852

File tree

13 files changed

+700
-151
lines changed

13 files changed

+700
-151
lines changed

Console/Command/Indexer/IndexProductsCommand.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
namespace Algolia\AlgoliaSearch\Console\Command\Indexer;
44

5+
use Algolia\AlgoliaSearch\Api\Processor\BatchQueueProcessorInterface;
56
use Algolia\AlgoliaSearch\Service\StoreNameFetcher;
6-
use Algolia\AlgoliaSearch\Service\Product\BatchQueueProcessor as ProductBatchQueueProcessor;
77
use Magento\Framework\App\State;
88
use Magento\Framework\Console\Cli;
99
use Magento\Store\Model\StoreManagerInterface;
@@ -13,7 +13,7 @@
1313
class IndexProductsCommand extends AbstractIndexerCommand
1414
{
1515
public function __construct(
16-
protected ProductBatchQueueProcessor $productBatchQueueProcessor,
16+
protected BatchQueueProcessorInterface $productBatchQueueProcessor,
1717
protected StoreManagerInterface $storeManager,
1818
State $state,
1919
StoreNameFetcher $storeNameFetcher,

Helper/Entity/Product/CacheHelper.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace Algolia\AlgoliaSearch\Helper\Entity\Product;
4+
5+
use Algolia\AlgoliaSearch\Logger\AlgoliaLogger;
6+
use Algolia\AlgoliaSearch\Model\Cache\Product\IndexCollectionSize as Cache;
7+
8+
class CacheHelper
9+
{
10+
const ATTRIBUTES_TO_OBSERVE = ['status', 'visibility'];
11+
12+
public function __construct(
13+
protected Cache $cache,
14+
protected AlgoliaLogger $logger
15+
) {}
16+
17+
public function handleBulkAttributeChange(array $productIds, array $attributes, int $storeId)
18+
{
19+
if ($productIds
20+
&& array_intersect(array_keys($attributes), self::ATTRIBUTES_TO_OBSERVE)) {
21+
$this->logger->info(sprintf("Clearing product index collection cache on store ID %d for attributes: %s", $storeId, join(',', array_keys($attributes))));
22+
$this->cache->clear($storeId ?: null);
23+
}
24+
}
25+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
namespace Algolia\AlgoliaSearch\Model\Cache\Product;
4+
5+
use Algolia\AlgoliaSearch\Model\Cache\Type\Indexer;
6+
use Magento\Framework\App\Cache\StateInterface;
7+
use Magento\Framework\App\Cache\TypeListInterface;
8+
use Magento\Framework\App\CacheInterface;
9+
10+
class IndexCollectionSize
11+
{
12+
const NOT_FOUND = -1;
13+
14+
public function __construct(
15+
protected CacheInterface $cache,
16+
protected StateInterface $state,
17+
protected TypeListInterface $typeList
18+
) {}
19+
20+
public function get(int $storeId): int
21+
{
22+
if (!$this->isCacheAvailable()) {
23+
return self::NOT_FOUND;
24+
}
25+
26+
/** @var string|false $data */
27+
$data = $this->cache->load($this->getCacheKey($storeId));
28+
if ($data === false) {
29+
return self::NOT_FOUND;
30+
}
31+
32+
return (int) $data;
33+
}
34+
35+
public function set(int $storeId, int $value, ?int $ttl = null): void
36+
{
37+
if ($this->isCacheAvailable()) {
38+
$this->cache->save($value, $this->getCacheKey($storeId), [Indexer::CACHE_TAG], $ttl);
39+
}
40+
}
41+
42+
protected function remove(int $storeId): void
43+
{
44+
$this->cache->remove($this->getCacheKey($storeId));
45+
}
46+
47+
public function isCacheAvailable(): bool
48+
{
49+
return $this->state->isEnabled(Indexer::TYPE_IDENTIFIER)
50+
&& !array_key_exists(Indexer::TYPE_IDENTIFIER, $this->typeList->getInvalidated());
51+
}
52+
53+
protected function getCacheKey(int $storeId): string
54+
{
55+
return sprintf('%s_%s_%d', Indexer::TYPE_IDENTIFIER, 'product', $storeId);
56+
}
57+
58+
public function clear(?int $storeId = null): void
59+
{
60+
if (is_null($storeId)) {
61+
$this->typeList->invalidate(Indexer::TYPE_IDENTIFIER);
62+
$this->typeList->cleanType(Indexer::TYPE_IDENTIFIER);
63+
}
64+
else {
65+
$this->remove($storeId);
66+
}
67+
}
68+
}

Model/Cache/Type/Indexer.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Algolia\AlgoliaSearch\Model\Cache\Type;
4+
5+
use Magento\Framework\App\Cache\Type\FrontendPool;
6+
use Magento\Framework\Cache\Frontend\Decorator\TagScope;
7+
8+
class Indexer extends TagScope {
9+
const TYPE_IDENTIFIER = 'algolia_indexer';
10+
const CACHE_TAG = 'ALGOLIA_INDEXER';
11+
12+
public function __construct(FrontendPool $cacheFrontendPool)
13+
{
14+
parent::__construct(
15+
$cacheFrontendPool->get(self::TYPE_IDENTIFIER),
16+
self::CACHE_TAG
17+
);
18+
}
19+
}

Model/Indexer/Product.php

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22

33
namespace Algolia\AlgoliaSearch\Model\Indexer;
44

5+
use Algolia\AlgoliaSearch\Api\Processor\BatchQueueProcessorInterface;
56
use Algolia\AlgoliaSearch\Helper\ConfigHelper;
6-
use Algolia\AlgoliaSearch\Service\Product\BatchQueueProcessor as ProductBatchQueueProcessor;
7-
use Magento\Framework\Exception\NoSuchEntityException;
87
use Magento\Store\Model\StoreManagerInterface;
98

109
/**
@@ -16,34 +15,43 @@ class Product implements \Magento\Framework\Indexer\ActionInterface, \Magento\Fr
1615
public function __construct(
1716
protected StoreManagerInterface $storeManager,
1817
protected ConfigHelper $configHelper,
19-
protected ProductBatchQueueProcessor $productBatchQueueProcessor
18+
protected BatchQueueProcessorInterface $productBatchQueueProcessor
2019
) {}
2120

2221
/**
23-
* @throws NoSuchEntityException
22+
* {@inheritdoc}
2423
*/
25-
public function execute($productIds)
24+
public function execute($ids): void
2625
{
2726
foreach (array_keys($this->storeManager->getStores()) as $storeId) {
28-
$this->productBatchQueueProcessor->processBatch($storeId, $productIds);
27+
$this->productBatchQueueProcessor->processBatch($storeId, $ids);
2928
}
3029
}
3130

32-
public function executeFull()
31+
/**
32+
* {@inheritdoc}
33+
*/
34+
public function executeFull(): void
3335
{
3436
if (!$this->configHelper->isProductsIndexerEnabled()) {
3537
return;
3638
}
3739

38-
$this->execute(null);
40+
$this->execute([]);
3941
}
4042

41-
public function executeList(array $ids)
43+
/**
44+
* {@inheritdoc}
45+
*/
46+
public function executeList(array $ids): void
4247
{
4348
$this->execute($ids);
4449
}
4550

46-
public function executeRow($id)
51+
/**
52+
* {@inheritdoc}
53+
*/
54+
public function executeRow($id): void
4755
{
4856
$this->execute([$id]);
4957
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace Algolia\AlgoliaSearch\Plugin\Cache;
4+
5+
use Algolia\AlgoliaSearch\Helper\Entity\Product\CacheHelper;
6+
use Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save;
7+
use Magento\Catalog\Helper\Product\Edit\Action\Attribute;
8+
use Magento\Framework\Controller\Result\Redirect;
9+
10+
class CacheCleanBulkAttributePlugin
11+
{
12+
public function __construct(
13+
protected Attribute $attributeHelper,
14+
protected CacheHelper $cacheHelper
15+
) {}
16+
17+
/** In the event that the product_action_attribute.update consumer does not handle this change and update occurs in process
18+
* then this plugin will preemptively clear the cache
19+
*/
20+
public function afterExecute(
21+
Save $subject,
22+
Redirect $result
23+
): Redirect
24+
{
25+
$this->cacheHelper->handleBulkAttributeChange(
26+
$this->attributeHelper->getProductIds(),
27+
$subject->getRequest()->getParam('attributes', []),
28+
$this->attributeHelper->getSelectedStoreId()
29+
);
30+
31+
return $result;
32+
}
33+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<?php
2+
3+
namespace Algolia\AlgoliaSearch\Plugin\Cache;
4+
5+
use Algolia\AlgoliaSearch\Helper\ConfigHelper;
6+
use Algolia\AlgoliaSearch\Helper\Entity\Product\CacheHelper;
7+
use Algolia\AlgoliaSearch\Model\Cache\Product\IndexCollectionSize as Cache;
8+
use Magento\Catalog\Model\Product;
9+
use Magento\Catalog\Model\Product\Action;
10+
use Magento\Catalog\Model\Product\Attribute\Source\Status;
11+
use Magento\Catalog\Model\Product\Visibility;
12+
use Magento\Catalog\Model\ResourceModel\Product as ProductResource;
13+
14+
class CacheCleanProductPlugin
15+
{
16+
protected array $originalData = [];
17+
18+
public function __construct(
19+
protected Cache $cache,
20+
protected ConfigHelper $configHelper,
21+
protected CacheHelper $cacheHelper
22+
) { }
23+
24+
public function beforeSave(ProductResource $subject, Product $product): void
25+
{
26+
$this->originalData[$product->getSku()] = $product->getOrigData();
27+
}
28+
29+
public function afterSave(ProductResource $subject, ProductResource $result, Product $product): ProductResource
30+
{
31+
$original = $this->originalData[$product->getSku()] ?? [];
32+
$storeId = $product->getStoreId();
33+
34+
$shouldClearCache =
35+
$this->isEligibleNewProduct($product)
36+
|| $this->hasEnablementChanged($original, $product->getData())
37+
|| $this->hasVisibilityChanged($original, $product->getData(), $storeId)
38+
|| $this->hasStockChanged($original, $product->getData(), $storeId);
39+
40+
if ($shouldClearCache) {
41+
$this->cache->clear($storeId ?: null);
42+
}
43+
44+
return $result;
45+
}
46+
47+
public function afterDelete(ProductResource $subject, ProductResource $result): ProductResource
48+
{
49+
$this->cache->clear();
50+
return $result;
51+
}
52+
53+
/**
54+
* Called on mass action "Change Status"
55+
* Called on "Update attributes" if `product_action_attribute.update` consumer is running
56+
*/
57+
public function afterUpdateAttributes(Action $subject, Action $result, array $productIds, array $attributes, int $storeId): Action
58+
{
59+
$this->cacheHelper->handleBulkAttributeChange($productIds, $attributes, $storeId);
60+
return $result;
61+
}
62+
63+
protected function isEligibleNewProduct(Product $product): bool
64+
{
65+
$storeId = $product->getStoreId();
66+
return $product->isObjectNew()
67+
&& $product->getStatus() === Status::STATUS_ENABLED
68+
&& $this->configHelper->includeNonVisibleProductsInIndex($storeId)
69+
|| $product->isVisibleInSiteVisibility()
70+
&& $this->configHelper->getShowOutOfStock($storeId)
71+
|| $product->isInStock();
72+
}
73+
74+
protected function hasEnablementChanged(array $orig, array $new): bool
75+
{
76+
$key = 'status';
77+
return $orig[$key] !== $new[$key];
78+
}
79+
80+
protected function hasVisibilityChanged(array $orig, array $new, int $storeId = null): bool
81+
{
82+
if ($this->configHelper->includeNonVisibleProductsInIndex($storeId)) {
83+
return false;
84+
}
85+
86+
$key = 'visibility';
87+
return $this->isVisible($orig[$key]) !== $this->isVisible($new[$key]);
88+
}
89+
90+
/**
91+
* Do not rely on this data point only
92+
* TODO revaluate with MSI support
93+
*/
94+
protected function hasStockChanged(array $orig, array $new, int $storeId): bool
95+
{
96+
if ($this->configHelper->getShowOutOfStock($storeId)) {
97+
return false;
98+
}
99+
100+
$key = 'quantity_and_stock_status';
101+
$oldStock = $orig[$key];
102+
$newStock = $new[$key];
103+
return $this->canCompareValues($oldStock, $newStock, 'is_in_stock')
104+
&& (bool) $oldStock['is_in_stock'] !== (bool) $newStock['is_in_stock']
105+
|| $this->canCompareValues($oldStock, $newStock, 'qty')
106+
&& $this->hasStock($oldStock['qty']) !== $this->hasStock($newStock['qty']);
107+
}
108+
109+
protected function canCompareValues(array $orig, array $new, string $key): bool
110+
{
111+
return array_key_exists($key, $orig) && array_key_exists($key, $new);
112+
}
113+
114+
protected function isVisible(int $visibility): bool
115+
{
116+
return $visibility !== Visibility::VISIBILITY_NOT_VISIBLE;
117+
}
118+
119+
/*
120+
* Reduce numeric to comparable boolean
121+
*/
122+
protected function hasStock(int $qty): bool
123+
{
124+
return $qty > 0;
125+
}
126+
}

0 commit comments

Comments
 (0)