diff --git a/app/code/Magento/Catalog/Model/Category.php b/app/code/Magento/Catalog/Model/Category.php index 571e4646f46a3d575eab73799a5be7c9dc3bd065..2002722684431cd63b392a7fb7480c30ff3188c3 100644 --- a/app/code/Magento/Catalog/Model/Category.php +++ b/app/code/Magento/Catalog/Model/Category.php @@ -943,8 +943,11 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements */ public function getProductCount() { - $count = $this->_getResource()->getProductCount($this); - $this->setData(self::KEY_PRODUCT_COUNT, $count); + if (!$this->hasData(self::KEY_PRODUCT_COUNT)) { + $count = $this->_getResource()->getProductCount($this); + $this->setData(self::KEY_PRODUCT_COUNT, $count); + } + return $this->getData(self::KEY_PRODUCT_COUNT); } diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php index c7ddb14a7649de19dadd57c25e4f4111e1992585..78fe3042b5f71661f2c77b5bed52d598f2e50586 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php @@ -8,9 +8,14 @@ namespace Magento\Catalog\Model\Indexer\Category\Product; -use Magento\Framework\DB\Query\Generator as QueryGenerator; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Query\Generator as QueryGenerator; +use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Store\Model\Store; /** * Class AbstractAction @@ -45,21 +50,21 @@ abstract class AbstractAction /** * Cached non anchor categories select by store id * - * @var \Magento\Framework\DB\Select[] + * @var Select[] */ protected $nonAnchorSelects = []; /** * Cached anchor categories select by store id * - * @var \Magento\Framework\DB\Select[] + * @var Select[] */ protected $anchorSelects = []; /** * Cached all product select by store id * - * @var \Magento\Framework\DB\Select[] + * @var Select[] */ protected $productsSelects = []; @@ -119,19 +124,21 @@ abstract class AbstractAction * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Catalog\Model\Config $config * @param QueryGenerator $queryGenerator + * @param MetadataPool|null $metadataPool */ public function __construct( \Magento\Framework\App\ResourceConnection $resource, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Catalog\Model\Config $config, - QueryGenerator $queryGenerator = null + QueryGenerator $queryGenerator = null, + MetadataPool $metadataPool = null ) { $this->resource = $resource; $this->connection = $resource->getConnection(); $this->storeManager = $storeManager; $this->config = $config; - $this->queryGenerator = $queryGenerator ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(QueryGenerator::class); + $this->queryGenerator = $queryGenerator ?: ObjectManager::getInstance()->get(QueryGenerator::class); + $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get(MetadataPool::class); } /** @@ -188,9 +195,9 @@ abstract class AbstractAction */ protected function getMainTmpTable() { - return $this->useTempTable ? $this->getTable( - self::MAIN_INDEX_TABLE . self::TEMPORARY_TABLE_SUFFIX - ) : $this->getMainTable(); + return $this->useTempTable + ? $this->getTable(self::MAIN_INDEX_TABLE . self::TEMPORARY_TABLE_SUFFIX) + : $this->getMainTable(); } /** @@ -218,24 +225,25 @@ abstract class AbstractAction /** * Retrieve select for reindex products of non anchor categories * - * @param \Magento\Store\Model\Store $store - * @return \Magento\Framework\DB\Select + * @param Store $store + * @return Select + * @throws \Exception when metadata not found for ProductInterface */ - protected function getNonAnchorCategoriesSelect(\Magento\Store\Model\Store $store) + protected function getNonAnchorCategoriesSelect(Store $store) { if (!isset($this->nonAnchorSelects[$store->getId()])) { $statusAttributeId = $this->config->getAttribute( - \Magento\Catalog\Model\Product::ENTITY, + Product::ENTITY, 'status' )->getId(); $visibilityAttributeId = $this->config->getAttribute( - \Magento\Catalog\Model\Product::ENTITY, + Product::ENTITY, 'visibility' )->getId(); $rootPath = $this->getPathFromCategoryId($store->getRootCategoryId()); - $metadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); $linkField = $metadata->getLinkField(); $select = $this->connection->select()->from( ['cc' => $this->getTable('catalog_category_entity')], @@ -304,12 +312,65 @@ abstract class AbstractAction ] ); + $this->addFilteringByChildProductsToSelect($select, $store); + $this->nonAnchorSelects[$store->getId()] = $select; } return $this->nonAnchorSelects[$store->getId()]; } + /** + * Add filtering by child products to select + * + * It's used for correct handling of composite products. + * This method makes assumption that select already joins `catalog_product_entity` as `cpe`. + * + * @param Select $select + * @param Store $store + * @return void + * @throws \Exception when metadata not found for ProductInterface + */ + private function addFilteringByChildProductsToSelect(Select $select, Store $store) + { + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $linkField = $metadata->getLinkField(); + + $statusAttributeId = $this->config->getAttribute(Product::ENTITY, 'status')->getId(); + + $select->joinLeft( + ['relation' => $this->getTable('catalog_product_relation')], + 'cpe.' . $linkField . ' = relation.parent_id', + [] + )->joinLeft( + ['relation_product_entity' => $this->getTable('catalog_product_entity')], + 'relation.child_id = relation_product_entity.entity_id', + [] + )->joinLeft( + ['child_cpsd' => $this->getTable('catalog_product_entity_int')], + 'child_cpsd.' . $linkField . ' = '. 'relation_product_entity.' . $linkField + . ' AND child_cpsd.store_id = 0' + . ' AND child_cpsd.attribute_id = ' . $statusAttributeId, + [] + )->joinLeft( + ['child_cpss' => $this->getTable('catalog_product_entity_int')], + 'child_cpss.' . $linkField . ' = '. 'relation_product_entity.' . $linkField . '' + . ' AND child_cpss.attribute_id = child_cpsd.attribute_id' + . ' AND child_cpss.store_id = ' . $store->getId(), + [] + )->where( + 'relation.child_id IS NULL OR ' + . $this->connection->getIfNullSql('child_cpss.value', 'child_cpsd.value') . ' = ?', + \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED + )->group( + [ + 'cc.entity_id', + 'ccp.product_id', + 'visibility' + ] + ); + } + /** * Check whether select ranging is needed * @@ -323,16 +384,13 @@ abstract class AbstractAction /** * Return selects cut by min and max * - * @param \Magento\Framework\DB\Select $select + * @param Select $select * @param string $field * @param int $range - * @return \Magento\Framework\DB\Select[] + * @return Select[] */ - protected function prepareSelectsByRange( - \Magento\Framework\DB\Select $select, - $field, - $range = self::RANGE_CATEGORY_STEP - ) { + protected function prepareSelectsByRange(Select $select, $field, $range = self::RANGE_CATEGORY_STEP) + { if ($this->isRangingNeeded()) { $iterator = $this->queryGenerator->generate( $field, @@ -353,10 +411,10 @@ abstract class AbstractAction /** * Reindex products of non anchor categories * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - protected function reindexNonAnchorCategories(\Magento\Store\Model\Store $store) + protected function reindexNonAnchorCategories(Store $store) { $selects = $this->prepareSelectsByRange($this->getNonAnchorCategoriesSelect($store), 'entity_id'); foreach ($selects as $select) { @@ -374,10 +432,10 @@ abstract class AbstractAction /** * Check if anchor select isset * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return bool */ - protected function hasAnchorSelect(\Magento\Store\Model\Store $store) + protected function hasAnchorSelect(Store $store) { return isset($this->anchorSelects[$store->getId()]); } @@ -385,19 +443,20 @@ abstract class AbstractAction /** * Create anchor select * - * @param \Magento\Store\Model\Store $store - * @return \Magento\Framework\DB\Select + * @param Store $store + * @return Select + * @throws \Exception when metadata not found for ProductInterface or CategoryInterface * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - protected function createAnchorSelect(\Magento\Store\Model\Store $store) + protected function createAnchorSelect(Store $store) { $isAnchorAttributeId = $this->config->getAttribute( \Magento\Catalog\Model\Category::ENTITY, 'is_anchor' )->getId(); - $statusAttributeId = $this->config->getAttribute(\Magento\Catalog\Model\Product::ENTITY, 'status')->getId(); + $statusAttributeId = $this->config->getAttribute(Product::ENTITY, 'status')->getId(); $visibilityAttributeId = $this->config->getAttribute( - \Magento\Catalog\Model\Product::ENTITY, + Product::ENTITY, 'visibility' )->getId(); $rootCatIds = explode('/', $this->getPathFromCategoryId($store->getRootCategoryId())); @@ -405,12 +464,12 @@ abstract class AbstractAction $temporaryTreeTable = $this->makeTempCategoryTreeIndex(); - $productMetadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); - $categoryMetadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\CategoryInterface::class); + $productMetadata = $this->metadataPool->getMetadata(ProductInterface::class); + $categoryMetadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\CategoryInterface::class); $productLinkField = $productMetadata->getLinkField(); $categoryLinkField = $categoryMetadata->getLinkField(); - return $this->connection->select()->from( + $select = $this->connection->select()->from( ['cc' => $this->getTable('catalog_category_entity')], [] )->joinInner( @@ -492,6 +551,10 @@ abstract class AbstractAction 'visibility' => new \Zend_Db_Expr($this->connection->getIfNullSql('cpvs.value', 'cpvd.value')), ] ); + + $this->addFilteringByChildProductsToSelect($select, $store); + + return $select; } /** @@ -586,10 +649,10 @@ abstract class AbstractAction /** * Retrieve select for reindex products of non anchor categories * - * @param \Magento\Store\Model\Store $store - * @return \Magento\Framework\DB\Select + * @param Store $store + * @return Select */ - protected function getAnchorCategoriesSelect(\Magento\Store\Model\Store $store) + protected function getAnchorCategoriesSelect(Store $store) { if (!$this->hasAnchorSelect($store)) { $this->anchorSelects[$store->getId()] = $this->createAnchorSelect($store); @@ -600,10 +663,10 @@ abstract class AbstractAction /** * Reindex products of anchor categories * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - protected function reindexAnchorCategories(\Magento\Store\Model\Store $store) + protected function reindexAnchorCategories(Store $store) { $selects = $this->prepareSelectsByRange($this->getAnchorCategoriesSelect($store), 'entity_id'); @@ -622,22 +685,23 @@ abstract class AbstractAction /** * Get select for all products * - * @param \Magento\Store\Model\Store $store - * @return \Magento\Framework\DB\Select + * @param Store $store + * @return Select + * @throws \Exception when metadata not found for ProductInterface */ - protected function getAllProducts(\Magento\Store\Model\Store $store) + protected function getAllProducts(Store $store) { if (!isset($this->productsSelects[$store->getId()])) { $statusAttributeId = $this->config->getAttribute( - \Magento\Catalog\Model\Product::ENTITY, + Product::ENTITY, 'status' )->getId(); $visibilityAttributeId = $this->config->getAttribute( - \Magento\Catalog\Model\Product::ENTITY, + Product::ENTITY, 'visibility' )->getId(); - $metadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); $linkField = $metadata->getLinkField(); $select = $this->connection->select()->from( @@ -726,10 +790,10 @@ abstract class AbstractAction /** * Reindex all products to root category * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return void */ - protected function reindexRootCategory(\Magento\Store\Model\Store $store) + protected function reindexRootCategory(Store $store) { if ($this->isIndexRootCategoryNeeded()) { $selects = $this->prepareSelectsByRange( @@ -750,16 +814,4 @@ abstract class AbstractAction } } } - - /** - * @return \Magento\Framework\EntityManager\MetadataPool - */ - private function getMetadataPool() - { - if (null === $this->metadataPool) { - $this->metadataPool = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\EntityManager\MetadataPool::class); - } - return $this->metadataPool; - } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php index 1b988534328e9971ecc38dec36d294a780a9b79d..99b087691ab09aa0b4b42a655d1efcc3890dfa1b 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php @@ -6,9 +6,19 @@ namespace Magento\Catalog\Model\Indexer\Product\Category\Action; use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Config; use Magento\Catalog\Model\Product; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Query\Generator as QueryGenerator; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Event\ManagerInterface as EventManagerInterface; use Magento\Framework\Indexer\CacheContext; +use Magento\Store\Model\StoreManagerInterface; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractAction { /** @@ -19,32 +29,102 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio protected $limitationByProducts; /** - * @var \Magento\Framework\Indexer\CacheContext + * @var CacheContext */ private $cacheContext; + /** + * @var EventManagerInterface|null + */ + private $eventManager; + + /** + * @param ResourceConnection $resource + * @param StoreManagerInterface $storeManager + * @param Config $config + * @param QueryGenerator|null $queryGenerator + * @param MetadataPool|null $metadataPool + * @param CacheContext|null $cacheContext + * @param EventManagerInterface|null $eventManager + */ + public function __construct( + ResourceConnection $resource, + StoreManagerInterface $storeManager, + Config $config, + QueryGenerator $queryGenerator = null, + MetadataPool $metadataPool = null, + CacheContext $cacheContext = null, + EventManagerInterface $eventManager = null + ) { + parent::__construct($resource, $storeManager, $config, $queryGenerator, $metadataPool); + $this->cacheContext = $cacheContext ?: ObjectManager::getInstance()->get(CacheContext::class); + $this->eventManager = $eventManager ?: ObjectManager::getInstance()->get(EventManagerInterface::class); + } + /** * Refresh entities index * * @param int[] $entityIds * @param bool $useTempTable * @return $this + * @throws \Exception if metadataPool doesn't contain metadata for ProductInterface + * @throws \DomainException */ public function execute(array $entityIds = [], $useTempTable = false) { - $this->limitationByProducts = $entityIds; + $idsToBeReIndexed = $this->getProductIdsWithParents($entityIds); + + $this->limitationByProducts = $idsToBeReIndexed; $this->useTempTable = $useTempTable; + $affectedCategories = $this->getCategoryIdsFromIndex($idsToBeReIndexed); + $this->removeEntries(); $this->reindex(); - $this->registerProducts($entityIds); - $this->registerCategories($entityIds); + $affectedCategories = array_merge($affectedCategories, $this->getCategoryIdsFromIndex($idsToBeReIndexed)); + + $this->registerProducts($idsToBeReIndexed); + $this->registerCategories($affectedCategories); + $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]); return $this; } + /** + * Get IDs of parent products by their child IDs. + * + * Returns identifiers of parent product from the catalog_product_relation. + * Please note that returned ids don't contain ids of passed child products. + * + * @param int[] $childProductIds + * @return int[] + * @throws \Exception if metadataPool doesn't contain metadata for ProductInterface + * @throws \DomainException + */ + private function getProductIdsWithParents(array $childProductIds) + { + /** @var \Magento\Framework\EntityManager\EntityMetadataInterface $metadata */ + $metadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); + $fieldForParent = $metadata->getLinkField(); + + $select = $this->connection + ->select() + ->from(['relation' => $this->getTable('catalog_product_relation')], []) + ->distinct(true) + ->where('child_id IN (?)', $childProductIds) + ->join( + ['cpe' => $this->getTable('catalog_product_entity')], + 'relation.parent_id = cpe.' . $fieldForParent, + ['cpe.entity_id'] + ); + + $parentProductIds = $this->connection->fetchCol($select); + + return array_unique(array_merge($childProductIds, $parentProductIds)); + } + /** * Register affected products * @@ -53,26 +133,19 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio */ private function registerProducts($entityIds) { - $this->getCacheContext()->registerEntities(Product::CACHE_TAG, $entityIds); + $this->cacheContext->registerEntities(Product::CACHE_TAG, $entityIds); } /** * Register categories assigned to products * - * @param array $entityIds + * @param array $categoryIds * @return void */ - private function registerCategories($entityIds) + private function registerCategories(array $categoryIds) { - $categories = $this->connection->fetchCol( - $this->connection->select() - ->from($this->getMainTable(), ['category_id']) - ->where('product_id IN (?)', $entityIds) - ->distinct() - ); - - if ($categories) { - $this->getCacheContext()->registerEntities(Category::CACHE_TAG, $categories); + if ($categoryIds) { + $this->cacheContext->registerEntities(Category::CACHE_TAG, $categoryIds); } } @@ -98,7 +171,7 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio protected function getNonAnchorCategoriesSelect(\Magento\Store\Model\Store $store) { $select = parent::getNonAnchorCategoriesSelect($store); - return $select->where('ccp.product_id IN (?)', $this->limitationByProducts); + return $select->where('ccp.product_id IN (?) OR relation.child_id IN (?)', $this->limitationByProducts); } /** @@ -136,16 +209,25 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio } /** - * Get cache context + * Returns a list of category ids which are assigned to product ids in the index * * @return \Magento\Framework\Indexer\CacheContext - * @deprecated 101.0.0 */ - private function getCacheContext() + private function getCategoryIdsFromIndex(array $productIds) { - if ($this->cacheContext === null) { - $this->cacheContext = \Magento\Framework\App\ObjectManager::getInstance()->get(CacheContext::class); + $categoryIds = $this->connection->fetchCol( + $this->connection->select() + ->from($this->getMainTable(), ['category_id']) + ->where('product_id IN (?)', $productIds) + ->distinct() + ); + $parentCategories = $categoryIds; + foreach ($categoryIds as $categoryId) { + $parentIds = explode('/', $this->getPathFromCategoryId($categoryId)); + $parentCategories = array_merge($parentCategories, $parentIds); } - return $this->cacheContext; + $categoryIds = array_unique($parentCategories); + + return $categoryIds; } } diff --git a/app/code/Magento/Catalog/view/frontend/templates/navigation/left.phtml b/app/code/Magento/Catalog/view/frontend/templates/navigation/left.phtml index fa70e1513557847339cf125e5d7f24a1b78a0e6f..01820361744e0aebc18ab15cf4545275e2c87804 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/navigation/left.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/navigation/left.phtml @@ -28,6 +28,7 @@ <dt><?= /* @escapeNotVerified */ __('Category') ?></dt> <dd> <ol class="items"> + <?php /** @var \Magento\Catalog\Model\Category $_category */ ?> <?php foreach ($_categories as $_category): ?> <?php if ($_category->getIsActive()): ?> <li class="item"> diff --git a/app/code/Magento/CatalogInventory/Helper/Stock.php b/app/code/Magento/CatalogInventory/Helper/Stock.php index 410e35096ee58c7ea1835ec48421f6999166ebf2..99a83753e43793f98ee2ba9cfbf465e3d845a2de 100644 --- a/app/code/Magento/CatalogInventory/Helper/Stock.php +++ b/app/code/Magento/CatalogInventory/Helper/Stock.php @@ -156,7 +156,7 @@ class Stock $resource = $this->getStockStatusResource(); $resource->addStockDataToCollection( $collection, - !$isShowOutOfStock || $collection->getFlag('require_stock_items') + !$isShowOutOfStock ); $collection->setFlag($stockFlag, true); } diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php index 9009a40c19dff3fc65772032b6480d92012d07a8..1aa3dba07fc78d8f8ec4cebea2969cc057fdb11b 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php @@ -123,11 +123,12 @@ class Fulltext implements \Magento\Framework\Indexer\ActionInterface, \Magento\F $saveHandler = $this->indexerHandlerFactory->create([ 'data' => $this->data ]); + foreach ($storeIds as $storeId) { $dimension = $this->dimensionFactory->create(['name' => 'scope', 'value' => $storeId]); $productIds = array_unique(array_merge($ids, $this->fulltextResource->getRelationsByChild($ids))); $saveHandler->deleteIndex([$dimension], new \ArrayObject($productIds)); - $saveHandler->saveIndex([$dimension], $this->fullAction->rebuildStoreIndex($storeId, $ids)); + $saveHandler->saveIndex([$dimension], $this->fullAction->rebuildStoreIndex($storeId, $productIds)); } } diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php index 07ff47610f3306b95f57f0287f948cf1988a41cc..98fb2528593e7fd917aee4d5ea4664ccafd1b9d7 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php @@ -377,9 +377,9 @@ class DataProvider public function getProductChildIds($productId, $typeId) { $typeInstance = $this->getProductTypeInstance($typeId); - $relation = $typeInstance->isComposite( - $this->getProductEmulator($typeId) - ) ? $typeInstance->getRelationInfo() : false; + $relation = $typeInstance->isComposite($this->getProductEmulator($typeId)) + ? $typeInstance->getRelationInfo() + : false; if ($relation && $relation->getTable() && $relation->getParentFieldName() && $relation->getChildFieldName()) { $select = $this->connection->select()->from( diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php index 639c0e8ca66f0a044880fda98ae0e3d642f8c8c8..5abcd5e7592e1a64ca8747469a6d7e43fa123ab3 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php @@ -5,6 +5,7 @@ */ namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Action; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\CatalogSearch\Model\Indexer\Fulltext; use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; @@ -297,27 +298,32 @@ class Full /** * Get parents IDs of product IDs to be re-indexed * + * @deprecated as it not used in the class anymore and duplicates another API method + * @see \Magento\CatalogSearch\Model\ResourceModel\Fulltext::getRelationsByChild() + * * @param int[] $entityIds * @return int[] + * @throws \Exception */ protected function getProductIdsFromParents(array $entityIds) { - /** @var \Magento\Framework\EntityManager\EntityMetadataInterface $metadata */ - $metadata = $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class); - $fieldForParent = $metadata->getLinkField(); + $connection = $this->connection; + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); - $select = $this->connection + $select = $connection ->select() - ->from(['relation' => $this->getTable('catalog_product_relation')], []) + ->from( + ['relation' => $this->getTable('catalog_product_relation')], + [] + ) ->distinct(true) ->where('child_id IN (?)', $entityIds) - ->where('parent_id NOT IN (?)', $entityIds) ->join( ['cpe' => $this->getTable('catalog_product_entity')], - 'relation.parent_id = cpe.' . $fieldForParent, + 'relation.parent_id = cpe.' . $linkField, ['cpe.entity_id'] ); - return $this->connection->fetchCol($select); + return $connection->fetchCol($select); } /** @@ -335,7 +341,7 @@ class Full public function rebuildStoreIndex($storeId, $productIds = null) { if ($productIds !== null) { - $productIds = array_unique(array_merge($productIds, $this->getProductIdsFromParents($productIds))); + $productIds = array_unique($productIds); } // prepare searchable attributes diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext.php index 15349e91c3fe9b9d1690d8b2eecf91c176def860..e9737d0aa059983d0b3daa1384800288e06e35ac 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext.php @@ -82,17 +82,20 @@ class Fulltext extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { $connection = $this->getConnection(); $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); - $select = $connection->select()->from( - ['relation' => $this->getTable('catalog_product_relation')], - [] - )->join( - ['cpe' => $this->getTable('catalog_product_entity')], - 'cpe.' . $linkField . ' = relation.parent_id', - ['cpe.entity_id'] - )->where( - 'relation.child_id IN (?)', - $childIds - )->distinct(true); + $select = $connection + ->select() + ->from( + ['relation' => $this->getTable('catalog_product_relation')], + [] + )->distinct(true) + ->join( + ['cpe' => $this->getTable('catalog_product_entity')], + 'cpe.' . $linkField . ' = relation.parent_id', + ['cpe.entity_id'] + )->where( + 'relation.child_id IN (?)', + $childIds + ); return $connection->fetchCol($select); } diff --git a/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/Constraint/AssertFilterProductList.php b/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/Constraint/AssertFilterProductList.php index ea80e5ac224808cb125054c8f69bb48455516ffd..19de61c698701525369af97ccadd01bb3e4ca3a7 100644 --- a/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/Constraint/AssertFilterProductList.php +++ b/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/Constraint/AssertFilterProductList.php @@ -3,7 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\LayeredNavigation\Test\Constraint; use Magento\Catalog\Test\Fixture\Category; diff --git a/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/Constraint/AssertProductsCount.php b/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/Constraint/AssertProductsCount.php new file mode 100644 index 0000000000000000000000000000000000000000..7c9a1f4faef1663af977b2fc3bbca99c012e13b7 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/Constraint/AssertProductsCount.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\LayeredNavigation\Test\Constraint; + +use Magento\Catalog\Test\Fixture\Category; +use Magento\Catalog\Test\Page\Category\CatalogCategoryView; +use Magento\Mtf\Client\BrowserInterface; +use Magento\Mtf\Constraint\AbstractConstraint; + +/** + * Assertion that category name and products qty are correct in category layered navigation + */ +class AssertProductsCount extends AbstractConstraint +{ + /** + * Browser instance. + * + * @var BrowserInterface + */ + private $browser; + + /** + * Catalog category view page. + * + * @var CatalogCategoryView $catalogCategoryView + */ + private $catalogCategoryView; + + /** + * Assert that category name and products cont in layered navigation are correct + * + * @param CatalogCategoryView $catalogCategoryView + * @param Category $category + * @param BrowserInterface $browser + * @param string $productsCount + * @return void + */ + public function processAssert( + CatalogCategoryView $catalogCategoryView, + Category $category, + BrowserInterface $browser, + $productsCount + ) { + $this->browser = $browser; + $this->catalogCategoryView = $catalogCategoryView; + while ($category) { + $parentCategory = $category->getDataFieldConfig('parent_id')['source']->getParentCategory(); + if ($parentCategory && $parentCategory->getData('is_anchor') == 'No') { + $this->openCategory($parentCategory); + \PHPUnit_Framework_Assert::assertTrue( + $this->catalogCategoryView->getLayeredNavigationBlock()->isCategoryVisible( + $category, + $productsCount + ), + 'Category ' . $category->getName() . ' is absent in Layered Navigation or products count is wrong' + ); + } + $category = $parentCategory; + } + } + + /** + * Open category. + * + * @param Category $category + * @return void + */ + private function openCategory(Category $category) + { + $categoryUrlKey = []; + + while ($category) { + $categoryUrlKey[] = $category->hasData('url_key') + ? strtolower($category->getUrlKey()) + : trim(strtolower(preg_replace('#[^0-9a-z%]+#i', '-', $category->getName())), '-'); + + $category = $category->getDataFieldConfig('parent_id')['source']->getParentCategory(); + if ($category !== null && 1 == $category->getParentId()) { + $category = null; + } + } + $categoryUrlKey = $_ENV['app_frontend_url'] . implode('/', array_reverse($categoryUrlKey)) . '.html'; + + $this->browser->open($categoryUrlKey); + } + + /** + * Assert success message that category is present in layered navigation and product is visible in product grid. + * + * @return string + */ + public function toString() + { + return 'Category is present in layered navigation and product is visible in product grid.'; + } +} diff --git a/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/Repository/Category.xml b/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/Repository/Category.xml new file mode 100644 index 0000000000000000000000000000000000000000..7799faf309ccfc3492feecf210cf2d981acdd568 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/Repository/Category.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" ?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/Magento/Mtf/Repository/etc/repository.xsd"> + <repository class="Magento\Catalog\Test\Repository\Category"> + <dataset name="default_non_anchored_subcategory"> + <field name="name" xsi:type="string">DefaultSubcategory%isolation%</field> + <field name="url_key" xsi:type="string">default-subcategory-%isolation%</field> + <field name="parent_id" xsi:type="array"> + <item name="dataset" xsi:type="string">default_category</item> + </field> + <field name="is_active" xsi:type="string">Yes</field> + <field name="is_anchor" xsi:type="string">No</field> + <field name="include_in_menu" xsi:type="string">Yes</field> + </dataset> + + <dataset name="default_second_level_anchored_subcategory"> + <field name="name" xsi:type="string">DefaultSubcategory%isolation%</field> + <field name="url_key" xsi:type="string">default-subcategory-%isolation%</field> + <field name="parent_id" xsi:type="array"> + <item name="dataset" xsi:type="string">default_non_anchored_subcategory</item> + </field> + <field name="is_active" xsi:type="string">Yes</field> + <field name="is_anchor" xsi:type="string">Yes</field> + <field name="include_in_menu" xsi:type="string">Yes</field> + </dataset> + + <dataset name="default_third_level_non_anchored_subcategory"> + <field name="name" xsi:type="string">DefaultSubcategory%isolation%</field> + <field name="url_key" xsi:type="string">default-subcategory-%isolation%</field> + <field name="parent_id" xsi:type="array"> + <item name="dataset" xsi:type="string">default_second_level_anchored_subcategory</item> + </field> + <field name="is_active" xsi:type="string">Yes</field> + <field name="is_anchor" xsi:type="string">No</field> + <field name="include_in_menu" xsi:type="string">Yes</field> + </dataset> + + <dataset name="default_fourth_level_anchored_subcategory"> + <field name="name" xsi:type="string">DefaultSubcategory%isolation%</field> + <field name="url_key" xsi:type="string">default-subcategory-%isolation%</field> + <field name="parent_id" xsi:type="array"> + <item name="dataset" xsi:type="string">default_third_level_non_anchored_subcategory</item> + </field> + <field name="is_active" xsi:type="string">Yes</field> + <field name="is_anchor" xsi:type="string">Yes</field> + <field name="include_in_menu" xsi:type="string">Yes</field> + </dataset> + </repository> +</config> diff --git a/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/TestCase/ProductsCountInLayeredNavigationTest.php b/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/TestCase/ProductsCountInLayeredNavigationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..179f9ef2579093184c9f452e9272a01b45900e50 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/TestCase/ProductsCountInLayeredNavigationTest.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\LayeredNavigation\Test\TestCase; + +use Magento\Catalog\Test\Fixture\Category; +use Magento\Mtf\TestCase\Injectable; +use Magento\Catalog\Test\Page\Adminhtml\CatalogProductEdit; +use Magento\Catalog\Test\Page\Adminhtml\CatalogProductIndex; +use Magento\Mtf\Fixture\FixtureFactory; + +/** + * Preconditions: + * 1. Create four categories assigned in ascending order (Default Category->first->second->third->fourth) + * first and third categories should not be anchored, second and fourth categories should be anchored + * 2. Create configurable product with two configurable options and assign it to category "fourth" + * + * Steps: + * 1. Disable configurable options via massaction or from edit product page + * 2. Open created non anchored categories on frontend + * 3. Perform assertions + * + * @group Layered_Navigation + * @ZephyrId MAGETWO-82891 + */ +class ProductsCountInLayeredNavigationTest extends Injectable +{ + /** + * Product page with a grid + * + * @var CatalogProductIndex + */ + protected $catalogProductIndex; + + /** + * Page to update a product + * + * @var CatalogProductEdit + */ + protected $editProductPage; + + /** + * Fixture factory + * + * @var FixtureFactory + */ + protected $fixtureFactory; + + /** + * Injection data + * + * @param CatalogProductIndex $catalogProductIndex + * @param CatalogProductEdit $editProductPage + * @param FixtureFactory $fixtureFactory + * @return void + */ + public function __inject( + CatalogProductIndex $catalogProductIndex, + CatalogProductEdit $editProductPage, + FixtureFactory $fixtureFactory + ) { + $this->catalogProductIndex = $catalogProductIndex; + $this->editProductPage = $editProductPage; + $this->fixtureFactory = $fixtureFactory; + } + + /** + * Test category name and products count displaying in layered navigation after configurable options disabling + * + * @param Category $category + * @param boolean $disableFromProductsGreed + * @return array + */ + public function test( + Category $category, + $disableFromProductsGreed = true + ) { + // Preconditions + $category->persist(); + // Steps + $products = $category->getDataFieldConfig('category_products')['source']->getProducts(); + $configurableOptions = []; + /** @var \Magento\ConfigurableProduct\Test\Fixture\ConfigurableProduct\ $product */ + foreach ($products as $product) { + $configurableOptions = array_merge( + $configurableOptions, + $product->getConfigurableAttributesData()['matrix'] + ); + } + // Disable configurable options + if ($disableFromProductsGreed) { + $this->catalogProductIndex->open(); + $this->catalogProductIndex->getProductGrid()->massaction( + array_map( + function ($assignedProduct) { + return ['sku' => $assignedProduct['sku']]; + }, + $configurableOptions + ), + ['Change status' => 'Disable'] + ); + } else { + $productToDisable = $this->fixtureFactory->createByCode( + 'catalogProductSimple', + ['data' => ['status' => 'No']] + ); + foreach ($configurableOptions as $configurableOption) { + $filter = ['sku' => $configurableOption['sku']]; + $this->catalogProductIndex->open(); + $this->catalogProductIndex->getProductGrid()->searchAndOpen($filter); + $this->editProductPage->getProductForm()->fill($productToDisable); + $this->editProductPage->getFormPageActions()->save(); + } + } + return [ + 'products' => $configurableOptions + ]; + } +} diff --git a/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/TestCase/ProductsCountInLayeredNavigationTest.xml b/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/TestCase/ProductsCountInLayeredNavigationTest.xml new file mode 100644 index 0000000000000000000000000000000000000000..41e18e64540970783c931e392c40c80c96c5c7f4 --- /dev/null +++ b/dev/tests/functional/tests/app/Magento/LayeredNavigation/Test/TestCase/ProductsCountInLayeredNavigationTest.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../vendor/magento/mtf/etc/variations.xsd"> + <testCase name="Magento\LayeredNavigation\Test\TestCase\ProductsCountInLayeredNavigationTest" summary="Check configurable products count in child categories of non-anchor category" ticketId="MAGETWO-82891"> + <variation name="ProductsCountInLayeredNavigationTestVariation1" summary="Check configurable products count with disabled by massaction products"> + <data name="runReindex" xsi:type="boolean">true</data> + <data name="flushCache" xsi:type="boolean">true</data> + <data name="category/dataset" xsi:type="string">default_fourth_level_anchored_subcategory</data> + <data name="category/data/category_products/dataset" xsi:type="string">configurableProduct::product_with_size</data> + <data name="productsCount" xsi:type="string">0</data> + <data name="disableFromProductsGreed" xsi:type="boolean">true</data> + <constraint name="Magento\LayeredNavigation\Test\Constraint\AssertProductsCount" /> + </variation> + <variation name="ProductsCountInLayeredNavigationTestVariation2" summary="Check configurable products count with disabled from edit page products"> + <data name="runReindex" xsi:type="boolean">true</data> + <data name="flushCache" xsi:type="boolean">true</data> + <data name="category/dataset" xsi:type="string">default_fourth_level_anchored_subcategory</data> + <data name="category/data/category_products/dataset" xsi:type="string">configurableProduct::product_with_size</data> + <data name="productsCount" xsi:type="string">0</data> + <data name="disableFromProductsGreed" xsi:type="boolean">false</data> + <constraint name="Magento\LayeredNavigation\Test\Constraint\AssertProductsCount" /> + </variation> + </testCase> +</config> diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/FulltextTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/FulltextTest.php index 29d1547f2d872f0764fb0ad5dbd10f8efcf02f9e..da2fc4498718b6b07342e9e2c37eced0e88f24b5 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/FulltextTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Model/Indexer/FulltextTest.php @@ -6,12 +6,14 @@ namespace Magento\CatalogSearch\Model\Indexer; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Visibility; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection; use Magento\TestFramework\Helper\Bootstrap; /** * @magentoDbIsolation disabled * @magentoDataFixture Magento/CatalogSearch/_files/indexer_fulltext.php + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class FulltextTest extends \PHPUnit\Framework\TestCase { @@ -186,13 +188,42 @@ class FulltextTest extends \PHPUnit\Framework\TestCase $this->assertCount(4, $products); } + /** + * Test the case when the last child product of the configurable becomes disabled/out of stock. + * + * Such behavior should enforce parent product to be deleted from the index as its latest child become unavailable + * and the configurable cannot be sold anymore. + * + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/CatalogSearch/_files/product_configurable_with_single_child.php + */ + public function testReindexParentProductWhenChildBeingDisabled() + { + $this->indexer->reindexAll(); + + $visibilityFilter = [ + Visibility::VISIBILITY_IN_SEARCH, + Visibility::VISIBILITY_IN_CATALOG, + Visibility::VISIBILITY_BOTH + ]; + $products = $this->search('Configurable', $visibilityFilter); + $this->assertCount(1, $products); + + $childProduct = $this->getProductBySku('configurable_option_single_child'); + $childProduct->setStatus(Product\Attribute\Source\Status::STATUS_DISABLED)->save(); + + $products = $this->search('Configurable', $visibilityFilter); + $this->assertCount(0, $products); + } + /** * Search the text and return result collection * * @param string $text + * @param null|array $visibilityFilter * @return Product[] */ - protected function search($text) + protected function search($text, $visibilityFilter = null) { $this->resourceFulltext->resetSearchResults(); $query = $this->queryFactory->get(); @@ -207,6 +238,10 @@ class FulltextTest extends \PHPUnit\Framework\TestCase ] ); $collection->addSearchFilter($text); + if (null !== $visibilityFilter) { + $collection->setVisibility($visibilityFilter); + } + foreach ($collection as $product) { $products[] = $product; } diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_configurable_with_single_child.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_configurable_with_single_child.php new file mode 100644 index 0000000000000000000000000000000000000000..5172ea94e53748ed708b6b268e8a79371b072be1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_configurable_with_single_child.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +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\CatalogInventory\Api\Data\StockItemInterface; +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; + +Bootstrap::getInstance()->reinitialize(); + +require __DIR__ . '/../../../Magento/ConfigurableProduct/_files/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'); +$productsSku = [1410]; +array_shift($options); //remove the first option which is empty + +$option = reset($options); + +/** @var $childProduct Product */ +$childProduct = Bootstrap::getObjectManager()->create(Product::class); +$productSku = array_shift($productsSku); +$childProduct->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($attributeSetId) + ->setName('Configurable Product Option' . $option->getLabel()) + ->setSku('configurable_option_single_child') + ->setPrice(11) + ->setTestConfigurable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED); +$childProduct = $productRepository->save($childProduct); + +/** @var StockItemInterface $stockItem */ +$stockItem = $childProduct->getExtensionAttributes()->getStockItem(); +$stockItem->setUseConfigManageStock(1)->setIsInStock(true)->setQty(100)->setIsQtyDecimal(0); + +$childProduct = $productRepository->save($childProduct); + +$attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), +]; + +/** @var $product Product */ +$configurableProduct = 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 = $configurableProduct->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks([$childProduct->getId()]); + +$configurableProduct->setExtensionAttributes($extensionConfigurableAttributes); + +$configurableProduct->setTypeId(Configurable::TYPE_CODE) + ->setAttributeSetId($attributeSetId) + ->setName('Configurable Product with single child') + ->setSku('configurable_with_single_child') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED); +$configurableProduct = $productRepository->save($configurableProduct); + +/** @var StockItemInterface $stockItem */ +$stockItem = $configurableProduct->getExtensionAttributes()->getStockItem(); +$stockItem->setUseConfigManageStock(1)->setIsInStock(1); + +$productRepository->save($configurableProduct); diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_configurable_with_single_child_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_configurable_with_single_child_rollback.php new file mode 100644 index 0000000000000000000000000000000000000000..85a72789e7fd3ae5941328fbe515ca17e64eb2b0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_configurable_with_single_child_rollback.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = 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 = Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +$productSkus = ['configurable_option_single_child', 'configurable_with_single_child']; +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = Bootstrap::getObjectManager()->get(SearchCriteriaBuilder::class); +$searchCriteriaBuilder->addFilter(ProductInterface::SKU, $productSkus, 'in'); +$result = $productRepository->getList($searchCriteriaBuilder->create()); +foreach ($result->getItems() as $product) { + $productRepository->delete($product); +} + +require __DIR__ . '/../../../Magento/Framework/Search/_files/configurable_attribute_rollback.php'; + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/lib/internal/Magento/Framework/Indexer/CacheContext.php b/lib/internal/Magento/Framework/Indexer/CacheContext.php index df0bed1dc1dcbb4bd78a824311e1b03f1ca93d7f..4a6964477ebd5cdd5121e17a0e3edf4d2b4656b5 100644 --- a/lib/internal/Magento/Framework/Indexer/CacheContext.php +++ b/lib/internal/Magento/Framework/Indexer/CacheContext.php @@ -29,15 +29,14 @@ class CacheContext implements \Magento\Framework\DataObject\IdentityInterface */ public function registerEntities($cacheTag, $ids) { - $this->entities[$cacheTag] = - array_merge($this->getRegisteredEntity($cacheTag), $ids); + $this->entities[$cacheTag] = array_merge($this->getRegisteredEntity($cacheTag), $ids); return $this; } /** * Register entity tags * - * @param string $cacheTag + * @param array $cacheTags * @return $this */ public function registerTags($cacheTags)