Skip to content

Commit e6bbe78

Browse files
author
Eric Bohanon
committed
MAGETWO-70346: Configurable product shows on frontend after deleting child products
- Add plugin around delete to update all indexers for child products - Add class to update all indexers related to product - Unit Tests - Functional test
1 parent 9223ab3 commit e6bbe78

File tree

11 files changed

+524
-7
lines changed

11 files changed

+524
-7
lines changed

app/code/Magento/Catalog/Controller/Adminhtml/Product/MassDelete.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
namespace Magento\Catalog\Controller\Adminhtml\Product;
88

99
use Magento\Framework\Controller\ResultFactory;
10-
use Magento\Catalog\Controller\Adminhtml\Product\Builder;
1110
use Magento\Backend\App\Action\Context;
1211
use Magento\Ui\Component\MassAction\Filter;
1312
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
13+
use Magento\Catalog\Api\ProductRepositoryInterface;
1414

1515
class MassDelete extends \Magento\Catalog\Controller\Adminhtml\Product
1616
{
@@ -26,20 +26,29 @@ class MassDelete extends \Magento\Catalog\Controller\Adminhtml\Product
2626
*/
2727
protected $collectionFactory;
2828

29+
/**
30+
* @var ProductRepositoryInterface
31+
*/
32+
private $productRepository;
33+
2934
/**
3035
* @param Context $context
3136
* @param Builder $productBuilder
3237
* @param Filter $filter
3338
* @param CollectionFactory $collectionFactory
39+
* @param ProductRepositoryInterface $productRepository
3440
*/
3541
public function __construct(
3642
Context $context,
3743
Builder $productBuilder,
3844
Filter $filter,
39-
CollectionFactory $collectionFactory
45+
CollectionFactory $collectionFactory,
46+
ProductRepositoryInterface $productRepository = null
4047
) {
4148
$this->filter = $filter;
4249
$this->collectionFactory = $collectionFactory;
50+
$this->productRepository = $productRepository
51+
?: \Magento\Framework\App\ObjectManager::getInstance()->create(ProductRepositoryInterface::class);
4352
parent::__construct($context, $productBuilder);
4453
}
4554

@@ -50,8 +59,9 @@ public function execute()
5059
{
5160
$collection = $this->filter->getCollection($this->collectionFactory->create());
5261
$productDeleted = 0;
62+
/** @var \Magento\Catalog\Model\Product $product */
5363
foreach ($collection->getItems() as $product) {
54-
$product->delete();
64+
$this->productRepository->delete($product);
5565
$productDeleted++;
5666
}
5767
$this->messageManager->addSuccess(
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento\Catalog\Model\Indexer\Product;
8+
9+
use Magento\Framework\Indexer\ActionInterface;
10+
use Magento\Framework\Indexer\IndexerRegistry;
11+
use Magento\PageCache\Model\Config;
12+
use Magento\Framework\App\Cache\TypeListInterface;
13+
14+
/**
15+
* Reindex all relevant product indexers
16+
*/
17+
class Full implements ActionInterface
18+
{
19+
/**
20+
* @var IndexerRegistry
21+
*/
22+
private $indexerRegistry;
23+
24+
/**
25+
* @var Config
26+
*/
27+
private $pageCacheConfig;
28+
29+
/**
30+
* @var TypeListInterface
31+
*/
32+
private $cacheTypeList;
33+
34+
/**
35+
* @var string[]
36+
*/
37+
private $indexerList;
38+
39+
/**
40+
* Initialize dependencies
41+
*
42+
* @param IndexerRegistry $indexerRegistry
43+
* @param Config $pageCacheConfig
44+
* @param TypeListInterface $cacheTypeList
45+
* @param string[] $indexerList
46+
*/
47+
public function __construct(
48+
IndexerRegistry $indexerRegistry,
49+
Config $pageCacheConfig,
50+
TypeListInterface $cacheTypeList,
51+
array $indexerList
52+
) {
53+
$this->indexerRegistry = $indexerRegistry;
54+
$this->pageCacheConfig = $pageCacheConfig;
55+
$this->cacheTypeList = $cacheTypeList;
56+
$this->indexerList = $indexerList;
57+
}
58+
59+
/**
60+
* {@inheritdoc}
61+
*/
62+
public function executeFull()
63+
{
64+
foreach ($this->indexerList as $indexerName) {
65+
$indexer = $this->indexerRegistry->get($indexerName);
66+
if (!$indexer->isScheduled()) {
67+
$indexer->reindexAll();
68+
}
69+
}
70+
}
71+
72+
/**
73+
* {@inheritdoc}
74+
*/
75+
public function executeList(array $ids)
76+
{
77+
if (!empty($ids)) {
78+
foreach ($this->indexerList as $indexerName) {
79+
$indexer = $this->indexerRegistry->get($indexerName);
80+
if (!$indexer->isScheduled()) {
81+
$indexer->reindexList($ids);
82+
}
83+
}
84+
}
85+
}
86+
87+
/**
88+
* {@inheritDoc}
89+
*/
90+
public function executeRow($id)
91+
{
92+
if (!empty($id)) {
93+
foreach ($this->indexerList as $indexerName) {
94+
$indexer = $this->indexerRegistry->get($indexerName);
95+
if (!$indexer->isScheduled()) {
96+
$indexer->reindexRow($id);
97+
}
98+
}
99+
}
100+
}
101+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento\Catalog\Test\Unit\Model\Indexer\Product;
8+
9+
use Magento\Catalog\Model\Indexer\Product\Full;
10+
use Magento\Framework\Indexer\IndexerInterface;
11+
use PHPUnit\Framework\TestCase;
12+
use Magento\Framework\Indexer\IndexerRegistry;
13+
use Magento\PageCache\Model\Config;
14+
use Magento\Framework\App\Cache\TypeListInterface;
15+
16+
class FullTest extends TestCase
17+
{
18+
/**
19+
* @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager
20+
*/
21+
private $objectManager;
22+
23+
/**
24+
* @var IndexerRegistry|\PHPUnit_Framework_MockObject_MockObject
25+
*/
26+
private $indexerRegistryMock;
27+
28+
/**
29+
* @var Config|\PHPUnit_Framework_MockObject_MockObject
30+
*/
31+
private $configMock;
32+
33+
/**
34+
* @var TypeListInterface|\PHPUnit_Framework_MockObject_MockObject
35+
*/
36+
private $typeListMock;
37+
38+
/**
39+
* @var Full
40+
*/
41+
private $full;
42+
43+
public function setUp()
44+
{
45+
$this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
46+
$this->indexerRegistryMock = $this->createMock(IndexerRegistry::class);
47+
$this->configMock = $this->createMock(Config::class);
48+
$this->typeListMock = $this->getMockForAbstractClass(TypeListInterface::class, [], "", false);
49+
50+
$this->full = $this->objectManager->getObject(
51+
Full::class,
52+
[
53+
'indexerRegistry' => $this->indexerRegistryMock,
54+
'pageCacheConfig' => $this->configMock,
55+
'cacheTypeList' => $this->typeListMock,
56+
'indexerList' => ['catalog_indexer', 'product_indexer', 'stock_indexer', 'search_indexer']
57+
]
58+
);
59+
}
60+
61+
public function testExecuteFull()
62+
{
63+
$indexerMock = $this->getMockForAbstractClass(IndexerInterface::class, [], "", false);
64+
$indexerMock->expects($this->exactly(4))->method('isScheduled')->willReturn(false);
65+
$indexerMock->expects($this->exactly(4))->method('reindexAll');
66+
$this->indexerRegistryMock->expects($this->exactly(4))->method('get')->willReturn($indexerMock);
67+
$this->configMock->expects($this->once())->method('isEnabled')->willReturn(true);
68+
$this->typeListMock->expects($this->once())->method('invalidate')->with('full_page');
69+
70+
$this->full->executeFull();
71+
}
72+
73+
public function testExecuteFullPageCacheDisabled()
74+
{
75+
$indexerMock = $this->getMockForAbstractClass(IndexerInterface::class, [], "", false);
76+
$indexerMock->expects($this->exactly(4))->method('isScheduled')->willReturn(false);
77+
$indexerMock->expects($this->exactly(4))->method('reindexAll');
78+
$this->indexerRegistryMock->expects($this->exactly(4))->method('get')->willReturn($indexerMock);
79+
$this->configMock->expects($this->once())->method('isEnabled')->willReturn(false);
80+
$this->typeListMock->expects($this->never())->method('invalidate');
81+
82+
$this->full->executeFull();
83+
}
84+
85+
public function testExecuteList()
86+
{
87+
$indexerMock = $this->getMockForAbstractClass(IndexerInterface::class, [], "", false);
88+
$indexerMock->expects($this->exactly(4))->method('isScheduled')->willReturn(false);
89+
$indexerMock->expects($this->exactly(4))->method('reindexList')->with([1, 2]);
90+
$this->indexerRegistryMock->expects($this->exactly(4))->method('get')->willReturn($indexerMock);
91+
$this->configMock->expects($this->once())->method('isEnabled')->willReturn(true);
92+
$this->typeListMock->expects($this->once())->method('invalidate')->with('full_page');
93+
94+
$this->full->executeList([1, 2]);
95+
}
96+
97+
public function testExecuteListPageCacheDisabled()
98+
{
99+
$indexerMock = $this->getMockForAbstractClass(IndexerInterface::class, [], "", false);
100+
$indexerMock->expects($this->exactly(4))->method('isScheduled')->willReturn(false);
101+
$indexerMock->expects($this->exactly(4))->method('reindexList')->with([1, 2]);
102+
$this->indexerRegistryMock->expects($this->exactly(4))->method('get')->willReturn($indexerMock);
103+
$this->configMock->expects($this->once())->method('isEnabled')->willReturn(false);
104+
$this->typeListMock->expects($this->never())->method('invalidate');
105+
106+
$this->full->executeList([1, 2]);
107+
}
108+
109+
public function testExecuteRow()
110+
{
111+
$indexerMock = $this->getMockForAbstractClass(IndexerInterface::class, [], "", false);
112+
$indexerMock->expects($this->exactly(4))->method('isScheduled')->willReturn(false);
113+
$indexerMock->expects($this->exactly(4))->method('reindexRow')->with(1);
114+
$this->indexerRegistryMock->expects($this->exactly(4))->method('get')->willReturn($indexerMock);
115+
$this->configMock->expects($this->once())->method('isEnabled')->willReturn(true);
116+
$this->typeListMock->expects($this->once())->method('invalidate')->with('full_page');
117+
118+
$this->full->executeRow(1);
119+
}
120+
121+
public function testExecuteRowPageCacheDisabled()
122+
{
123+
$indexerMock = $this->getMockForAbstractClass(IndexerInterface::class, [], "", false);
124+
$indexerMock->expects($this->exactly(4))->method('isScheduled')->willReturn(false);
125+
$indexerMock->expects($this->exactly(4))->method('reindexRow')->with(1);
126+
$this->indexerRegistryMock->expects($this->exactly(4))->method('get')->willReturn($indexerMock);
127+
$this->configMock->expects($this->once())->method('isEnabled')->willReturn(false);
128+
$this->typeListMock->expects($this->never())->method('invalidate');
129+
130+
$this->full->executeRow(1);
131+
}
132+
}

app/code/Magento/Catalog/etc/di.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,16 @@
8484
<argument name="changelog" xsi:type="object" shared="false">Magento\Framework\Mview\View\ChangelogInterface</argument>
8585
</arguments>
8686
</type>
87+
<type name="Magento\Catalog\Model\Indexer\Product\Full">
88+
<arguments>
89+
<argument name="indexerList" xsi:type="array">
90+
<item name="catalog_category_product" xsi:type="const">Magento\Catalog\Model\Indexer\Category\Product::INDEXER_ID</item>
91+
<item name="catalog_product_category" xsi:type="const">Magento\Catalog\Model\Indexer\Product\Category::INDEXER_ID</item>
92+
<item name="catalog_product_price" xsi:type="const">Magento\Catalog\Model\Indexer\Product\Price\Processor::INDEXER_ID</item>
93+
<item name="catalog_product_attribute" xsi:type="const">Magento\Catalog\Model\Indexer\Product\Eav\Processor::INDEXER_ID</item>
94+
</argument>
95+
</arguments>
96+
</type>
8797
<type name="Magento\Catalog\Model\Product\Attribute\Backend\Media\EntryConverterPool">
8898
<arguments>
8999
<argument name="mediaGalleryEntryConvertersCollection" xsi:type="array">

app/code/Magento/CatalogInventory/etc/di.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@
6060
<type name="Magento\Store\Model\ResourceModel\Group">
6161
<plugin name="storeGroupResourceAroundBeforeSave" type="Magento\CatalogInventory\Model\Indexer\Stock\Plugin\StoreGroup"/>
6262
</type>
63+
<type name="Magento\Catalog\Model\Indexer\Product\Full">
64+
<arguments>
65+
<argument name="indexerList" xsi:type="array">
66+
<item name="cataloginventory_stock" xsi:type="const">Magento\CatalogInventory\Model\Indexer\Stock\Processor::INDEXER_ID</item>
67+
</argument>
68+
</arguments>
69+
</type>
6370
<type name="Magento\Catalog\Block\Product\View">
6471
<plugin name="quantityValidators" type="Magento\CatalogInventory\Block\Plugin\ProductView" />
6572
</type>

app/code/Magento/CatalogSearch/etc/di.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@
3939
</arguments>
4040
</type>
4141
<preference for="Magento\Framework\Indexer\IndexStructureInterface" type="Magento\CatalogSearch\Model\Indexer\IndexStructureProxy" />
42+
<type name="Magento\Catalog\Model\Indexer\Product\Full">
43+
<arguments>
44+
<argument name="indexerList" xsi:type="array">
45+
<item name="catalogsearch_fulltext" xsi:type="const">Magento\CatalogSearch\Model\Indexer\Fulltext::INDEXER_ID</item>
46+
</argument>
47+
</arguments>
48+
</type>
4249
<type name="Magento\Framework\Search\Adapter\Mysql\Aggregation\DataProviderContainer">
4350
<arguments>
4451
<argument name="dataProviders" xsi:type="array">

app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,34 @@
77
namespace Magento\ConfigurableProduct\Plugin\Model\ResourceModel;
88

99
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
10+
use Magento\Framework\Indexer\ActionInterface;
1011

1112
class Product
1213
{
14+
/**
15+
* @var Configurable
16+
*/
17+
private $configurable;
18+
19+
/**
20+
* @var ActionInterface
21+
*/
22+
private $productIndexer;
23+
24+
/**
25+
* Initialize Product dependencies.
26+
*
27+
* @param Configurable $configurable
28+
* @param ActionInterface $productIndexer
29+
*/
30+
public function __construct(
31+
Configurable $configurable,
32+
ActionInterface $productIndexer
33+
) {
34+
$this->configurable = $configurable;
35+
$this->productIndexer = $productIndexer;
36+
}
37+
1338
/**
1439
* We need reset attribute set id to attribute after related simple product was saved
1540
*
@@ -28,4 +53,25 @@ public function beforeSave(
2853
$object->getTypeInstance()->getSetAttributes($object);
2954
}
3055
}
56+
57+
/**
58+
* Gather configurable parent ids of product being deleted and reindex after delete is complete.
59+
*
60+
* @param \Magento\Catalog\Model\ResourceModel\Product $subject
61+
* @param \Closure $proceed
62+
* @param \Magento\Catalog\Model\Product $product
63+
* @return \Magento\Catalog\Model\ResourceModel\Product
64+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
65+
*/
66+
public function aroundDelete(
67+
\Magento\Catalog\Model\ResourceModel\Product $subject,
68+
\Closure $proceed,
69+
\Magento\Catalog\Model\Product $product
70+
) {
71+
$configurableProductIds = $this->configurable->getParentIdsByChild($product->getId());
72+
$result = $proceed($product);
73+
$this->productIndexer->executeList($configurableProductIds);
74+
75+
return $result;
76+
}
3177
}

0 commit comments

Comments
 (0)