diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index c60953e33e9eb..5b50cc0ebd5e0 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -9,12 +9,14 @@ use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Config; use Magento\Catalog\Model\Product\Gallery\ReadHandler as GalleryReadHandler; use Magento\ConfigurableProduct\Model\Product\Type\Collection\SalableProcessor; use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Api\SearchCriteriaBuilder; /** * Configurable product type implementation @@ -194,9 +196,18 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType */ private $salableProcessor; + /** + * @var ProductAttributeRepositoryInterface|null + */ + private $productAttributeRepository; + + /** + * @var SearchCriteriaBuilder|null + */ + private $searchCriteriaBuilder; + /** * @codingStandardsIgnoreStart/End - * * @param \Magento\Catalog\Model\Product\Option $catalogProductOption * @param \Magento\Eav\Model\Config $eavConfig * @param \Magento\Catalog\Model\Product\Type $catalogProductType @@ -214,9 +225,13 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType * @param \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable $catalogProductTypeConfigurable * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor + * @param \Magento\Framework\Cache\FrontendInterface|null $cache + * @param \Magento\Customer\Model\Session|null $customerSession * @param \Magento\Framework\Serialize\Serializer\Json $serializer * @param ProductInterfaceFactory $productFactory * @param SalableProcessor $salableProcessor + * @param ProductAttributeRepositoryInterface|null $productAttributeRepository + * @param SearchCriteriaBuilder|null $searchCriteriaBuilder * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -241,7 +256,9 @@ public function __construct( \Magento\Customer\Model\Session $customerSession = null, \Magento\Framework\Serialize\Serializer\Json $serializer = null, ProductInterfaceFactory $productFactory = null, - SalableProcessor $salableProcessor = null + SalableProcessor $salableProcessor = null, + ProductAttributeRepositoryInterface $productAttributeRepository = null, + SearchCriteriaBuilder $searchCriteriaBuilder = null ) { $this->typeConfigurableFactory = $typeConfigurableFactory; $this->_eavAttributeFactory = $eavAttributeFactory; @@ -256,6 +273,10 @@ public function __construct( $this->productFactory = $productFactory ?: ObjectManager::getInstance() ->get(ProductInterfaceFactory::class); $this->salableProcessor = $salableProcessor ?: ObjectManager::getInstance()->get(SalableProcessor::class); + $this->productAttributeRepository = $productAttributeRepository ?: + ObjectManager::getInstance()->get(ProductAttributeRepositoryInterface::class); + $this->searchCriteriaBuilder = $searchCriteriaBuilder ?: + ObjectManager::getInstance()->get(SearchCriteriaBuilder::class); parent::__construct( $catalogProductOption, $eavConfig, @@ -1231,19 +1252,16 @@ public function isPossibleBuyFromList($product) /** * Returns array of sub-products for specified configurable product - * - * $requiredAttributeIds - one dimensional array, if provided * Result array contains all children for specified configurable product * * @param \Magento\Catalog\Model\Product $product - * @param array $requiredAttributeIds + * @param array $requiredAttributeIds Attributes to include in the select; one-dimensional array * @return ProductInterface[] - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getUsedProducts($product, $requiredAttributeIds = null) { if (!$product->hasData($this->_usedProducts)) { - $collection = $this->getConfiguredUsedProductCollection($product, false); + $collection = $this->getConfiguredUsedProductCollection($product, false, $requiredAttributeIds); $usedProducts = array_values($collection->getItems()); $product->setData($this->_usedProducts, $usedProducts); } @@ -1390,16 +1408,18 @@ private function getUsedProductsCacheKey($keyParts) /** * Prepare collection for retrieving sub-products of specified configurable product - * * Retrieve related products collection with additional configuration * * @param \Magento\Catalog\Model\Product $product * @param bool $skipStockFilter + * @param array $requiredAttributeIds Attributes to include in the select * @return \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection + * @throws \Magento\Framework\Exception\LocalizedException */ private function getConfiguredUsedProductCollection( \Magento\Catalog\Model\Product $product, - $skipStockFilter = true + $skipStockFilter = true, + $requiredAttributeIds = null ) { $collection = $this->getUsedProductCollection($product); @@ -1407,8 +1427,19 @@ private function getConfiguredUsedProductCollection( $collection->setFlag('has_stock_status_filter', true); } + $attributesForSelect = $this->getAttributesForCollection($product); + if ($requiredAttributeIds) { + $this->searchCriteriaBuilder->addFilter('attribute_id', $requiredAttributeIds, 'in'); + $requiredAttributes = $this->productAttributeRepository + ->getList($this->searchCriteriaBuilder->create())->getItems(); + $requiredAttributeCodes = []; + foreach ($requiredAttributes as $requiredAttribute) { + $requiredAttributeCodes[] = $requiredAttribute->getAttributeCode(); + } + $attributesForSelect = array_unique(array_merge($attributesForSelect, $requiredAttributeCodes)); + } $collection - ->addAttributeToSelect($this->getAttributesForCollection($product)) + ->addAttributeToSelect($attributesForSelect) ->addFilterByRequiredOptions() ->setStoreId($product->getStoreId()); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php index 78fa4733a2562..0d2043434d359 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/ConfigurableTest.php @@ -254,6 +254,33 @@ public function testGetUsedProducts() } } + /** + * Tests the $requiredAttributes parameter; uses meta_description as an example of an attribute that is not + * included in default attribute select. + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable_with_metadescription.php + */ + public function testGetUsedProductsWithRequiredAttributes() + { + $requiredAttributeIds = [86]; + $products = $this->model->getUsedProducts($this->product, $requiredAttributeIds); + foreach ($products as $product) { + self::assertNotNull($product->getData('meta_description')); + } + } + + /** + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable_with_metadescription.php + */ + public function testGetUsedProductsWithoutRequiredAttributes() + { + $products = $this->model->getUsedProducts($this->product); + foreach ($products as $product) { + self::assertNull($product->getData('meta_description')); + } + } + /** * Test getUsedProducts returns array with same indexes regardless collections was cache or not. * diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_metadescription.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_metadescription.php new file mode 100644 index 0000000000000..d0afeeaf19fe8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_metadescription.php @@ -0,0 +1,145 @@ +reinitialize(); + +require __DIR__ . '/configurable_attribute.php'; + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->create(ProductRepositoryInterface::class); + +/** @var $installer CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(CategorySetup::class); + +/* Create simple products per each option value*/ +/** @var AttributeOptionInterface[] $options */ +$options = $attribute->getOptions(); + +$attributeValues = []; +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); +$associatedProductIds = []; +$productIds = [10, 20]; +array_shift($options); //remove the first option which is empty + +foreach ($options as $option) { + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $productId = array_shift($productIds); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setId($productId) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Option' . $option->getLabel()) + ->setSku('simple_' . $productId) + ->setPrice($productId) + ->setTestConfigurable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setMetaDescription('meta_description' . $productId) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + + $product = $productRepository->save($product); + + /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ + $stockItem = Bootstrap::getObjectManager()->create(\Magento\CatalogInventory\Model\Stock\Item::class); + $stockItem->load($productId, 'product_id'); + + if (!$stockItem->getProductId()) { + $stockItem->setProductId($productId); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty(1000); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock(1); + $stockItem->save(); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); +} + +/** @var $product Product */ +$product = Bootstrap::getObjectManager()->create(Product::class); + +/** @var Factory $optionsFactory */ +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); + +$configurableAttributesData = [ + [ + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + 'values' => $attributeValues, + ], +]; + +$configurableOptions = $optionsFactory->create($configurableAttributesData); + +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); + +$product->setExtensionAttributes($extensionConfigurableAttributes); + +// Remove any previously created product with the same id. +/** @var \Magento\Framework\Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +try { + $productToDelete = $productRepository->getById(1); + $productRepository->delete($productToDelete); + + /** @var \Magento\Quote\Model\ResourceModel\Quote\Item $itemResource */ + $itemResource = Bootstrap::getObjectManager()->get(\Magento\Quote\Model\ResourceModel\Quote\Item::class); + $itemResource->getConnection()->delete( + $itemResource->getMainTable(), + 'product_id = ' . $productToDelete->getId() + ); +} catch (\Exception $e) { + // Nothing to remove +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +$product->setTypeId(Configurable::TYPE_CODE) + ->setId(1) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Product') + ->setSku('configurable') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); + +$productRepository->save($product); + +/** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ +$categoryLinkManagement = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); + +$categoryLinkManagement->assignProductToCategories( + $product->getSku(), + [2] +); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_metadescription_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_metadescription_rollback.php new file mode 100644 index 0000000000000..21953dea6f587 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_metadescription_rollback.php @@ -0,0 +1,39 @@ +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +foreach (['simple_10', 'simple_20', 'configurable'] as $sku) { + try { + $product = $productRepository->get($sku, true); + + $stockStatus = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Status::class); + $stockStatus->load($product->getEntityId(), 'product_id'); + $stockStatus->delete(); + + if ($product->getId()) { + $productRepository->delete($product); + } + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Product already removed + } +} + +require __DIR__ . '/configurable_attribute_rollback.php'; + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false);