Skip to content
Open
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
37 changes: 37 additions & 0 deletions app/code/Magento/SalesRule/Model/Plugin/FrontController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
/**
* Copyright 2025 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

namespace Magento\SalesRule\Model\Plugin;

class FrontController
{
/**
* @param RequestTypeRegistry $requestTypeRegistry
*/
public function __construct(
private RequestTypeRegistry $requestTypeRegistry
) {
}

/**
* Identify the Request Type and set true if it is a GET request
*
* @param \Magento\Framework\App\FrontControllerInterface $subject
* @param \Magento\Framework\App\RequestInterface $request
* @return void
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function beforeDispatch(
\Magento\Framework\App\FrontControllerInterface $subject,
\Magento\Framework\App\RequestInterface $request
) {
$method = strtoupper($request->getMethod());
$isReadOnly = ($method === 'GET');

$this->requestTypeRegistry->setIsGetRequestOrQuery($isReadOnly);
}
}
38 changes: 38 additions & 0 deletions app/code/Magento/SalesRule/Model/Plugin/GraphQlController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php
/**
* Copyright 2025 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

namespace Magento\SalesRule\Model\Plugin;

use Magento\Framework\App\ResponseInterface;
use Magento\Framework\App\FrontControllerInterface;

class GraphQlController
{
/**
* @param RequestTypeRegistry $requestTypeRegistry
*/
public function __construct(
private RequestTypeRegistry $requestTypeRegistry
) {
}

/**
* Reset request type registry after dispatching GraphQL controller
*
* @param FrontControllerInterface $subject
* @param ResponseInterface $result
* @return ResponseInterface $result
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function afterDispatch(
FrontControllerInterface $subject,
ResponseInterface $result
) : ResponseInterface {
$this->requestTypeRegistry->reset();
return $result;
}
}
47 changes: 47 additions & 0 deletions app/code/Magento/SalesRule/Model/Plugin/QueryParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php
/**
* Copyright 2025 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

namespace Magento\SalesRule\Model\Plugin;

class QueryParser
{

/**
* @param RequestTypeRegistry $requestTypeRegistry
*/
public function __construct(
private RequestTypeRegistry $requestTypeRegistry
) {
}

/**
* Set QueryType to RequestTypeRegistry
*
* @param \Magento\Framework\GraphQl\Query\QueryParser $subject
* @param \GraphQL\Language\AST\DocumentNode $documentNode
* @return \GraphQL\Language\AST\DocumentNode $documentNode
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function afterParse(\Magento\Framework\GraphQl\Query\QueryParser $subject, $documentNode)
{
// Get the first operation definition
$operation = null;
foreach ($documentNode->definitions as $definition) {
if ($definition instanceof \GraphQL\Language\AST\OperationDefinitionNode) {
$operation = $definition;
break;
}
}

if ($operation) {
$isOperationTypeQuery = ($operation->operation === 'query');
$this->requestTypeRegistry->setIsGetRequestOrQuery($isOperationTypeQuery);
}

return $documentNode;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
<?php
/**
* Copyright 2013 Adobe
* Copyright 2025 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

namespace Magento\SalesRule\Model\Plugin;

use Magento\Framework\App\CacheInterface;
use Magento\Framework\Serialize\SerializerInterface;
use Magento\Quote\Model\Quote\Config;
use Magento\Quote\Model\Quote;
use Magento\SalesRule\Model\ResourceModel\Rule as RuleResource;

class QuoteConfigProductAttributes
Expand All @@ -21,11 +25,28 @@ class QuoteConfigProductAttributes
*/
private $activeAttributeCodes;

/**
* Cache key for active salesrule attributes
*/
private const CACHE_KEY = 'salesrule_active_product_attributes';

/**
* Cache tag for salesrule attributes
*/
private const CACHE_TAG = 'salesrule';

