From eb4f3b6469f82c02fe6b6edf1ab1000c3be22a9d Mon Sep 17 00:00:00 2001 From: Roger Date: Wed, 7 Sep 2022 08:51:18 +0000 Subject: [PATCH 1/3] Issue 36096: Layered Navigation does not filter products properly --- .../Model/ResourceModel/Index.php | 73 ++++++++- .../Collection/SearchResultApplier.php | 139 +----------------- .../SearchAdapter/Query/Builder/Sort.php | 49 +++++- app/code/Magento/Elasticsearch/etc/di.xml | 1 + 4 files changed, 122 insertions(+), 140 deletions(-) diff --git a/app/code/Magento/AdvancedSearch/Model/ResourceModel/Index.php b/app/code/Magento/AdvancedSearch/Model/ResourceModel/Index.php index 420f7b1e0e72a..0a08a7a9ff1c7 100644 --- a/app/code/Magento/AdvancedSearch/Model/ResourceModel/Index.php +++ b/app/code/Magento/AdvancedSearch/Model/ResourceModel/Index.php @@ -3,20 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\AdvancedSearch\Model\ResourceModel; -use Magento\Framework\Model\ResourceModel\Db\AbstractDb; -use Magento\Framework\Search\Request\IndexScopeResolverInterface; -use Magento\Store\Model\StoreManagerInterface; -use Magento\Framework\Model\ResourceModel\Db\Context; -use Magento\Framework\EntityManager\MetadataPool; use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Indexer\Category\Product\AbstractAction; +use Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory; use Magento\Framework\App\ObjectManager; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; +use Magento\Framework\Model\ResourceModel\Db\Context; use Magento\Framework\Search\Request\Dimension; -use Magento\Catalog\Model\Indexer\Category\Product\AbstractAction; +use Magento\Framework\Search\Request\IndexScopeResolverInterface; use Magento\Framework\Search\Request\IndexScopeResolverInterface as TableResolver; -use Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory; use Magento\Store\Model\Indexer\WebsiteDimensionProvider; +use Magento\Store\Model\StoreManagerInterface; /** * @api @@ -150,6 +152,63 @@ public function getPriceIndexData($productIds, $storeId) return $priceProductsIndexData[$websiteId]; } + /** + * Return array of inventory data products + * + * @param null|array $productIds + * @return array + * @since 100.1.0 + */ + protected function _getCatalogProductInventoryData($productIds = null) + { + $connection = $this->getConnection(); + $catalogProductIndexInventorySelect = []; + + foreach ($this->dimensionCollectionFactory->create() as $dimensions) { + if (!isset($dimensions[WebsiteDimensionProvider::DIMENSION_NAME]) || + $this->websiteId === null || + $dimensions[WebsiteDimensionProvider::DIMENSION_NAME]->getValue() === $this->websiteId) { + $select = $connection->select()->from( + $this->tableResolver->resolve('cataloginventory_stock_status', $dimensions), + ['product_id', 'website_id', 'stock_status'] + ); + if ($productIds) { + $select->where('product_id IN (?)', $productIds); + } + $catalogProductIndexInventorySelect[] = $select; + } + } + + $catalogProductIndexInventoryUnionSelect = $connection->select()->union($catalogProductIndexInventorySelect); + + $result = []; + foreach ($connection->fetchAll($catalogProductIndexInventoryUnionSelect) as $row) { + $result[$row['website_id']][$row['product_id']] = (int) $row['stock_status']; + } + + return $result; + } + + /** + * Retrieve inventory data for product + * + * @param null|array $productIds + * @param int $websiteId + * @return array + */ + public function getInventoryIndexData($productIds, $websiteId = null) + { + $this->websiteId = $websiteId; + $inventoryProductsIndexData = $this->_getCatalogProductInventoryData($productIds); + $this->websiteId = null; + + if (!isset($inventoryProductsIndexData[(int) $websiteId])) { + return []; + } + + return $inventoryProductsIndexData[(int) $websiteId]; + } + /** * Prepare system index data for products. * diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php index 97cb92ab3b06d..4f4b8b5737280 100644 --- a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php +++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php @@ -3,12 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection; -use Magento\Catalog\Api\Data\ProductInterface; -use Magento\CatalogInventory\Model\StockStatusApplierInterface; use Magento\CatalogInventory\Model\ResourceModel\StockStatusFilterInterface; +use Magento\CatalogInventory\Model\StockStatusApplierInterface; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; use Magento\Framework\Api\Search\SearchResultInterface; use Magento\Framework\App\Config\ScopeConfigInterface; @@ -105,14 +105,12 @@ public function apply() return; } - $ids = $this->getProductIdsBySaleability(); - - if (count($ids) == 0) { - $items = $this->sliceItems($this->searchResult->getItems(), $this->size, $this->currentPage); - foreach ($items as $item) { - $ids[] = (int)$item->getId(); - } + $ids = []; + $items = $this->sliceItems($this->searchResult->getItems(), $this->size, $this->currentPage); + foreach ($items as $item) { + $ids[] = (int)$item->getId(); } + $orderList = implode(',', $ids); $this->collection->getSelect() ->where('e.entity_id IN (?)', $ids) @@ -160,127 +158,4 @@ private function getOffset(int $pageNumber, int $pageSize): int { return ($pageNumber - 1) * $pageSize; } - /** - * Fetch filtered product ids sorted by the saleability and other applied sort orders - * - * @return array - */ - private function getProductIdsBySaleability(): array - { - $ids = []; - - if (!$this->hasShowOutOfStockStatus()) { - return $ids; - } - - if ($this->collection->getFlag('has_stock_status_filter') - || $this->collection->getFlag('has_category_filter')) { - $categoryId = null; - $searchCriteria = $this->searchResult->getSearchCriteria(); - foreach ($searchCriteria->getFilterGroups() as $filterGroup) { - foreach ($filterGroup->getFilters() as $filter) { - if ($filter->getField() === 'category_ids') { - $categoryId = $filter->getValue(); - break 2; - } - } - } - - if ($categoryId) { - $resultSet = $this->categoryProductByCustomSortOrder($categoryId); - foreach ($resultSet as $item) { - $ids[] = (int)$item['entity_id']; - } - } - } - - return $ids; - } - - /** - * Fetch product resultset by custom sort orders - * - * @param int $categoryId - * @return array - * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Exception - */ - private function categoryProductByCustomSortOrder(int $categoryId): array - { - $storeId = $this->collection->getStoreId(); - $searchCriteria = $this->searchResult->getSearchCriteria(); - $sortOrders = $searchCriteria->getSortOrders() ?? []; - $sortOrders = array_merge(['is_salable' => \Magento\Framework\DB\Select::SQL_DESC], $sortOrders); - - $connection = $this->collection->getConnection(); - $query = clone $connection->select() - ->reset(\Magento\Framework\DB\Select::ORDER) - ->reset(\Magento\Framework\DB\Select::LIMIT_COUNT) - ->reset(\Magento\Framework\DB\Select::LIMIT_OFFSET) - ->reset(\Magento\Framework\DB\Select::COLUMNS); - $query->from( - ['e' => $this->collection->getTable('catalog_product_entity')], - ['e.entity_id'] - ); - $this->stockStatusApplier->setSearchResultApplier(true); - $query = $this->stockStatusFilter->execute($query, 'e', 'stockItem'); - $query->join( - ['cat_index' => $this->collection->getTable('catalog_category_product_index_store' . $storeId)], - 'cat_index.product_id = e.entity_id' - . ' AND cat_index.category_id = ' . $categoryId - . ' AND cat_index.store_id = ' . $storeId, - ['cat_index.position'] - ); - foreach ($sortOrders as $field => $dir) { - if ($field === 'name') { - $entityTypeId = $this->collection->getEntity()->getTypeId(); - $entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class); - $linkField = $entityMetadata->getLinkField(); - $query->joinLeft( - ['product_var' => $this->collection->getTable('catalog_product_entity_varchar')], - "product_var.{$linkField} = e.{$linkField} AND product_var.attribute_id = - (SELECT attribute_id FROM eav_attribute WHERE entity_type_id={$entityTypeId} - AND attribute_code='name')", - ['product_var.value AS name'] - ); - } elseif ($field === 'price') { - $query->joinLeft( - ['price_index' => $this->collection->getTable('catalog_product_index_price')], - 'price_index.entity_id = e.entity_id' - . ' AND price_index.customer_group_id = 0' - . ' AND price_index.website_id = (Select website_id FROM store WHERE store_id = ' - . $storeId . ')', - ['price_index.max_price AS price'] - ); - } - $columnFilters = []; - $columnsParts = $query->getPart('columns'); - foreach ($columnsParts as $columns) { - $columnFilters[] = $columns[2] ?? $columns[1]; - } - if (in_array($field, $columnFilters, true)) { - $query->order(new \Zend_Db_Expr("{$field} {$dir}")); - } - } - - $query->limit( - $searchCriteria->getPageSize(), - $searchCriteria->getCurrentPage() * $searchCriteria->getPageSize() - ); - - return $connection->fetchAssoc($query) ?? []; - } - - /** - * Returns if display out of stock status set or not in catalog inventory - * - * @return bool - */ - private function hasShowOutOfStockStatus(): bool - { - return (bool) $this->scopeConfig->getValue( - \Magento\CatalogInventory\Model\Configuration::XML_PATH_SHOW_OUT_OF_STOCK, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - } } diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php index 7d41d54fb22a5..e58a894ba92cd 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php @@ -1,8 +1,10 @@ attributeAdapterProvider = $attributeAdapterProvider; $this->fieldNameResolver = $fieldNameResolver; + $this->scopeConfig = $scopeConfig; $this->skippedFields = array_merge(self::DEFAULT_SKIPPED_FIELDS, $skippedFields); $this->map = array_merge(self::DEFAULT_MAP, $map); } @@ -80,7 +91,8 @@ public function __construct( */ public function getSort(RequestInterface $request) { - $sorts = []; + $sorts = $this->getSortBySaleability(); + /** * Temporary solution for an existing interface of a fulltext search request in Backward compatibility purposes. * Scope to split Search request interface on two different 'Search' and 'Fulltext Search' contains in MC-16461. @@ -124,4 +136,39 @@ public function getSort(RequestInterface $request) return $sorts; } + + /** + * Prepare sort by saleability. + * + * @return array + */ + public function getSortBySaleability() + { + $sorts = []; + + if ($this->hasShowOutOfStockStatus()) { + $attribute = $this->attributeAdapterProvider->getByAttributeCode('is_salable'); + $fieldName = $this->fieldNameResolver->getFieldName($attribute); + $sorts[] = [ + $fieldName => [ + 'order' => 'desc' + ] + ]; + } + + return $sorts; + } + + /** + * Returns if display out of stock status set or not in catalog inventory + * + * @return bool + */ + private function hasShowOutOfStockStatus(): bool + { + return (bool) $this->scopeConfig->getValue( + \Magento\CatalogInventory\Model\Configuration::XML_PATH_SHOW_OUT_OF_STOCK, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + } } diff --git a/app/code/Magento/Elasticsearch/etc/di.xml b/app/code/Magento/Elasticsearch/etc/di.xml index 95aec47dbf1f6..78174d36efe96 100644 --- a/app/code/Magento/Elasticsearch/etc/di.xml +++ b/app/code/Magento/Elasticsearch/etc/di.xml @@ -148,6 +148,7 @@ Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper\CategoryFieldsProviderProxy Magento\Elasticsearch\Model\Adapter\BatchDataMapper\PriceFieldsProvider + Magento\Elasticsearch\Model\Adapter\BatchDataMapper\InventoryFieldsProvider From 4863da34a32da3a78307257b13f57ac1153e63a9 Mon Sep 17 00:00:00 2001 From: Roger Date: Mon, 12 Sep 2022 02:33:37 +0000 Subject: [PATCH 2/3] issue 36096: update code --- .../Model/ResourceModel/Index.php | 4 +- .../InventoryFieldsProvider.php | 122 ++++++++++++++++ .../Collection/SearchResultApplier.php | 133 +++++++++++++++++- .../SearchAdapter/Query/Builder/Sort.php | 17 ++- 4 files changed, 266 insertions(+), 10 deletions(-) create mode 100644 app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/InventoryFieldsProvider.php diff --git a/app/code/Magento/AdvancedSearch/Model/ResourceModel/Index.php b/app/code/Magento/AdvancedSearch/Model/ResourceModel/Index.php index 0a08a7a9ff1c7..cacebfe600765 100644 --- a/app/code/Magento/AdvancedSearch/Model/ResourceModel/Index.php +++ b/app/code/Magento/AdvancedSearch/Model/ResourceModel/Index.php @@ -159,7 +159,7 @@ public function getPriceIndexData($productIds, $storeId) * @return array * @since 100.1.0 */ - protected function _getCatalogProductInventoryData($productIds = null) + private function getCatalogProductInventoryData($productIds = null) { $connection = $this->getConnection(); $catalogProductIndexInventorySelect = []; @@ -199,7 +199,7 @@ protected function _getCatalogProductInventoryData($productIds = null) public function getInventoryIndexData($productIds, $websiteId = null) { $this->websiteId = $websiteId; - $inventoryProductsIndexData = $this->_getCatalogProductInventoryData($productIds); + $inventoryProductsIndexData = $this->getCatalogProductInventoryData($productIds); $this->websiteId = null; if (!isset($inventoryProductsIndexData[(int) $websiteId])) { diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/InventoryFieldsProvider.php b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/InventoryFieldsProvider.php new file mode 100644 index 0000000000000..e07e8ec892977 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/InventoryFieldsProvider.php @@ -0,0 +1,122 @@ +resourceIndex = $resourceIndex; + $this->storeManager = $storeManager; + $this->attributeAdapterProvider = $attributeAdapterProvider; + $this->fieldNameResolver = $fieldNameResolver; + $this->scopeConfig = $scopeConfig; + } + + /** + * @inheritdoc + */ + public function getFields(array $productIds, $storeId) + { + $fields = []; + + if ($this->hasShowOutOfStockStatus()) { + $websiteId = $this->storeManager->getStore($storeId)->getWebsiteId(); + $inventoryData = $this->resourceIndex->getInventoryIndexData($productIds); + foreach ($productIds as $productId) { + $fields[$productId] = $this->getProductInventoryData($productId, $websiteId, $inventoryData); + } + } + return $fields; + } + + /** + * Prepare inventory index for product. + * + * @param int $productId + * @param int $websiteId + * @param array $inventoryData + * @return array + */ + private function getProductInventoryData($productId, $websiteId, array $inventoryData) + { + $result = []; + if (array_key_exists($productId, $inventoryData)) { + $inStockAttribute = $this->attributeAdapterProvider->getByAttributeCode(self::IS_SALABLE); + $fieldName = $this->fieldNameResolver->getFieldName( + $inStockAttribute, + ['websiteId' => $websiteId] + ); + $result[$fieldName] = $inventoryData[$productId]; + } + + return $result; + } + + /** + * Returns if display out of stock status set or not in catalog inventory + * + * @return bool + */ + private function hasShowOutOfStockStatus(): bool + { + return (bool) $this->scopeConfig->getValue( + \Magento\CatalogInventory\Model\Configuration::XML_PATH_SHOW_OUT_OF_STOCK, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + } +} diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php index 4f4b8b5737280..8933e03be1c88 100644 --- a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php +++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php @@ -7,6 +7,7 @@ namespace Magento\Elasticsearch\Model\ResourceModel\Fulltext\Collection; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\CatalogInventory\Model\ResourceModel\StockStatusFilterInterface; use Magento\CatalogInventory\Model\StockStatusApplierInterface; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; @@ -137,7 +138,7 @@ private function sliceItems(array $items, int $size, int $currentPage): array $currentPage = 1; } if ($currentPage > $maxAllowedPageNumber) { - $currentPage = $maxAllowedPageNumber; + $currentPage = (int) $maxAllowedPageNumber; } $offset = $this->getOffset($currentPage, $size); @@ -158,4 +159,134 @@ private function getOffset(int $pageNumber, int $pageSize): int { return ($pageNumber - 1) * $pageSize; } + /** + * Fetch filtered product ids sorted by the saleability and other applied sort orders + * + * @return array + * @deprecated + * @see Not use anymore + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + */ + private function getProductIdsBySaleability(): array + { + $ids = []; + + if (!$this->hasShowOutOfStockStatus()) { + return $ids; + } + + if ($this->collection->getFlag('has_stock_status_filter') + || $this->collection->getFlag('has_category_filter')) { + $categoryId = null; + $searchCriteria = $this->searchResult->getSearchCriteria(); + foreach ($searchCriteria->getFilterGroups() as $filterGroup) { + foreach ($filterGroup->getFilters() as $filter) { + if ($filter->getField() === 'category_ids') { + $categoryId = $filter->getValue(); + break 2; + } + } + } + + if ($categoryId) { + $resultSet = $this->categoryProductByCustomSortOrder($categoryId); + foreach ($resultSet as $item) { + $ids[] = (int)$item['entity_id']; + } + } + } + + return $ids; + } + + /** + * Fetch product resultset by custom sort orders + * + * @param int $categoryId + * @return array + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Exception + * @deprecated + * @see Not use anymore + */ + private function categoryProductByCustomSortOrder(int $categoryId): array + { + $storeId = $this->collection->getStoreId(); + $searchCriteria = $this->searchResult->getSearchCriteria(); + $sortOrders = $searchCriteria->getSortOrders() ?? []; + $sortOrders = array_merge(['is_salable' => \Magento\Framework\DB\Select::SQL_DESC], $sortOrders); + + $connection = $this->collection->getConnection(); + $query = clone $connection->select() + ->reset(\Magento\Framework\DB\Select::ORDER) + ->reset(\Magento\Framework\DB\Select::LIMIT_COUNT) + ->reset(\Magento\Framework\DB\Select::LIMIT_OFFSET) + ->reset(\Magento\Framework\DB\Select::COLUMNS); + $query->from( + ['e' => $this->collection->getTable('catalog_product_entity')], + ['e.entity_id'] + ); + $this->stockStatusApplier->setSearchResultApplier(true); + $query = $this->stockStatusFilter->execute($query, 'e', 'stockItem'); + $query->join( + ['cat_index' => $this->collection->getTable('catalog_category_product_index_store' . $storeId)], + 'cat_index.product_id = e.entity_id' + . ' AND cat_index.category_id = ' . $categoryId + . ' AND cat_index.store_id = ' . $storeId, + ['cat_index.position'] + ); + foreach ($sortOrders as $field => $dir) { + if ($field === 'name') { + $entityTypeId = $this->collection->getEntity()->getTypeId(); + $entityMetadata = $this->metadataPool->getMetadata(ProductInterface::class); + $linkField = $entityMetadata->getLinkField(); + $query->joinLeft( + ['product_var' => $this->collection->getTable('catalog_product_entity_varchar')], + "product_var.{$linkField} = e.{$linkField} AND product_var.attribute_id = + (SELECT attribute_id FROM eav_attribute WHERE entity_type_id={$entityTypeId} + AND attribute_code='name')", + ['product_var.value AS name'] + ); + } elseif ($field === 'price') { + $query->joinLeft( + ['price_index' => $this->collection->getTable('catalog_product_index_price')], + 'price_index.entity_id = e.entity_id' + . ' AND price_index.customer_group_id = 0' + . ' AND price_index.website_id = (Select website_id FROM store WHERE store_id = ' + . $storeId . ')', + ['price_index.max_price AS price'] + ); + } + $columnFilters = []; + $columnsParts = $query->getPart('columns'); + foreach ($columnsParts as $columns) { + $columnFilters[] = $columns[2] ?? $columns[1]; + } + if (in_array($field, $columnFilters, true)) { + $query->order(new \Zend_Db_Expr("{$field} {$dir}")); + } + } + + $query->limit( + $searchCriteria->getPageSize(), + $searchCriteria->getCurrentPage() * $searchCriteria->getPageSize() + ); + + return $connection->fetchAssoc($query) ?? []; + } + + /** + * Returns if display out of stock status set or not in catalog inventory + * + * @return bool + * @deprecated + * @see Not use anymore + */ + private function hasShowOutOfStockStatus(): bool + { + return (bool) $this->scopeConfig->getValue( + \Magento\CatalogInventory\Model\Configuration::XML_PATH_SHOW_OUT_OF_STOCK, + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + } } diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php index e58a894ba92cd..23b7f861dc761 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php @@ -8,11 +8,14 @@ namespace Magento\Elasticsearch\SearchAdapter\Query\Builder; +use Magento\Elasticsearch\Model\Adapter\BatchDataMapper\InventoryFieldsProvider; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface as FieldNameResolver; use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Collection; use Magento\Framework\Search\RequestInterface; /** @@ -62,22 +65,22 @@ class Sort /** * @param AttributeProvider $attributeAdapterProvider * @param FieldNameResolver $fieldNameResolver - * @param ScopeConfigInterface $scopeConfig * @param array $skippedFields * @param array $map + * @param ScopeConfigInterface|null $scopeConfig */ public function __construct( AttributeProvider $attributeAdapterProvider, FieldNameResolver $fieldNameResolver, - ScopeConfigInterface $scopeConfig, array $skippedFields = [], - array $map = [] + array $map = [], + ScopeConfigInterface $scopeConfig = null ) { $this->attributeAdapterProvider = $attributeAdapterProvider; $this->fieldNameResolver = $fieldNameResolver; - $this->scopeConfig = $scopeConfig; $this->skippedFields = array_merge(self::DEFAULT_SKIPPED_FIELDS, $skippedFields); $this->map = array_merge(self::DEFAULT_MAP, $map); + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); } /** @@ -142,16 +145,16 @@ public function getSort(RequestInterface $request) * * @return array */ - public function getSortBySaleability() + private function getSortBySaleability() { $sorts = []; if ($this->hasShowOutOfStockStatus()) { - $attribute = $this->attributeAdapterProvider->getByAttributeCode('is_salable'); + $attribute = $this->attributeAdapterProvider->getByAttributeCode(InventoryFieldsProvider::IS_SALABLE); $fieldName = $this->fieldNameResolver->getFieldName($attribute); $sorts[] = [ $fieldName => [ - 'order' => 'desc' + 'order' => Collection::SORT_ORDER_DESC ] ]; } From 3859540f81f853534a57108a5fe9cdfe822f2790 Mon Sep 17 00:00:00 2001 From: Roger Date: Mon, 12 Sep 2022 16:23:15 +0000 Subject: [PATCH 3/3] issue 36096: update code --- .../InventoryFieldsProvider.php | 11 +- .../FieldName/Resolver/IsSalable.php | 31 +++ .../SearchAdapter/Query/Builder/SortTest.php | 239 +++++++++++++++++- app/code/Magento/Elasticsearch/etc/di.xml | 2 + 4 files changed, 267 insertions(+), 16 deletions(-) create mode 100644 app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/IsSalable.php diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/InventoryFieldsProvider.php b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/InventoryFieldsProvider.php index e07e8ec892977..f1f0fba7e20b1 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/InventoryFieldsProvider.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/InventoryFieldsProvider.php @@ -75,10 +75,9 @@ public function getFields(array $productIds, $storeId) $fields = []; if ($this->hasShowOutOfStockStatus()) { - $websiteId = $this->storeManager->getStore($storeId)->getWebsiteId(); $inventoryData = $this->resourceIndex->getInventoryIndexData($productIds); foreach ($productIds as $productId) { - $fields[$productId] = $this->getProductInventoryData($productId, $websiteId, $inventoryData); + $fields[$productId] = $this->getProductInventoryData($productId, $inventoryData); } } return $fields; @@ -88,19 +87,15 @@ public function getFields(array $productIds, $storeId) * Prepare inventory index for product. * * @param int $productId - * @param int $websiteId * @param array $inventoryData * @return array */ - private function getProductInventoryData($productId, $websiteId, array $inventoryData) + private function getProductInventoryData($productId, array $inventoryData) { $result = []; if (array_key_exists($productId, $inventoryData)) { $inStockAttribute = $this->attributeAdapterProvider->getByAttributeCode(self::IS_SALABLE); - $fieldName = $this->fieldNameResolver->getFieldName( - $inStockAttribute, - ['websiteId' => $websiteId] - ); + $fieldName = $this->fieldNameResolver->getFieldName($inStockAttribute); $result[$fieldName] = $inventoryData[$productId]; } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/IsSalable.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/IsSalable.php new file mode 100644 index 0000000000000..5e217bf961be0 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/IsSalable.php @@ -0,0 +1,31 @@ +getAttributeCode() === InventoryFieldsProvider::IS_SALABLE) { + return $attribute->getAttributeCode(); + } + + return null; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/SortTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/SortTest.php index 689384f95a896..e6515384419dd 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/SortTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/Builder/SortTest.php @@ -7,11 +7,13 @@ namespace Magento\Elasticsearch\Test\Unit\SearchAdapter\Query\Builder; +use Magento\Elasticsearch\Model\Adapter\BatchDataMapper\InventoryFieldsProvider; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\ResolverInterface as FieldNameResolver; use Magento\Elasticsearch\SearchAdapter\Query\Builder\Sort; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Search\RequestInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; @@ -34,6 +36,11 @@ class SortTest extends TestCase */ private $sortBuilder; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + /** * @inheritdoc */ @@ -48,11 +55,15 @@ protected function setUp(): void ->setMethods(['getFieldName']) ->getMock(); + $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) + ->getMock(); + $this->sortBuilder = (new ObjectManager($this))->getObject( Sort::class, [ 'attributeAdapterProvider' => $this->attributeAdapterProvider, 'fieldNameResolver' => $this->fieldNameResolver, + 'scopeConfig' => $this->scopeConfig, ] ); } @@ -66,6 +77,7 @@ protected function setUp(): void * @param $isFloatType * @param $isIntegerType * @param $fieldName + * @param $showOutStock * @param array $expected */ public function testGetSort( @@ -75,6 +87,7 @@ public function testGetSort( $isIntegerType, $isComplexType, $fieldName, + $showOutStock, array $expected ) { /** @var MockObject|RequestInterface $request */ @@ -87,7 +100,7 @@ public function testGetSort( ->willReturn($sortItems); $attributeMock = $this->getMockBuilder(AttributeAdapter::class) ->disableOriginalConstructor() - ->setMethods(['isSortable', 'isFloatType', 'isIntegerType', 'isComplexType']) + ->setMethods(['isSortable', 'isFloatType', 'isIntegerType', 'isComplexType', 'getAttributeCode']) ->getMock(); $attributeMock->expects($this->any()) ->method('isSortable') @@ -101,22 +114,55 @@ public function testGetSort( $attributeMock->expects($this->any()) ->method('isComplexType') ->willReturn($isComplexType); + $attributeMock->expects($this->any()) + ->method('getAttributeCode') + ->willReturn((string) $fieldName); + + $salesAttributeMock = $this->getMockBuilder(AttributeAdapter::class) + ->disableOriginalConstructor() + ->setMethods(['getAttributeCode']) + ->getMock(); + $salesAttributeMock->expects($this->any()) + ->method('getAttributeCode') + ->willReturn(InventoryFieldsProvider::IS_SALABLE); + + $maps = [ + [null, $attributeMock], + [ InventoryFieldsProvider::IS_SALABLE, $salesAttributeMock ], + [ $fieldName, $attributeMock ], + ]; + foreach ($sortItems as $item) { + $maps[] = [$item['field'], $attributeMock]; + } $this->attributeAdapterProvider->expects($this->any()) ->method('getByAttributeCode') - ->with($this->anything()) - ->willReturn($attributeMock); + ->will( + $this->returnValueMap( + $maps + ) + ); + $this->fieldNameResolver->expects($this->any()) ->method('getFieldName') ->with($this->anything()) ->willReturnCallback( function ($attribute, $context) use ($fieldName) { - if (empty($context)) { - return $fieldName; - } elseif ($context['type'] === 'sort') { - return 'sort_' . $fieldName; + if ($attribute->getAttributeCode() === InventoryFieldsProvider::IS_SALABLE) { + return InventoryFieldsProvider::IS_SALABLE; + } + if ($attribute->getAttributeCode() === $fieldName) { + if (empty($context)) { + return $fieldName; + } elseif ($context['type'] === 'sort') { + return 'sort_' . $fieldName; + } } } ); + $this->scopeConfig->expects($this->any()) + ->method('getValue') + ->with($this->anything()) + ->willReturn($showOutStock); $this->assertEquals( $expected, @@ -143,6 +189,7 @@ public function getSortProvider() false, false, null, + false, [] ], [ @@ -161,6 +208,7 @@ public function getSortProvider() false, false, 'price', + false, [ [ 'price' => [ @@ -185,6 +233,7 @@ public function getSortProvider() true, false, 'price', + false, [ [ 'price' => [ @@ -209,6 +258,7 @@ public function getSortProvider() false, false, 'name', + false, [ [ 'name.sort_name' => [ @@ -233,6 +283,7 @@ public function getSortProvider() false, false, 'not_eav_attribute', + false, [ [ 'not_eav_attribute' => [ @@ -257,6 +308,7 @@ public function getSortProvider() false, true, 'color', + false, [ [ 'color_value.sort_color' => [ @@ -264,7 +316,178 @@ public function getSortProvider() ] ] ] - ] + ], + [ + [ + [ + 'field' => 'entity_id', + 'direction' => 'DESC' + ] + ], + false, + false, + false, + false, + null, + true, + [ + [ + InventoryFieldsProvider::IS_SALABLE => [ + 'order' => 'DESC' + ] + ] + ] + ], + [ + [ + [ + 'field' => 'entity_id', + 'direction' => 'DESC' + ], + [ + 'field' => 'price', + 'direction' => 'DESC' + ], + ], + false, + false, + false, + false, + 'price', + true, + [ + [ + InventoryFieldsProvider::IS_SALABLE => [ + 'order' => 'DESC' + ] + ], + [ + 'price' => [ + 'order' => 'desc' + ] + ] + ] + ], + [ + [ + [ + 'field' => 'entity_id', + 'direction' => 'DESC' + ], + [ + 'field' => 'price', + 'direction' => 'DESC' + ], + ], + true, + true, + true, + false, + 'price', + true, + [ + [ + InventoryFieldsProvider::IS_SALABLE => [ + 'order' => 'DESC' + ] + ], + [ + 'price' => [ + 'order' => 'desc' + ] + ] + ] + ], + [ + [ + [ + 'field' => 'entity_id', + 'direction' => 'DESC' + ], + [ + 'field' => 'name', + 'direction' => 'DESC' + ], + ], + true, + false, + false, + false, + 'name', + true, + [ + [ + InventoryFieldsProvider::IS_SALABLE => [ + 'order' => 'DESC' + ] + ], + [ + 'name.sort_name' => [ + 'order' => 'desc' + ] + ] + ] + ], + [ + [ + [ + 'field' => 'entity_id', + 'direction' => 'DESC' + ], + [ + 'field' => 'not_eav_attribute', + 'direction' => 'DESC' + ], + ], + false, + false, + false, + false, + 'not_eav_attribute', + true, + [ + [ + InventoryFieldsProvider::IS_SALABLE => [ + 'order' => 'DESC' + ] + ], + [ + 'not_eav_attribute' => [ + 'order' => 'desc' + ] + ] + ] + ], + [ + [ + [ + 'field' => 'entity_id', + 'direction' => 'DESC' + ], + [ + 'field' => 'color', + 'direction' => 'DESC' + ], + ], + true, + false, + false, + true, + 'color', + true, + [ + [ + InventoryFieldsProvider::IS_SALABLE => [ + 'order' => 'DESC' + ] + ], + [ + 'color_value.sort_color' => [ + 'order' => 'desc' + ] + ] + ] + ], ]; } } diff --git a/app/code/Magento/Elasticsearch/etc/di.xml b/app/code/Magento/Elasticsearch/etc/di.xml index 78174d36efe96..24c10f840252a 100644 --- a/app/code/Magento/Elasticsearch/etc/di.xml +++ b/app/code/Magento/Elasticsearch/etc/di.xml @@ -364,6 +364,7 @@ \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\Price \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CategoryName \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\Position + \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\IsSalable \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\DefaultResolver @@ -376,6 +377,7 @@ \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\Price \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CategoryName \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\Position + \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\IsSalable elasticsearch5FieldNameDefaultResolver