diff --git a/app/code/Magento/AdvancedSearch/Model/ResourceModel/Index.php b/app/code/Magento/AdvancedSearch/Model/ResourceModel/Index.php
index 420f7b1e0e72a..cacebfe600765 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
+ */
+ private 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/Adapter/BatchDataMapper/InventoryFieldsProvider.php b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/InventoryFieldsProvider.php
new file mode 100644
index 0000000000000..f1f0fba7e20b1
--- /dev/null
+++ b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/InventoryFieldsProvider.php
@@ -0,0 +1,117 @@
+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()) {
+ $inventoryData = $this->resourceIndex->getInventoryIndexData($productIds);
+ foreach ($productIds as $productId) {
+ $fields[$productId] = $this->getProductInventoryData($productId, $inventoryData);
+ }
+ }
+ return $fields;
+ }
+
+ /**
+ * Prepare inventory index for product.
+ *
+ * @param int $productId
+ * @param array $inventoryData
+ * @return array
+ */
+ 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);
+ $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/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/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php
index 97cb92ab3b06d..8933e03be1c88 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,13 @@
* 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 +106,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)
@@ -139,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);
@@ -164,6 +163,9 @@ private function getOffset(int $pageNumber, int $pageSize): int
* 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
{
@@ -204,6 +206,8 @@ private function getProductIdsBySaleability(): array
* @return array
* @throws \Magento\Framework\Exception\LocalizedException
* @throws \Exception
+ * @deprecated
+ * @see Not use anymore
*/
private function categoryProductByCustomSortOrder(int $categoryId): array
{
@@ -275,6 +279,8 @@ private function categoryProductByCustomSortOrder(int $categoryId): array
* Returns if display out of stock status set or not in catalog inventory
*
* @return bool
+ * @deprecated
+ * @see Not use anymore
*/
private function hasShowOutOfStockStatus(): bool
{
diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php
index 7d41d54fb22a5..23b7f861dc761 100644
--- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php
+++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/Builder/Sort.php
@@ -1,15 +1,21 @@
attributeAdapterProvider = $attributeAdapterProvider;
$this->fieldNameResolver = $fieldNameResolver;
$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);
}
/**
@@ -80,7 +94,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 +139,39 @@ public function getSort(RequestInterface $request)
return $sorts;
}
+
+ /**
+ * Prepare sort by saleability.
+ *
+ * @return array
+ */
+ private function getSortBySaleability()
+ {
+ $sorts = [];
+
+ if ($this->hasShowOutOfStockStatus()) {
+ $attribute = $this->attributeAdapterProvider->getByAttributeCode(InventoryFieldsProvider::IS_SALABLE);
+ $fieldName = $this->fieldNameResolver->getFieldName($attribute);
+ $sorts[] = [
+ $fieldName => [
+ 'order' => Collection::SORT_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/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 95aec47dbf1f6..24c10f840252a 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
@@ -363,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
@@ -375,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