/**
* @param RuleResource $ruleResource
* @param RequestTypeRegistry $requestTypeRegistry
* @param CacheInterface $cache
* @param SerializerInterface $serializer
*/
public function __construct(RuleResource $ruleResource)
{
public function __construct(
RuleResource $ruleResource,
private RequestTypeRegistry $requestTypeRegistry,
private CacheInterface $cache,
private SerializerInterface $serializer
) {
$this->ruleResource = $ruleResource;
}

Expand All @@ -40,8 +61,24 @@ 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');
if ($this->requestTypeRegistry->isGetRequestOrQuery()) {
return $attributeKeys;
}

$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);
Expand Down
36 changes: 36 additions & 0 deletions app/code/Magento/SalesRule/Model/Plugin/QuoteItemCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
/**
* Copyright 2025 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

namespace Magento\SalesRule\Model\Plugin;

class QuoteItemCollection
{
/**
* @param RequestTypeRegistry $requestTypeRegistry
*/
public function __construct(
private RequestTypeRegistry $requestTypeRegistry
) {
}

/**
* Set the Request Type before setting the quote in the Quote Item Collection.
*
* @param \Magento\Quote\Model\ResourceModel\Quote\Item\Collection $subject The collection instance.
* @param \Magento\Quote\Model\Quote $quote The quote to be processed.
* @return void
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function beforeSetQuote(
\Magento\Quote\Model\ResourceModel\Quote\Item\Collection $subject,
\Magento\Quote\Model\Quote $quote
) {
if ($quote->getTriggerRecollect() == 1 && $this->requestTypeRegistry->isGetRequestOrQuery()) {
$this->requestTypeRegistry->setIsGetRequestOrQuery(false);
}
}
}
53 changes: 53 additions & 0 deletions app/code/Magento/SalesRule/Model/Plugin/RequestTypeRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php
/**
* Copyright 2025 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

namespace Magento\SalesRule\Model\Plugin;

/**
* Tracks whether the request is http post or mutation
*
* This class is used to optimize performance by only loading salesrule product attributes
* when they're actually needed during totals collection, rather than on every quote load.
*/
class RequestTypeRegistry
{
/**
* @var bool
*/
private $isGetRequestOrQuery= false;

/**
* Set Request state
*
* @param bool $state
* @return void
*/
public function setIsGetRequestOrQuery(bool $state): void
{
$this->isGetRequestOrQuery = $state;
}

/**
* Check if request is a get or query
*
* @return bool
*/
public function isGetRequestOrQuery(): bool
{
return $this->isGetRequestOrQuery;
}

/**
* Resets the state of the object by setting the isGetRequestOrQuery property to false.
*
* @return void
*/
public function reset(): void
{
$this->isGetRequestOrQuery = false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php
/**
* Copyright 2025 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

namespace Magento\SalesRule\Test\Unit\Model\Plugin;

use Magento\Framework\App\FrontControllerInterface;
use Magento\Framework\App\RequestInterface;
use Magento\SalesRule\Model\Plugin\FrontController;
use Magento\SalesRule\Model\Plugin\RequestTypeRegistry;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

class FrontControllerTest extends TestCase
{
/** @var RequestTypeRegistry|MockObject */
private $requestTypeRegistry;

/** @var FrontController */
private $plugin;

protected function setUp(): void
{
$this->requestTypeRegistry = $this->createMock(RequestTypeRegistry::class);
$this->plugin = new FrontController($this->requestTypeRegistry);
}

public function testBeforeDispatchSetsTrueForGetRequests(): void
{
$subject = $this->createMock(FrontControllerInterface::class);
$request = $this->createMock(RequestInterface::class);
$request->method('getMethod')->willReturn('GET');

$this->requestTypeRegistry
->expects($this->once())
->method('setIsGetRequestOrQuery')
->with(true);

$this->plugin->beforeDispatch($subject, $request);
}

public function testBeforeDispatchSetsFalseForPostRequests(): void
{
$subject = $this->createMock(FrontControllerInterface::class);
$request = $this->createMock(RequestInterface::class);
$request->method('getMethod')->willReturn('POST');

$this->requestTypeRegistry
->expects($this->once())
->method('setIsGetRequestOrQuery')
->with(false);

$this->plugin->beforeDispatch($subject, $request);
}

public function testBeforeDispatchIsCaseInsensitive(): void
{
$subject = $this->createMock(FrontControllerInterface::class);
$request = $this->createMock(RequestInterface::class);
$request->method('getMethod')->willReturn('get'); // lowercase

$this->requestTypeRegistry
->expects($this->once())
->method('setIsGetRequestOrQuery')
->with(true);

$this->plugin->beforeDispatch($subject, $request);
}
}
Loading