Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -1390,25 +1408,38 @@ 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);

if ($skipStockFilter) {
$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());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\Product\Attribute\Source\Status;
use Magento\Catalog\Model\Product\Type;
use Magento\Catalog\Model\Product\Visibility;
use Magento\Catalog\Setup\CategorySetup;
use Magento\ConfigurableProduct\Helper\Product\Options\Factory;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\Eav\Api\Data\AttributeOptionInterface;
use Magento\TestFramework\Helper\Bootstrap;

\Magento\TestFramework\Helper\Bootstrap::getInstance()->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]
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();

/** @var \Magento\Framework\Registry $registry */
$registry = $objectManager->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);