diff --git a/app/code/Magento/SalesRule/Model/Plugin/QuoteConfigProductAttributes.php b/app/code/Magento/SalesRule/Model/Plugin/QuoteConfigProductAttributes.php index 764658b3ac00a..f7ee3e6c27914 100644 --- a/app/code/Magento/SalesRule/Model/Plugin/QuoteConfigProductAttributes.php +++ b/app/code/Magento/SalesRule/Model/Plugin/QuoteConfigProductAttributes.php @@ -6,11 +6,23 @@ namespace Magento\SalesRule\Model\Plugin; +use Magento\Framework\App\CacheInterface; +use Magento\Framework\Serialize\SerializerInterface; use Magento\Quote\Model\Quote\Config; use Magento\SalesRule\Model\ResourceModel\Rule as RuleResource; class QuoteConfigProductAttributes { + /** + * Cache key for active salesrule attributes + */ + private const CACHE_KEY = 'salesrule_active_product_attributes'; + + /** + * Cache tag for salesrule attributes + */ + private const CACHE_TAG = 'salesrule'; + /** * @var RuleResource */ @@ -21,17 +33,36 @@ class QuoteConfigProductAttributes */ private $activeAttributeCodes; + /** + * @var CacheInterface + */ + private $cache; + + /** + * @var SerializerInterface + */ + private $serializer; + /** * @param RuleResource $ruleResource + * @param CacheInterface $cache + * @param SerializerInterface $serializer */ - public function __construct(RuleResource $ruleResource) - { + public function __construct( + RuleResource $ruleResource, + CacheInterface $cache, + SerializerInterface $serializer + ) { $this->ruleResource = $ruleResource; + $this->cache = $cache; + $this->serializer = $serializer; } /** * Append sales rule product attribute keys to select by quote item collection * + * Uses cache to avoid database queries on every request. + * * @param Config $subject * @param array $attributeKeys * @@ -41,7 +72,21 @@ public function __construct(RuleResource $ruleResource) public function afterGetProductAttributes(Config $subject, array $attributeKeys): array { if ($this->activeAttributeCodes === null) { - $this->activeAttributeCodes = array_column($this->ruleResource->getActiveAttributes(), 'attribute_code'); + $cachedData = $this->cache->load(self::CACHE_KEY); + + if ($cachedData !== false) { + $this->activeAttributeCodes = $this->serializer->unserialize($cachedData); + } else { + $this->activeAttributeCodes = array_column( + $this->ruleResource->getActiveAttributes(), + 'attribute_code' + ); + $this->cache->save( + $this->serializer->serialize($this->activeAttributeCodes), + self::CACHE_KEY, + [self::CACHE_TAG] + ); + } } return array_merge($attributeKeys, $this->activeAttributeCodes); diff --git a/app/code/Magento/SalesRule/Observer/ClearProductAttributesCacheObserver.php b/app/code/Magento/SalesRule/Observer/ClearProductAttributesCacheObserver.php new file mode 100644 index 0000000000000..68e14e2bef4c9 --- /dev/null +++ b/app/code/Magento/SalesRule/Observer/ClearProductAttributesCacheObserver.php @@ -0,0 +1,47 @@ +cache = $cache; + } + + /** + * Clear salesrule product attributes cache + * + * @param Observer $observer + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function execute(Observer $observer): void + { + $this->cache->remove(self::CACHE_KEY); + } +} diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/QuoteConfigProductAttributesTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/QuoteConfigProductAttributesTest.php index a06584cec2273..c9b2f211c5463 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/QuoteConfigProductAttributesTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Plugin/QuoteConfigProductAttributesTest.php @@ -7,6 +7,8 @@ namespace Magento\SalesRule\Test\Unit\Model\Plugin; +use Magento\Framework\App\CacheInterface; +use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Quote\Model\Quote\Config; use Magento\SalesRule\Model\Plugin\QuoteConfigProductAttributes; @@ -17,7 +19,7 @@ class QuoteConfigProductAttributesTest extends TestCase { /** - * @var QuoteConfigProductAttributes|MockObject + * @var QuoteConfigProductAttributes */ protected $plugin; @@ -26,24 +28,67 @@ class QuoteConfigProductAttributesTest extends TestCase */ protected $ruleResource; + /** + * @var CacheInterface|MockObject + */ + protected $cache; + + /** + * @var SerializerInterface|MockObject + */ + protected $serializer; + protected function setUp(): void { $objectManager = new ObjectManager($this); $this->ruleResource = $this->createMock(Rule::class); + $this->cache = $this->createMock(CacheInterface::class); + $this->serializer = $this->createMock(SerializerInterface::class); $this->plugin = $objectManager->getObject( QuoteConfigProductAttributes::class, [ - 'ruleResource' => $this->ruleResource + 'ruleResource' => $this->ruleResource, + 'cache' => $this->cache, + 'serializer' => $this->serializer ] ); } - public function testAfterGetProductAttributes() + public function testAfterGetProductAttributesWithCache() + { + $subject = $this->createMock(Config::class); + $attributeCode = 'code of the attribute'; + $expected = [0 => $attributeCode]; + $serializedData = '["' . $attributeCode . '"]'; + + $this->cache->expects($this->once()) + ->method('load') + ->with('salesrule_active_product_attributes') + ->willReturn($serializedData); + + $this->serializer->expects($this->once()) + ->method('unserialize') + ->with($serializedData) + ->willReturn([$attributeCode]); + + $this->ruleResource->expects($this->never()) + ->method('getActiveAttributes'); + + $this->assertEquals($expected, $this->plugin->afterGetProductAttributes($subject, [])); + } + + public function testAfterGetProductAttributesWithoutCache() { $subject = $this->createMock(Config::class); $attributeCode = 'code of the attribute'; $expected = [0 => $attributeCode]; + $serializedData = '["' . $attributeCode . '"]'; + + $this->cache->expects($this->once()) + ->method('load') + ->with('salesrule_active_product_attributes') + ->willReturn(false); $this->ruleResource->expects($this->once()) ->method('getActiveAttributes') @@ -53,6 +98,15 @@ public function testAfterGetProductAttributes() ] ); + $this->serializer->expects($this->once()) + ->method('serialize') + ->with([$attributeCode]) + ->willReturn($serializedData); + + $this->cache->expects($this->once()) + ->method('save') + ->with($serializedData, 'salesrule_active_product_attributes', ['salesrule']); + $this->assertEquals($expected, $this->plugin->afterGetProductAttributes($subject, [])); } } diff --git a/app/code/Magento/SalesRule/Test/Unit/Observer/ClearProductAttributesCacheObserverTest.php b/app/code/Magento/SalesRule/Test/Unit/Observer/ClearProductAttributesCacheObserverTest.php new file mode 100644 index 0000000000000..904b642b0731b --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Unit/Observer/ClearProductAttributesCacheObserverTest.php @@ -0,0 +1,44 @@ +cache = $this->createMock(CacheInterface::class); + $this->observer = new ClearProductAttributesCacheObserver($this->cache); + } + + public function testExecuteClearsCache() + { + $event = $this->createMock(Observer::class); + + $this->cache->expects($this->once()) + ->method('remove') + ->with('salesrule_active_product_attributes'); + + $this->observer->execute($event); + } +} diff --git a/app/code/Magento/SalesRule/etc/events.xml b/app/code/Magento/SalesRule/etc/events.xml index f430ef2ee8758..20e448a5ef3a1 100644 --- a/app/code/Magento/SalesRule/etc/events.xml +++ b/app/code/Magento/SalesRule/etc/events.xml @@ -32,9 +32,11 @@ + +