Skip to content

Commit e0c65b7

Browse files
ENGCOM-6015: Use attribute IDs when getting configurable child products if specified (#24483) #24875
2 parents 1b96ea1 + d82c334 commit e0c65b7

File tree

4 files changed

+252
-10
lines changed

4 files changed

+252
-10
lines changed

app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
use Magento\Catalog\Api\Data\ProductAttributeInterface;
1010
use Magento\Catalog\Api\Data\ProductInterface;
1111
use Magento\Catalog\Api\Data\ProductInterfaceFactory;
12+
use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
1213
use Magento\Catalog\Api\ProductRepositoryInterface;
1314
use Magento\Catalog\Model\Config;
1415
use Magento\Catalog\Model\Product\Gallery\ReadHandler as GalleryReadHandler;
1516
use Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor;
1617
use Magento\Framework\App\ObjectManager;
1718
use Magento\Framework\EntityManager\MetadataPool;
19+
use Magento\Framework\Api\SearchCriteriaBuilder;
1820

1921
/**
2022
* Configurable product type implementation
@@ -194,9 +196,18 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType
194196
*/
195197
private $salableProcessor;
196198

199+
/**
200+
* @var ProductAttributeRepositoryInterface|null
201+
*/
202+
private $productAttributeRepository;
203+
204+
/**
205+
* @var SearchCriteriaBuilder|null
206+
*/
207+
private $searchCriteriaBuilder;
208+
197209
/**
198210
* @codingStandardsIgnoreStart/End
199-
*
200211
* @param \Magento\Catalog\Model\Product\Option $catalogProductOption
201212
* @param \Magento\Eav\Model\Config $eavConfig
202213
* @param \Magento\Catalog\Model\Product\Type $catalogProductType
@@ -214,9 +225,13 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType
214225
* @param \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable $catalogProductTypeConfigurable
215226
* @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
216227
* @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor
228+
* @param \Magento\Framework\Cache\FrontendInterface|null $cache
229+
* @param \Magento\Customer\Model\Session|null $customerSession
217230
* @param \Magento\Framework\Serialize\Serializer\Json $serializer
218231
* @param ProductInterfaceFactory $productFactory
219232
* @param SalableProcessor $salableProcessor
233+
* @param ProductAttributeRepositoryInterface|null $productAttributeRepository
234+
* @param SearchCriteriaBuilder|null $searchCriteriaBuilder
220235
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
221236
*/
222237
public function __construct(
@@ -241,7 +256,9 @@ public function __construct(
241256
\Magento\Customer\Model\Session $customerSession = null,
242257
\Magento\Framework\Serialize\Serializer\Json $serializer = null,
243258
ProductInterfaceFactory $productFactory = null,
244-
SalableProcessor $salableProcessor = null
259+
SalableProcessor $salableProcessor = null,
260+
ProductAttributeRepositoryInterface $productAttributeRepository = null,
261+
SearchCriteriaBuilder $searchCriteriaBuilder = null
245262
) {
246263
$this->typeConfigurableFactory = $typeConfigurableFactory;
247264
$this->_eavAttributeFactory = $eavAttributeFactory;
@@ -256,6 +273,10 @@ public function __construct(
256273
$this->productFactory = $productFactory ?: ObjectManager::getInstance()
257274
->get(ProductInterfaceFactory::class);
258275
$this->salableProcessor = $salableProcessor ?: ObjectManager::getInstance()->get(SalableProcessor::class);
276+
$this->productAttributeRepository = $productAttributeRepository ?:
277+
ObjectManager::getInstance()->get(ProductAttributeRepositoryInterface::class);
278+
$this->searchCriteriaBuilder = $searchCriteriaBuilder ?:
279+
ObjectManager::getInstance()->get(SearchCriteriaBuilder::class);
259280
parent::__construct(
260281
$catalogProductOption,
261282
$eavConfig,
@@ -1231,19 +1252,16 @@ public function isPossibleBuyFromList($product)
12311252

12321253
/**
12331254
* Returns array of sub-products for specified configurable product
1234-
*
1235-
* $requiredAttributeIds - one dimensional array, if provided
12361255
* Result array contains all children for specified configurable product
12371256
*
12381257
* @param \Magento\Catalog\Model\Product $product
1239-
* @param array $requiredAttributeIds
1258+
* @param array $requiredAttributeIds Attributes to include in the select; one-dimensional array
12401259
* @return ProductInterface[]
1241-
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
12421260
*/
12431261
public function getUsedProducts($product, $requiredAttributeIds = null)
12441262
{
12451263
if (!$product->hasData($this->_usedProducts)) {
1246-
$collection = $this->getConfiguredUsedProductCollection($product, false);
1264+
$collection = $this->getConfiguredUsedProductCollection($product, false, $requiredAttributeIds);
12471265
$usedProducts = array_values($collection->getItems());
12481266
$product->setData($this->_usedProducts, $usedProducts);
12491267
}
@@ -1390,25 +1408,38 @@ private function getUsedProductsCacheKey($keyParts)
13901408

13911409
/**
13921410
* Prepare collection for retrieving sub-products of specified configurable product
1393-
*
13941411
* Retrieve related products collection with additional configuration
13951412
*
13961413
* @param \Magento\Catalog\Model\Product $product
13971414
* @param bool $skipStockFilter
1415+
* @param array $requiredAttributeIds Attributes to include in the select
13981416
* @return \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection
1417+
* @throws \Magento\Framework\Exception\LocalizedException
13991418
*/
14001419
private function getConfiguredUsedProductCollection(
14011420
\Magento\Catalog\Model\Product $product,
1402-
$skipStockFilter = true
1421+
$skipStockFilter = true,
1422+
$requiredAttributeIds = null
14031423
) {
14041424
$collection = $this->getUsedProductCollection($product);
14051425

14061426
if ($skipStockFilter) {
14071427
$collection->setFlag('has_stock_status_filter', true);
14081428
}
14091429

1430+
$attributesForSelect = $this->getAttributesForCollection($product);
1431+
if ($requiredAttributeIds) {
1432+
$this->searchCriteriaBuilder->addFilter('attribute_id', $requiredAttributeIds, 'in');
1433+
$requiredAttributes = $this->productAttributeRepository
1434+
->getList($this->searchCriteriaBuilder->create())->getItems();
1435+
$requiredAttributeCodes = [];
1436+
foreach ($requiredAttributes as $requiredAttribute) {
1437+
$requiredAttributeCodes[] = $requiredAttribute->getAttributeCode();
1438+
}
1439+
$attributesForSelect = array_unique(array_merge($attributesForSelect, $requiredAttributeCodes));
1440+
}
14101441
$collection
1411-
->addAttributeToSelect($this->getAttributesForCollection($product))
1442+
->addAttributeToSelect($attributesForSelect)
14121443
->addFilterByRequiredOptions()
14131444
->setStoreId($product->getStoreId());
14141445

dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,33 @@ public function testGetUsedProducts()
254254
}
255255
}
256256

257+
/**
258+
* Tests the $requiredAttributes parameter; uses meta_description as an example of an attribute that is not
259+
* included in default attribute select.
260+
* @magentoAppIsolation enabled
261+
* @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable_with_metadescription.php
262+
*/
263+
public function testGetUsedProductsWithRequiredAttributes()
264+
{
265+
$requiredAttributeIds = [86];
266+
$products = $this->model->getUsedProducts($this->product, $requiredAttributeIds);
267+
foreach ($products as $product) {
268+
self::assertNotNull($product->getData('meta_description'));
269+
}
270+
}
271+
272+
/**
273+
* @magentoAppIsolation enabled
274+
* @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable_with_metadescription.php
275+
*/
276+
public function testGetUsedProductsWithoutRequiredAttributes()
277+
{
278+
$products = $this->model->getUsedProducts($this->product);
279+
foreach ($products as $product) {
280+
self::assertNull($product->getData('meta_description'));
281+
}
282+
}
283+
257284
/**
258285
* Test getUsedProducts returns array with same indexes regardless collections was cache or not.
259286
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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+
use Magento\Catalog\Api\ProductRepositoryInterface;
10+
use Magento\Catalog\Model\Product;
11+
use Magento\Catalog\Model\Product\Attribute\Source\Status;
12+
use Magento\Catalog\Model\Product\Type;
13+
use Magento\Catalog\Model\Product\Visibility;
14+
use Magento\Catalog\Setup\CategorySetup;
15+
use Magento\ConfigurableProduct\Helper\Product\Options\Factory;
16+
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
17+
use Magento\Eav\Api\Data\AttributeOptionInterface;
18+
use Magento\TestFramework\Helper\Bootstrap;
19+
20+
\Magento\TestFramework\Helper\Bootstrap::getInstance()->reinitialize();
21+
22+
require __DIR__ . '/configurable_attribute.php';
23+
24+
/** @var ProductRepositoryInterface $productRepository */
25+
$productRepository = Bootstrap::getObjectManager()
26+
->create(ProductRepositoryInterface::class);
27+
28+
/** @var $installer CategorySetup */
29+
$installer = Bootstrap::getObjectManager()->create(CategorySetup::class);
30+
31+
/* Create simple products per each option value*/
32+
/** @var AttributeOptionInterface[] $options */
33+
$options = $attribute->getOptions();
34+
35+
$attributeValues = [];
36+
$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default');
37+
$associatedProductIds = [];
38+
$productIds = [10, 20];
39+
array_shift($options); //remove the first option which is empty
40+
41+
foreach ($options as $option) {
42+
/** @var $product Product */
43+
$product = Bootstrap::getObjectManager()->create(Product::class);
44+
$productId = array_shift($productIds);
45+
$product->setTypeId(Type::TYPE_SIMPLE)
46+
->setId($productId)
47+
->setAttributeSetId($attributeSetId)
48+
->setWebsiteIds([1])
49+
->setName('Configurable Option' . $option->getLabel())
50+
->setSku('simple_' . $productId)
51+
->setPrice($productId)
52+
->setTestConfigurable($option->getValue())
53+
->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE)
54+
->setStatus(Status::STATUS_ENABLED)
55+
->setMetaDescription('meta_description' . $productId)
56+
->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]);
57+
58+
$product = $productRepository->save($product);
59+
60+
/** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */
61+
$stockItem = Bootstrap::getObjectManager()->create(\Magento\CatalogInventory\Model\Stock\Item::class);
62+
$stockItem->load($productId, 'product_id');
63+
64+
if (!$stockItem->getProductId()) {
65+
$stockItem->setProductId($productId);
66+
}
67+
$stockItem->setUseConfigManageStock(1);
68+
$stockItem->setQty(1000);
69+
$stockItem->setIsQtyDecimal(0);
70+
$stockItem->setIsInStock(1);
71+
$stockItem->save();
72+
73+
$attributeValues[] = [
74+
'label' => 'test',
75+
'attribute_id' => $attribute->getId(),
76+
'value_index' => $option->getValue(),
77+
];
78+
$associatedProductIds[] = $product->getId();
79+
}
80+
81+
/** @var $product Product */
82+
$product = Bootstrap::getObjectManager()->create(Product::class);
83+
84+
/** @var Factory $optionsFactory */
85+
$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class);
86+
87+
$configurableAttributesData = [
88+
[
89+
'attribute_id' => $attribute->getId(),
90+
'code' => $attribute->getAttributeCode(),
91+
'label' => $attribute->getStoreLabel(),
92+
'position' => '0',
93+
'values' => $attributeValues,
94+
],
95+
];
96+
97+
$configurableOptions = $optionsFactory->create($configurableAttributesData);
98+
99+
$extensionConfigurableAttributes = $product->getExtensionAttributes();
100+
$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions);
101+
$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds);
102+
103+
$product->setExtensionAttributes($extensionConfigurableAttributes);
104+
105+
// Remove any previously created product with the same id.
106+
/** @var \Magento\Framework\Registry $registry */
107+
$registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class);
108+
$registry->unregister('isSecureArea');
109+
$registry->register('isSecureArea', true);
110+
try {
111+
$productToDelete = $productRepository->getById(1);
112+
$productRepository->delete($productToDelete);
113+
114+
/** @var \Magento\Quote\Model\ResourceModel\Quote\Item $itemResource */
115+
$itemResource = Bootstrap::getObjectManager()->get(\Magento\Quote\Model\ResourceModel\Quote\Item::class);
116+
$itemResource->getConnection()->delete(
117+
$itemResource->getMainTable(),
118+
'product_id = ' . $productToDelete->getId()
119+
);
120+
} catch (\Exception $e) {
121+
// Nothing to remove
122+
}
123+
$registry->unregister('isSecureArea');
124+
$registry->register('isSecureArea', false);
125+
126+
$product->setTypeId(Configurable::TYPE_CODE)
127+
->setId(1)
128+
->setAttributeSetId($attributeSetId)
129+
->setWebsiteIds([1])
130+
->setName('Configurable Product')
131+
->setSku('configurable')
132+
->setVisibility(Visibility::VISIBILITY_BOTH)
133+
->setStatus(Status::STATUS_ENABLED)
134+
->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]);
135+
136+
$productRepository->save($product);
137+
138+
/** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */
139+
$categoryLinkManagement = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
140+
->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class);
141+
142+
$categoryLinkManagement->assignProductToCategories(
143+
$product->getSku(),
144+
[2]
145+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
9+
10+
/** @var \Magento\Framework\Registry $registry */
11+
$registry = $objectManager->get(\Magento\Framework\Registry::class);
12+
13+
$registry->unregister('isSecureArea');
14+
$registry->register('isSecureArea', true);
15+
16+
/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */
17+
$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
18+
->get(\Magento\Catalog\Api\ProductRepositoryInterface::class);
19+
20+
foreach (['simple_10', 'simple_20', 'configurable'] as $sku) {
21+
try {
22+
$product = $productRepository->get($sku, true);
23+
24+
$stockStatus = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Status::class);
25+
$stockStatus->load($product->getEntityId(), 'product_id');
26+
$stockStatus->delete();
27+
28+
if ($product->getId()) {
29+
$productRepository->delete($product);
30+
}
31+
} catch (\Magento\Framework\Exception\NoSuchEntityException $e) {
32+
//Product already removed
33+
}
34+
}
35+
36+
require __DIR__ . '/configurable_attribute_rollback.php';
37+
38+
$registry->unregister('isSecureArea');
39+
$registry->register('isSecureArea', false);

0 commit comments

Comments
 (0)