Skip to content

Commit 9c0e123

Browse files
authored
Merge pull request #8211 from magento-arcticfoxes/B2B-2451
B2B-2451: Implement GraphQL Resolver Cache for cmsPage query
2 parents 2f0555a + c93c2c0 commit 9c0e123

File tree

20 files changed

+1272
-11
lines changed

20 files changed

+1272
-11
lines changed

app/code/Magento/Backend/Test/Mftf/ActionGroup/SecondaryGridActionGroup.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@
2727
<waitForPageLoad stepKey="waitForTaxRateLoad"/>
2828

2929
<!-- delete the rule -->
30+
<waitForElementVisible selector="{{AdminStoresMainActionsSection.deleteButton}}" stepKey="waitForDelete"/>
3031
<click stepKey="clickDelete" selector="{{AdminStoresMainActionsSection.deleteButton}}"/>
32+
<waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForConfirmationModal"/>
3133
<click stepKey="clickOk" selector="{{AdminConfirmationModalSection.ok}}"/>
32-
<see stepKey="seeSuccess" selector="{{AdminMessagesSection.success}}" userInput="deleted"/>
34+
<waitForText stepKey="seeSuccess" selector="{{AdminMessagesSection.success}}" userInput="deleted"/>
3335
</actionGroup>
3436
</actionGroups>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\CmsGraphQl\Model\Resolver\Page;
9+
10+
use Magento\Cms\Api\Data\PageInterface;
11+
use Magento\Cms\Model\Page;
12+
use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface;
13+
14+
/**
15+
* Identity for resolved CMS page for resolver cache type
16+
*/
17+
class ResolverCacheIdentity implements IdentityInterface
18+
{
19+
/**
20+
* @var string
21+
*/
22+
private $cacheTag = Page::CACHE_TAG;
23+
24+
/**
25+
* Get page ID from resolved data
26+
*
27+
* @param array $resolvedData
28+
* @return string[]
29+
*/
30+
public function getIdentities(array $resolvedData): array
31+
{
32+
return empty($resolvedData[PageInterface::PAGE_ID]) ?
33+
[] : [sprintf('%s_%s', $this->cacheTag, $resolvedData[PageInterface::PAGE_ID])];
34+
}
35+
}
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\CmsGraphQl\Test\Integration\Model\Resolver;
9+
10+
use Magento\Cms\Api\Data\PageInterface;
11+
use Magento\Cms\Model\PageRepository;
12+
use Magento\Framework\Api\SearchCriteriaBuilder;
13+
use Magento\Framework\App\Cache\StateInterface as CacheStateInterface;
14+
use Magento\Framework\App\Cache\Type\FrontendPool;
15+
use Magento\Framework\ObjectManagerInterface;
16+
use Magento\GraphQl\Service\GraphQlRequest;
17+
use Magento\GraphQlCache\Model\Cache\Query\Resolver\Result\Type as GraphQlResolverCache;
18+
use Magento\GraphQlCache\Model\Plugin\Query\Resolver\Result\Cache as ResolverResultCachePlugin;
19+
use Magento\TestFramework\Helper\Bootstrap;
20+
use PHPUnit\Framework\TestCase;
21+
22+
/**
23+
* Test GraphQl Resolver cache saves and loads properly
24+
* @magentoAppArea graphql
25+
*/
26+
class PageTest extends TestCase
27+
{
28+
/**
29+
* @var ObjectManagerInterface
30+
*/
31+
private $objectManager;
32+
33+
/**
34+
* @var GraphQlRequest
35+
*/
36+
private $graphQlRequest;
37+
38+
/**
39+
* @var ResolverResultCachePlugin
40+
*/
41+
private $originalResolverResultCachePlugin;
42+
43+
/**
44+
* @var SearchCriteriaBuilder
45+
*/
46+
private $searchCriteriaBuilder;
47+
48+
/**
49+
* @var PageRepository
50+
*/
51+
private $pageRepository;
52+
53+
/**
54+
* @var CacheStateInterface
55+
*/
56+
private $cacheState;
57+
58+
/**
59+
* @var bool
60+
*/
61+
private $originalCacheStateEnabledStatus;
62+
63+
protected function setUp(): void
64+
{
65+
$this->objectManager = $objectManager = Bootstrap::getObjectManager();
66+
$this->graphQlRequest = $objectManager->create(GraphQlRequest::class);
67+
$this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class);
68+
$this->pageRepository = $objectManager->get(PageRepository::class);
69+
$this->originalResolverResultCachePlugin = $objectManager->get(ResolverResultCachePlugin::class);
70+
71+
$this->cacheState = $objectManager->get(CacheStateInterface::class);
72+
$this->originalCacheStateEnabledStatus = $this->cacheState->isEnabled(GraphQlResolverCache::TYPE_IDENTIFIER);
73+
$this->cacheState->setEnabled(GraphQlResolverCache::TYPE_IDENTIFIER, true);
74+
}
75+
76+
protected function tearDown(): void
77+
{
78+
$objectManager = $this->objectManager;
79+
80+
// reset to original resolver plugin
81+
$objectManager->addSharedInstance($this->originalResolverResultCachePlugin, ResolverResultCachePlugin::class);
82+
83+
// clean graphql resolver cache and reset to original enablement status
84+
$objectManager->get(GraphQlResolverCache::class)->clean();
85+
$this->cacheState->setEnabled(GraphQlResolverCache::TYPE_IDENTIFIER, $this->originalCacheStateEnabledStatus);
86+
}
87+
88+
/**
89+
* Test that result can be loaded continuously after saving once when passing the same arguments
90+
*
91+
* @magentoDataFixture Magento/Cms/Fixtures/page_list.php
92+
* @return void
93+
*/
94+
public function testResultIsLoadedMultipleTimesAfterOnlyBeingSavedOnce()
95+
{
96+
$objectManager = $this->objectManager;
97+
$page = $this->getPageByTitle('Page with 1column layout');
98+
99+
$frontendPool = $objectManager->get(FrontendPool::class);
100+
101+
$cacheProxy = $this->getMockBuilder(GraphQlResolverCache::class)
102+
->enableProxyingToOriginalMethods()
103+
->setConstructorArgs([
104+
$frontendPool
105+
])
106+
->getMock();
107+
108+
// assert cache proxy calls load at least once for the same CMS page query
109+
$cacheProxy
110+
->expects($this->atLeastOnce())
111+
->method('load');
112+
113+
// assert save is called at most once for the same CMS page query
114+
$cacheProxy
115+
->expects($this->once())
116+
->method('save');
117+
118+
$resolverPluginWithCacheProxy = $objectManager->create(ResolverResultCachePlugin::class, [
119+
'graphQlResolverCache' => $cacheProxy,
120+
]);
121+
122+
// override resolver plugin with plugin instance containing cache proxy class
123+
$objectManager->addSharedInstance($resolverPluginWithCacheProxy, ResolverResultCachePlugin::class);
124+
125+
$query = $this->getQuery($page->getIdentifier());
126+
127+
// send request and assert save is called
128+
$this->graphQlRequest->send($query);
129+
130+
// send again and assert save is not called (i.e. result is loaded from resolver cache)
131+
$this->graphQlRequest->send($query);
132+
133+
// send again with whitespace appended and assert save is not called (i.e. result is loaded from resolver cache)
134+
$this->graphQlRequest->send($query . ' ');
135+
136+
// send again with a different field and assert save is not called (i.e. result is loaded from resolver cache)
137+
$differentQuery = $this->getQuery($page->getIdentifier(), ['meta_title']);
138+
$this->graphQlRequest->send($differentQuery);
139+
}
140+
141+
/**
142+
* Test that resolver plugin does not call GraphQlResolverCache's save or load methods when it is disabled
143+
*
144+
* @magentoDataFixture Magento/Cms/Fixtures/page_list.php
145+
* @return void
146+
*/
147+
public function testNeitherSaveNorLoadAreCalledWhenResolverCacheIsDisabled()
148+
{
149+
$objectManager = $this->objectManager;
150+
$page = $this->getPageByTitle('Page with 1column layout');
151+
152+
// disable graphql resolver cache
153+
$this->cacheState->setEnabled(GraphQlResolverCache::TYPE_IDENTIFIER, false);
154+
155+
$frontendPool = $objectManager->get(FrontendPool::class);
156+
157+
$cacheProxy = $this->getMockBuilder(GraphQlResolverCache::class)
158+
->enableProxyingToOriginalMethods()
159+
->setConstructorArgs([
160+
$frontendPool
161+
])
162+
->getMock();
163+
164+
// assert cache proxy never calls load
165+
$cacheProxy
166+
->expects($this->never())
167+
->method('load');
168+
169+
// assert save is also never called
170+
$cacheProxy
171+
->expects($this->never())
172+
->method('save');
173+
174+
$resolverPluginWithCacheProxy = $objectManager->create(ResolverResultCachePlugin::class, [
175+
'graphQlResolverCache' => $cacheProxy,
176+
]);
177+
178+
// override resolver plugin with plugin instance containing cache proxy class
179+
$objectManager->addSharedInstance($resolverPluginWithCacheProxy, ResolverResultCachePlugin::class);
180+
181+
$query = $this->getQuery($page->getIdentifier());
182+
183+
// send request multiple times and assert neither save nor load are called
184+
$this->graphQlRequest->send($query);
185+
$this->graphQlRequest->send($query);
186+
}
187+
188+
public function testSaveIsNeverCalledWhenMissingRequiredArgumentInQuery()
189+
{
190+
$objectManager = $this->objectManager;
191+
192+
$frontendPool = $objectManager->get(FrontendPool::class);
193+
194+
$cacheProxy = $this->getMockBuilder(GraphQlResolverCache::class)
195+
->enableProxyingToOriginalMethods()
196+
->setConstructorArgs([
197+
$frontendPool
198+
])
199+
->getMock();
200+
201+
// assert cache proxy never calls save
202+
$cacheProxy
203+
->expects($this->never())
204+
->method('save');
205+
206+
$resolverPluginWithCacheProxy = $objectManager->create(ResolverResultCachePlugin::class, [
207+
'graphQlResolverCache' => $cacheProxy,
208+
]);
209+
210+
// override resolver plugin with plugin instance containing cache proxy class
211+
$objectManager->addSharedInstance($resolverPluginWithCacheProxy, ResolverResultCachePlugin::class);
212+
213+
$query = <<<QUERY
214+
{
215+
cmsPage {
216+
title
217+
}
218+
}
219+
QUERY;
220+
221+
// send request multiple times and assert save is never called
222+
$this->graphQlRequest->send($query);
223+
$this->graphQlRequest->send($query);
224+
}
225+
226+
private function getQuery(string $identifier, array $fields = ['title']): string
227+
{
228+
$fields = implode(PHP_EOL, $fields);
229+
230+
return <<<QUERY
231+
{
232+
cmsPage(identifier: "$identifier") {
233+
$fields
234+
}
235+
}
236+
QUERY;
237+
}
238+
239+
private function getPageByTitle(string $title): PageInterface
240+
{
241+
$searchCriteria = $this->searchCriteriaBuilder
242+
->addFilter('title', $title)
243+
->create();
244+
245+
$pages = $this->pageRepository->getList($searchCriteria)->getItems();
246+
247+
/** @var PageInterface $page */
248+
$page = reset($pages);
249+
250+
return $page;
251+
}
252+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0"?>
2+
<!--
3+
/**
4+
* Copyright © Magento, Inc. All rights reserved.
5+
* See COPYING.txt for license details.
6+
*/
7+
-->
8+
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
9+
<type name="Magento\GraphQlCache\Model\Cache\Query\Resolver\Result\TagResolver">
10+
<arguments>
11+
<argument name="invalidatableObjectTypes" xsi:type="array">
12+
<item name="Magento\Cms\Api\Data\PageInterface" xsi:type="string">
13+
Magento\Cms\Api\Data\PageInterface
14+
</item>
15+
</argument>
16+
</arguments>
17+
</type>
18+
</config>

