Skip to content

Commit a100d24

Browse files
committed
Introduce separate BlockByIdentifier class to get Layout Block based on CMS Block Identifier
1 parent 6b6f428 commit a100d24

File tree

3 files changed

+330
-0
lines changed

3 files changed

+330
-0
lines changed

app/code/Magento/Cms/Block/Block.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
/**
1212
* Cms block content block
13+
* @deprecated This class introduces caching issues and should no longer be used
14+
* @see \Magento\Cms\Block\BlockByIdentifier
1315
*/
1416
class Block extends AbstractBlock implements \Magento\Framework\DataObject\IdentityInterface
1517
{
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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\Cms\Block;
9+
10+
use Magento\Cms\Api\Data\BlockInterface;
11+
use Magento\Cms\Api\GetBlockByIdentifierInterface;
12+
use Magento\Cms\Model\Template\FilterProvider;
13+
use Magento\Framework\DataObject\IdentityInterface;
14+
use Magento\Framework\Exception\NoSuchEntityException;
15+
use Magento\Framework\View\Element\AbstractBlock;
16+
use Magento\Framework\View\Element\Context;
17+
use Magento\Store\Api\Data\StoreInterface;
18+
use Magento\Store\Model\StoreManagerInterface;
19+
20+
/**
21+
* This class is replacement of \Magento\Cms\Block\Block, that accepts only `string` identifier of CMS Block
22+
*
23+
* @method getIdentifier(): int Returns the value of `identifier` injected in `<block>` definition
24+
*/
25+
class BlockByIdentifier extends AbstractBlock implements IdentityInterface
26+
{
27+
const CACHE_KEY_PREFIX = 'CMS_BLOCK';
28+
29+
/**
30+
* @var GetBlockByIdentifierInterface
31+
*/
32+
private $blockByIdentifier;
33+
34+
/**
35+
* @var StoreManagerInterface
36+
*/
37+
private $storeManager;
38+
39+
/**
40+
* @var FilterProvider
41+
*/
42+
private $filterProvider;
43+
44+
/**
45+
* @var BlockInterface
46+
*/
47+
private $cmsBlock;
48+
49+
public function __construct(
50+
GetBlockByIdentifierInterface $blockByIdentifier,
51+
StoreManagerInterface $storeManager,
52+
FilterProvider $filterProvider,
53+
Context $context,
54+
array $data = []
55+
) {
56+
parent::__construct($context, $data);
57+
$this->blockByIdentifier = $blockByIdentifier;
58+
$this->storeManager = $storeManager;
59+
$this->filterProvider = $filterProvider;
60+
}
61+
62+
/**
63+
* @inheritDoc
64+
*/
65+
protected function _toHtml(): string
66+
{
67+
try {
68+
return $this->filterOutput(
69+
$this->getCmsBlock()->getContent()
70+
);
71+
} catch (NoSuchEntityException $e) {
72+
return '';
73+
}
74+
}
75+
76+
/**
77+
* Filters the Content
78+
*
79+
* @param string $content
80+
* @return string
81+
* @throws NoSuchEntityException
82+
*/
83+
private function filterOutput(string $content): string
84+
{
85+
return $this->filterProvider->getBlockFilter()
86+
->setStoreId($this->getCurrentStore()->getId())
87+
->filter($content);
88+
}
89+
90+
/**
91+
* Loads the CMS block by `identifier` provided as an argument
92+
*
93+
* @return BlockInterface
94+
* @throws NoSuchEntityException
95+
*/
96+
private function getCmsBlock(): BlockInterface
97+
{
98+
if (!$this->getIdentifier()) {
99+
throw new NoSuchEntityException(
100+
__('Expected value of `identifier` was not provided')
101+
);
102+
}
103+
104+
if (null === $this->cmsBlock) {
105+
$this->cmsBlock = $this->blockByIdentifier->execute(
106+
(string)$this->getIdentifier(),
107+
(int)$this->getCurrentStore()->getId()
108+
);
109+
}
110+
111+
return $this->cmsBlock;
112+
}
113+
114+
/**
115+
* Returns the StoreInterface of currently opened Store scope
116+
*
117+
* @return StoreInterface
118+
* @throws \Magento\Framework\Exception\NoSuchEntityException
119+
*/
120+
private function getCurrentStore(): StoreInterface
121+
{
122+
return $this->storeManager->getStore();
123+
}
124+
125+
/**
126+
* Returns array of Block Identifiers used to determine Cache Tags
127+
*
128+
* This implementation supports different CMS blocks caching having the same identifier,
129+
* resolving the bug introduced in scope of \Magento\Cms\Block\Block
130+
*
131+
* @return string[]
132+
*/
133+
public function getIdentities(): array
134+
{
135+
try {
136+
return [
137+
self::CACHE_KEY_PREFIX . '_' . $this->getCmsBlock()->getId(),
138+
self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier() . '_' . $this->getCurrentStore()->getId()
139+
];
140+
} catch (NoSuchEntityException $e) {
141+
// If CMS Block does not exist, it should not be cached
142+
return [];
143+
}
144+
}
145+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<?php
2+
3+
/**
4+
* Copyright © Magento, Inc. All rights reserved.
5+
* See COPYING.txt for license details.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Magento\Cms\Test\Unit\Block;
10+
11+
use Magento\Cms\Api\Data\BlockInterface;
12+
use Magento\Cms\Api\GetBlockByIdentifierInterface;
13+
use Magento\Cms\Block\BlockByIdentifier;
14+
use Magento\Cms\Model\Template\FilterProvider;
15+
use Magento\Framework\App\Config\ScopeConfigInterface;
16+
use Magento\Framework\Event\ManagerInterface;
17+
use Magento\Framework\Filter\Template;
18+
use Magento\Framework\View\Element\Context;
19+
use Magento\Store\Api\Data\StoreInterface;
20+
use Magento\Store\Model\StoreManagerInterface;
21+
use PHPUnit\Framework\MockObject\MockObject;
22+
use PHPUnit\Framework\TestCase;
23+
24+
class BlockByIdentifierTest extends TestCase
25+
{
26+
private const STUB_MODULE_OUTPUT_DISABLED = false;
27+
private const STUB_EXISTING_IDENTIFIER = 'existingOne';
28+
private const STUB_DEFAULT_STORE = 1;
29+
private const STUB_CMS_BLOCK_ID = 1;
30+
private const STUB_CONTENT = 'Content';
31+
32+
private const ASSERT_EMPTY_BLOCK_HTML = '';
33+
private const ASSERT_CONTENT_HTML = self::STUB_CONTENT;
34+
private const ASSERT_NO_CACHE_IDENTITIES = [];
35+
36+
/** @var MockObject|GetBlockByIdentifierInterface */
37+
private $getBlockByIdentifierMock;
38+
39+
/** @var MockObject|StoreManagerInterface */
40+
private $storeManagerMock;
41+
42+
/** @var MockObject|FilterProvider */
43+
private $filterProviderMock;
44+
45+
/** @var MockObject|StoreInterface */
46+
private $storeMock;
47+
48+
protected function setUp()
49+
{
50+
$this->storeMock = $this->createMock(StoreInterface::class);
51+
$this->storeManagerMock = $this->createMock(StoreManagerInterface::class);
52+
$this->storeManagerMock->method('getStore')->willReturn($this->storeMock);
53+
54+
$this->getBlockByIdentifierMock = $this->createMock(GetBlockByIdentifierInterface::class);
55+
56+
$this->filterProviderMock = $this->createMock(FilterProvider::class);
57+
$this->filterProviderMock->method('getBlockFilter')->willReturn($this->getPassthroughFilterMock());
58+
}
59+
60+
public function testBlockReturnsEmptyStringWhenNoIdentifierProvided(): void
61+
{
62+
// Given
63+
$missingIdentifierBlock = $this->getTestedBlockUsingIdentifier(null);
64+
65+
// Expect
66+
$this->assertSame(self::ASSERT_EMPTY_BLOCK_HTML, $missingIdentifierBlock->toHtml());
67+
$this->assertSame(self::ASSERT_NO_CACHE_IDENTITIES, $missingIdentifierBlock->getIdentities());
68+
}
69+
70+
public function testBlockReturnsCmsContentsWhenIdentifierFound(): void
71+
{
72+
// Given
73+
$cmsBlockMock = $this->getCmsBlockMock(
74+
self::STUB_CMS_BLOCK_ID,
75+
self::STUB_EXISTING_IDENTIFIER,
76+
self::STUB_CONTENT
77+
);
78+
$this->storeMock->method('getId')->willReturn(self::STUB_DEFAULT_STORE);
79+
$this->getBlockByIdentifierMock->method('execute')
80+
->with(self::STUB_EXISTING_IDENTIFIER, self::STUB_DEFAULT_STORE)
81+
->willReturn($cmsBlockMock);
82+
$block = $this->getTestedBlockUsingIdentifier(self::STUB_EXISTING_IDENTIFIER);
83+
84+
// Expect
85+
$this->assertSame(self::ASSERT_CONTENT_HTML, $block->toHtml());
86+
}
87+
88+
public function testBlockCacheIdentitiesContainExplicitScopeInformation(): void
89+
{
90+
// Given
91+
$cmsBlockMock = $this->getCmsBlockMock(
92+
self::STUB_CMS_BLOCK_ID,
93+
self::STUB_EXISTING_IDENTIFIER,
94+
self::STUB_CONTENT
95+
);
96+
$this->storeMock->method('getId')->willReturn(self::STUB_DEFAULT_STORE);
97+
$this->getBlockByIdentifierMock->method('execute')
98+
->with(self::STUB_EXISTING_IDENTIFIER, self::STUB_DEFAULT_STORE)
99+
->willReturn($cmsBlockMock);
100+
$block = $this->getTestedBlockUsingIdentifier(self::STUB_EXISTING_IDENTIFIER);
101+
102+
// When
103+
$identities = $block->getIdentities();
104+
105+
// Then
106+
$this->assertContains($this->getCacheKeyStubById(self::STUB_CMS_BLOCK_ID), $identities);
107+
$this->assertContains(
108+
$this->getCacheKeyStubByIdentifier(self::STUB_EXISTING_IDENTIFIER, self::STUB_DEFAULT_STORE),
109+
$identities
110+
);
111+
}
112+
113+
/**
114+
* Initializes the tested block with injecting the references required by parent classes.
115+
*
116+
* @param string|null $identifier
117+
* @return BlockByIdentifier
118+
*/
119+
private function getTestedBlockUsingIdentifier(?string $identifier): BlockByIdentifier
120+
{
121+
$eventManagerMock = $this->createMock(ManagerInterface::class);
122+
$scopeConfigMock = $this->createMock(ScopeConfigInterface::class);
123+
$scopeConfigMock->method('getValue')->willReturn(self::STUB_MODULE_OUTPUT_DISABLED);
124+
125+
$contextMock = $this->createMock(Context::class);
126+
$contextMock->method('getEventManager')->willReturn($eventManagerMock);
127+
$contextMock->method('getScopeConfig')->willReturn($scopeConfigMock);
128+
129+
return new BlockByIdentifier(
130+
$this->getBlockByIdentifierMock,
131+
$this->storeManagerMock,
132+
$this->filterProviderMock,
133+
$contextMock,
134+
['identifier' => $identifier]
135+
);
136+
}
137+
138+
/**
139+
* Mocks the CMS Block object for further play
140+
*
141+
* @param int $entityId
142+
* @param string $identifier
143+
* @param string $content
144+
* @return MockObject|BlockInterface
145+
*/
146+
private function getCmsBlockMock(int $entityId, string $identifier, string $content): BlockInterface
147+
{
148+
$cmsBlock = $this->createMock(BlockInterface::class);
149+
150+
$cmsBlock->method('getId')->willReturn($entityId);
151+
$cmsBlock->method('getIdentifier')->willReturn($identifier);
152+
$cmsBlock->method('getContent')->willReturn($content);
153+
154+
return $cmsBlock;
155+
}
156+
157+
/**
158+
* Creates mock of the Filter that actually is doing nothing
159+
*
160+
* @return MockObject|Template
161+
*/
162+
private function getPassthroughFilterMock(): Template
163+
{
164+
$filterMock = $this->getMockBuilder(Template::class)
165+
->disableOriginalConstructor()
166+
->setMethods(['setStoreId', 'filter'])
167+
->getMock();
168+
$filterMock->method('setStoreId')->willReturnSelf();
169+
$filterMock->method('filter')->willReturnArgument(0);
170+
171+
return $filterMock;
172+
}
173+
174+
private function getCacheKeyStubByIdentifier(string $identifier, int $storeId = self::STUB_DEFAULT_STORE): string
175+
{
176+
return BlockByIdentifier::CACHE_KEY_PREFIX . '_' . $identifier . '_' . $storeId;
177+
}
178+
179+
private function getCacheKeyStubById(int $cmsBlockId): string
180+
{
181+
return BlockByIdentifier::CACHE_KEY_PREFIX . '_' . $cmsBlockId;
182+
}
183+
}

0 commit comments

Comments
 (0)