app/code/Magento/CmsGraphQl/etc/graphql/di.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,13 @@
1818
</argument>
1919
</arguments>
2020
</type>
21+
<type name="Magento\GraphQlCache\Model\Cache\Query\Resolver\Result\ResolverIdentityClassLocator">
22+
<arguments>
23+
<argument name="cacheableResolverClassNameIdentityMap" xsi:type="array">
24+
<item name="Magento\CmsGraphQl\Model\Resolver\Page" xsi:type="string">
25+
Magento\CmsGraphQl\Model\Resolver\Page\ResolverCacheIdentity
26+
</item>
27+
</argument>
28+
</arguments>
29+
</type>
2130
</config>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\GraphQlCache\Model\Cache\Query\Resolver\Result;
9+
10+
use Magento\Framework\GraphQl\Query\Resolver\IdentityInterface;
11+
use Magento\Framework\GraphQl\Query\ResolverInterface;
12+
use Magento\GraphQlCache\Model\Resolver\IdentityPool;
13+
14+
class ResolverIdentityClassLocator
15+
{
16+
/**
17+
* @var IdentityPool
18+
*/
19+
private $identityPool;
20+
21+
/**
22+
* Map of Resolver Class Name => Identity Provider
23+
*
24+
* @var string[]
25+
*/
26+
private array $cacheableResolverClassNameIdentityMap;
27+
28+
/**
29+
* @param IdentityPool $identityPool
30+
* @param array $cacheableResolverClassNameIdentityMap
31+
*/
32+
public function __construct(
33+
IdentityPool $identityPool,
34+
array $cacheableResolverClassNameIdentityMap
35+
) {
36+
$this->identityPool = $identityPool;
37+
$this->cacheableResolverClassNameIdentityMap = $cacheableResolverClassNameIdentityMap;
38+
}
39+
40+
/**
41+
* Get Identity provider based on $resolver
42+
*
43+
* @param ResolverInterface $resolver
44+
* @return IdentityInterface|null
45+
*/
46+
public function getIdentityFromResolver(ResolverInterface $resolver): ?IdentityInterface
47+
{
48+
$matchingIdentityProviderClassName = null;
49+
50+
foreach ($this->cacheableResolverClassNameIdentityMap as $resolverClassName => $identityProviderClassName) {
51+
if ($resolver instanceof $resolverClassName) {
52+
$matchingIdentityProviderClassName = $identityProviderClassName;
53+
break;
54+
}
55+
}
56+
57+
if (!$matchingIdentityProviderClassName) {
58+
return null;
59+
}
60+
61+
return $this->identityPool->get($matchingIdentityProviderClassName);
62+
}
63+
}

0 commit comments

Comments
 (0)