diff --git a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php
index 50a3caaf68b3b..538c80d9b1cf2 100644
--- a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php
+++ b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php
@@ -268,15 +268,12 @@ protected function getBundleOptions()
'arguments' => [
'data' => [
'config' => [
- 'componentType' => 'dynamicRows',
+ 'componentType' => Container::NAME,
+ 'component' => 'Magento_Bundle/js/components/bundle-dynamic-rows',
'template' => 'ui/dynamic-rows/templates/collapsible',
- 'label' => '',
'additionalClasses' => 'admin__field-wide',
- 'collapsibleHeader' => true,
- 'columnsHeader' => false,
- 'deleteProperty' => false,
- 'addButton' => false,
'dataScope' => 'data.bundle_options',
+ 'bundleSelectionsName' => 'product_bundle_container.bundle_selections'
],
],
],
@@ -318,14 +315,11 @@ protected function getBundleOptions()
'arguments' => [
'data' => [
'config' => [
- 'componentType' => DynamicRows::NAME,
- 'label' => '',
+ 'componentType' => Container::NAME,
+ 'component' => 'Magento_Bundle/js/components/bundle-dynamic-rows-grid',
'sortOrder' => 50,
'additionalClasses' => 'admin__field-wide',
- 'component' => 'Magento_Ui/js/dynamic-rows/dynamic-rows-grid',
'template' => 'ui/dynamic-rows/templates/default',
- 'columnsHeader' => false,
- 'columnsHeaderAfterRender' => true,
'provider' => 'product_form.product_form_data_source',
'dataProvider' => '${ $.dataScope }' . '.bundle_button_proxy',
'identificationDRProperty' => 'product_id',
@@ -343,8 +337,7 @@ protected function getBundleOptions()
'selection_qty' => '',
],
'links' => ['insertData' => '${ $.provider }:${ $.dataProvider }'],
- 'source' => 'product',
- 'addButton' => false,
+ 'source' => 'product'
],
],
],
@@ -561,7 +554,7 @@ protected function getBundleSelections()
'componentType' => Container::NAME,
'isTemplate' => true,
'component' => 'Magento_Ui/js/dynamic-rows/record',
- 'is_collection' => true,
+ 'is_collection' => true
],
],
],
diff --git a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows-grid.js b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows-grid.js
new file mode 100644
index 0000000000000..e9a924e1cffe6
--- /dev/null
+++ b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows-grid.js
@@ -0,0 +1,59 @@
+/**
+ * Copyright © 2016 Magento. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+define([
+ 'underscore',
+ 'Magento_Ui/js/dynamic-rows/dynamic-rows-grid'
+], function (_, dynamicRowsGrid) {
+ 'use strict';
+
+ return dynamicRowsGrid.extend({
+ defaults: {
+ label: '',
+ columnsHeader: false,
+ columnsHeaderAfterRender: true,
+ addButton: false
+ },
+
+ /**
+ * Initialize elements from grid
+ *
+ * @param {Array} data
+ *
+ * @returns {Object} Chainable.
+ */
+ initElements: function (data) {
+ var newData = this.getNewData(data),
+ recordIndex;
+
+ this.parsePagesData(data);
+
+ if (newData.length) {
+ if (this.insertData().length) {
+ recordIndex = data.length - newData.length - 1;
+
+ _.each(newData, function (newRecord) {
+ this.processingAddChild(newRecord, ++recordIndex, newRecord[this.identificationProperty]);
+ }, this);
+ }
+ }
+
+ return this;
+ },
+
+ /**
+ * Mapping value from grid
+ *
+ * @param {Array} data
+ */
+ mappingValue: function (data) {
+ if (_.isEmpty(data)) {
+ return;
+ }
+
+ this._super();
+ }
+ });
+});
diff --git a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows.js b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows.js
new file mode 100644
index 0000000000000..b36d8003a399f
--- /dev/null
+++ b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows.js
@@ -0,0 +1,98 @@
+/**
+ * Copyright © 2016 Magento. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+define([
+ 'underscore',
+ 'mageUtils',
+ 'uiRegistry',
+ 'Magento_Ui/js/dynamic-rows/dynamic-rows'
+], function (_, utils, registry, dynamicRows) {
+ 'use strict';
+
+ return dynamicRows.extend({
+ defaults: {
+ label: '',
+ collapsibleHeader: true,
+ columnsHeader: false,
+ deleteProperty: false,
+ addButton: false
+ },
+
+ /**
+ * Set new data to dataSource,
+ * delete element
+ *
+ * @param {Array} data - record data
+ */
+ _updateData: function (data) {
+ var elems = _.clone(this.elems()),
+ path,
+ dataArr,
+ optionBaseData;
+
+ dataArr = this.recordData.splice(this.startIndex, this.recordData().length - this.startIndex);
+ dataArr.splice(0, this.pageSize);
+ elems = _.sortBy(this.elems(), function (elem) {
+ return ~~elem.index;
+ });
+
+ data.concat(dataArr).forEach(function (rec, idx) {
+ if (elems[idx]) {
+ elems[idx].recordId = rec[this.identificationProperty];
+ }
+
+ if (!rec.position) {
+ rec.position = this.maxPosition;
+ this.setMaxPosition();
+ }
+
+ path = this.dataScope + '.' + this.index + '.' + (this.startIndex + idx);
+ optionBaseData = _.pick(rec, function (value) {
+ return !_.isObject(value);
+ });
+ this.source.set(path, optionBaseData);
+ this.source.set(path + '.bundle_button_proxy', []);
+ this.source.set(path + '.bundle_selections', []);
+ this.removeBundleItemsFromOption(idx);
+ _.each(rec['bundle_selections'], function (obj, index) {
+ this.source.set(path + '.bundle_button_proxy' + '.' + index, rec['bundle_button_proxy'][index]);
+ this.source.set(path + '.bundle_selections' + '.' + index, obj);
+ }, this);
+ }, this);
+
+ this.elems(elems);
+ },
+
+ /**
+ * Removes nested dynamic-rows-grid rendered records from option
+ *
+ * @param {Number|String} index - element index
+ */
+ removeBundleItemsFromOption: function (index) {
+ var bundleSelections = registry.get(this.name + '.' + index + '.' + this.bundleSelectionsName),
+ bundleSelectionsLength = (bundleSelections.elems() || []).length,
+ i;
+
+ if (bundleSelectionsLength) {
+ for (i = 0; i < bundleSelectionsLength; i++) {
+ bundleSelections.elems()[0].destroy();
+ }
+ }
+ },
+
+ /**
+ * {@inheritdoc}
+ */
+ processingAddChild: function (ctx, index, prop) {
+ var recordIds = _.map(this.recordData(), function (rec) {
+ return parseInt(rec['record_id'], 10);
+ }),
+ maxRecordId = _.max(recordIds);
+
+ prop = maxRecordId > -1 ? maxRecordId + 1 : prop;
+ this._super(ctx, index, prop);
+ }
+ });
+});
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php
index d9612f18c9111..432bc696ef7ce 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/AbstractEav.php
@@ -197,7 +197,7 @@ protected function _prepareRelationIndexSelect($parentIds = null)
)->joinLeft(
['e' => $this->getTable('catalog_product_entity')],
'e.' . $linkField .' = l.parent_id',
- ['e.entity_id as parent_id']
+ []
)->join(
['cs' => $this->getTable('store')],
'',
@@ -205,9 +205,17 @@ protected function _prepareRelationIndexSelect($parentIds = null)
)->join(
['i' => $idxTable],
'l.child_id = i.entity_id AND cs.store_id = i.store_id',
- ['attribute_id', 'store_id', 'value']
+ []
)->group(
- ['parent_id', 'i.attribute_id', 'i.store_id', 'i.value']
+ ['parent_id', 'i.attribute_id', 'i.store_id', 'i.value', 'l.child_id']
+ )->columns(
+ [
+ 'parent_id' => 'e.entity_id',
+ 'attribute_id' => 'i.attribute_id',
+ 'store_id' => 'i.store_id',
+ 'value' => 'i.value',
+ 'source_id' => 'l.child_id'
+ ]
);
if ($parentIds !== null) {
$select->where('e.entity_id IN(?)', $parentIds);
@@ -222,7 +230,7 @@ protected function _prepareRelationIndexSelect($parentIds = null)
'select' => $select,
'entity_field' => new \Zend_Db_Expr('l.parent_id'),
'website_field' => new \Zend_Db_Expr('cs.website_id'),
- 'store_field' => new \Zend_Db_Expr('cs.store_id')
+ 'store_field' => new \Zend_Db_Expr('cs.store_id'),
]
);
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Decimal.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Decimal.php
index e8d9889e68d59..a45d4f13a1a9a 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Decimal.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Decimal.php
@@ -85,6 +85,7 @@ protected function _prepareIndex($entityIds = null, $attributeId = null)
'pdd.attribute_id',
'cs.store_id',
'value' => $productValueExpression,
+ 'source_id' => 'cpe.entity_id',
]
);
@@ -116,7 +117,7 @@ protected function _prepareIndex($entityIds = null, $attributeId = null)
'select' => $select,
'entity_field' => new \Zend_Db_Expr('cpe.entity_id'),
'website_field' => new \Zend_Db_Expr('cs.website_id'),
- 'store_field' => new \Zend_Db_Expr('cs.store_id')
+ 'store_field' => new \Zend_Db_Expr('cs.store_id'),
]
);
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php
index c4eda1c987192..1d37c57aa8b25 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Eav/Source.php
@@ -178,6 +178,7 @@ protected function _prepareSelectIndex($entityIds = null, $attributeId = null)
'pid.attribute_id',
'pid.store_id',
'value' => $ifNullSql,
+ 'pid.entity_id',
]
)->where(
'pid.attribute_id IN(?)',
@@ -200,7 +201,7 @@ protected function _prepareSelectIndex($entityIds = null, $attributeId = null)
'select' => $select,
'entity_field' => new \Zend_Db_Expr('pid.entity_id'),
'website_field' => new \Zend_Db_Expr('pid.website_id'),
- 'store_field' => new \Zend_Db_Expr('pid.store_id')
+ 'store_field' => new \Zend_Db_Expr('pid.store_id'),
]
);
$query = $select->insertFromSelect($idxTable);
@@ -221,11 +222,7 @@ protected function _prepareMultiselectIndex($entityIds = null, $attributeId = nu
$connection = $this->getConnection();
// prepare multiselect attributes
- if ($attributeId === null) {
- $attrIds = $this->_getIndexableAttributes(true);
- } else {
- $attrIds = [$attributeId];
- }
+ $attrIds = $attributeId === null ? $this->_getIndexableAttributes(true) : [$attributeId];
if (!$attrIds) {
return $this;
@@ -247,20 +244,20 @@ protected function _prepareMultiselectIndex($entityIds = null, $attributeId = nu
$productValueExpression = $connection->getCheckSql('pvs.value_id > 0', 'pvs.value', 'pvd.value');
$select = $connection->select()->from(
['pvd' => $this->getTable('catalog_product_entity_varchar')],
- [$productIdField, 'attribute_id']
+ []
)->join(
['cs' => $this->getTable('store')],
'',
- ['store_id']
+ []
)->joinLeft(
['pvs' => $this->getTable('catalog_product_entity_varchar')],
"pvs.{$productIdField} = pvd.{$productIdField} AND pvs.attribute_id = pvd.attribute_id"
. ' AND pvs.store_id=cs.store_id',
- ['value' => $productValueExpression]
+ []
)->joinLeft(
['cpe' => $this->getTable('catalog_product_entity')],
"cpe.{$productIdField} = pvd.{$productIdField}",
- ['entity_id']
+ []
)->where(
'pvd.store_id=?',
$connection->getIfNullSql('pvs.store_id', \Magento\Store\Model\Store::DEFAULT_STORE_ID)
@@ -272,6 +269,14 @@ protected function _prepareMultiselectIndex($entityIds = null, $attributeId = nu
$attrIds
)->where(
'cpe.entity_id IS NOT NULL'
+ )->columns(
+ [
+ 'entity_id' => 'cpe.entity_id',
+ 'attribute_id' => 'attribute_id',
+ 'store_id' => 'cs.store_id',
+ 'value' => $productValueExpression,
+ 'source_id' => 'cpe.entity_id',
+ ]
);
$statusCond = $connection->quoteInto('=?', ProductStatus::STATUS_ENABLED);
@@ -289,30 +294,11 @@ protected function _prepareMultiselectIndex($entityIds = null, $attributeId = nu
'select' => $select,
'entity_field' => new \Zend_Db_Expr('cpe.entity_id'),
'website_field' => new \Zend_Db_Expr('cs.website_id'),
- 'store_field' => new \Zend_Db_Expr('cs.store_id')
+ 'store_field' => new \Zend_Db_Expr('cs.store_id'),
]
);
- $i = 0;
- $data = [];
- $query = $select->query();
- while ($row = $query->fetch()) {
- $values = explode(',', $row['value']);
- foreach ($values as $valueId) {
- if (isset($options[$row['attribute_id']][$valueId])) {
- $data[] = [$row['entity_id'], $row['attribute_id'], $row['store_id'], $valueId];
- $i++;
- if ($i % 10000 == 0) {
- $this->_saveIndexData($data);
- $data = [];
- }
- }
- }
- }
-
- $this->_saveIndexData($data);
- unset($options);
- unset($data);
+ $this->saveDataFromSelect($select, $options);
return $this;
}
@@ -331,7 +317,7 @@ protected function _saveIndexData(array $data)
$connection = $this->getConnection();
$connection->insertArray(
$this->getIdxTable(),
- ['entity_id', 'attribute_id', 'store_id', 'value'],
+ ['entity_id', 'attribute_id', 'store_id', 'value', 'source_id'],
$data
);
return $this;
@@ -348,4 +334,31 @@ public function getIdxTable($table = null)
{
return $this->tableStrategy->getTableName('catalog_product_index_eav');
}
+
+ /**
+ * @param \Magento\Framework\DB\Select $select
+ * @param array $options
+ * @return void
+ */
+ private function saveDataFromSelect(\Magento\Framework\DB\Select $select, array $options)
+ {
+ $i = 0;
+ $data = [];
+ $query = $select->query();
+ while ($row = $query->fetch()) {
+ $values = explode(',', $row['value']);
+ foreach ($values as $valueId) {
+ if (isset($options[$row['attribute_id']][$valueId])) {
+ $data[] = [$row['entity_id'], $row['attribute_id'], $row['store_id'], $valueId, $row['source_id']];
+ $i++;
+ if ($i % 10000 == 0) {
+ $this->_saveIndexData($data);
+ $data = [];
+ }
+ }
+ }
+ }
+
+ $this->_saveIndexData($data);
+ }
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php
index 289445ae2daf0..2b979ff79fe5c 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php
@@ -368,7 +368,7 @@ protected function prepareFinalPriceDataForType($entityIds, $type)
'select' => $select,
'entity_field' => new \Zend_Db_Expr('e.entity_id'),
'website_field' => new \Zend_Db_Expr('cw.website_id'),
- 'store_field' => new \Zend_Db_Expr('cs.store_id')
+ 'store_field' => new \Zend_Db_Expr('cs.store_id'),
]
);
diff --git a/app/code/Magento/Catalog/Setup/InstallSchema.php b/app/code/Magento/Catalog/Setup/InstallSchema.php
index 171e96efe9b0a..a2ba6aa283f65 100644
--- a/app/code/Magento/Catalog/Setup/InstallSchema.php
+++ b/app/code/Magento/Catalog/Setup/InstallSchema.php
@@ -18,6 +18,7 @@ class InstallSchema implements InstallSchemaInterface
/**
* {@inheritdoc}
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
+ * @throws \Zend_Db_Exception
*/
public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
{
@@ -2429,7 +2430,6 @@ public function install(SchemaSetupInterface $setup, ModuleContextInterface $con
'option_id',
$installer->getTable('catalog_product_option'),
'option_id',
- \Magento\Framework\DB\Ddl\Table::ACTION_CASCADE,
\Magento\Framework\DB\Ddl\Table::ACTION_CASCADE
)
->setComment(
diff --git a/app/code/Magento/Catalog/Setup/UpgradeSchema.php b/app/code/Magento/Catalog/Setup/UpgradeSchema.php
index cbcce1d427bb6..7fc2ef7d219ba 100755
--- a/app/code/Magento/Catalog/Setup/UpgradeSchema.php
+++ b/app/code/Magento/Catalog/Setup/UpgradeSchema.php
@@ -61,9 +61,56 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con
}
}
+ if (version_compare($context->getVersion(), '2.1.2', '<')) {
+ $this->addSourceEntityIdToProductEavIndex($setup);
+ }
+
$setup->endSetup();
}
+ /**
+ * Add the column 'source_id' to the Product EAV index tables.
+ * It allows to identify which entity was used to create value in the index.
+ * It is useful to identify original entity in a composite products.
+ *
+ * @param SchemaSetupInterface $setup
+ * @return void
+ */
+ private function addSourceEntityIdToProductEavIndex(SchemaSetupInterface $setup)
+ {
+ $tables = [
+ 'catalog_product_index_eav',
+ 'catalog_product_index_eav_idx',
+ 'catalog_product_index_eav_tmp',
+ 'catalog_product_index_eav_decimal',
+ 'catalog_product_index_eav_decimal_idx',
+ 'catalog_product_index_eav_decimal_tmp',
+ ];
+ $connection = $setup->getConnection();
+ foreach ($tables as $tableName) {
+ $tableName = $setup->getTable($tableName);
+ $connection->addColumn(
+ $tableName,
+ 'source_id',
+ [
+ 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER,
+ 'unsigned' => true,
+ 'nullable' => false,
+ 'default' => 0,
+ 'comment' => 'Original entity Id for attribute value',
+ ]
+ );
+ $connection->dropIndex($tableName, $connection->getPrimaryKeyName($tableName));
+ $primaryKeyFields = ['entity_id', 'attribute_id', 'store_id', 'value', 'source_id'];
+ $setup->getConnection()->addIndex(
+ $tableName,
+ $connection->getIndexName($tableName, $primaryKeyFields),
+ $primaryKeyFields,
+ \Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_PRIMARY
+ );
+ }
+ }
+
/**
* @param SchemaSetupInterface $setup
* @return void
diff --git a/app/code/Magento/Catalog/etc/module.xml b/app/code/Magento/Catalog/etc/module.xml
index c629bf6a180cc..0c9e6bb356fe1 100644
--- a/app/code/Magento/Catalog/etc/module.xml
+++ b/app/code/Magento/Catalog/etc/module.xml
@@ -6,7 +6,7 @@
*/
-->
-
+
diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php
index 9b75e6e6e0c32..ddf86951068ac 100644
--- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php
+++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Aggregation/DataProvider.php
@@ -6,6 +6,7 @@
namespace Magento\CatalogSearch\Model\Adapter\Mysql\Aggregation;
use Magento\Catalog\Model\Product;
+use Magento\CatalogInventory\Model\Stock;
use Magento\Customer\Model\Session;
use Magento\Eav\Model\Config;
use Magento\Framework\App\ResourceConnection;
@@ -79,7 +80,13 @@ public function getDataSet(
$select = $this->getSelect();
- if ($attribute->getAttributeCode() == 'price') {
+ $select->joinInner(
+ ['entities' => $entityIdsTable->getName()],
+ 'main_table.entity_id = entities.entity_id',
+ []
+ );
+
+ if ($attribute->getAttributeCode() === 'price') {
/** @var \Magento\Store\Model\Store $store */
$store = $this->scopeResolver->getScope($currentScope);
if (!$store instanceof \Magento\Store\Model\Store) {
@@ -94,19 +101,24 @@ public function getDataSet(
$currentScopeId = $this->scopeResolver->getScope($currentScope)
->getId();
$table = $this->resource->getTableName(
- 'catalog_product_index_eav' . ($attribute->getBackendType() == 'decimal' ? '_decimal' : '')
+ 'catalog_product_index_eav' . ($attribute->getBackendType() === 'decimal' ? '_decimal' : '')
);
- $select->from(['main_table' => $table], ['value'])
+ $subSelect = $select;
+ $subSelect->from(['main_table' => $table], ['main_table.value'])
+ ->joinLeft(
+ ['stock_index' => $this->resource->getTableName('cataloginventory_stock_status')],
+ 'main_table.source_id = stock_index.product_id',
+ []
+ )
->where('main_table.attribute_id = ?', $attribute->getAttributeId())
- ->where('main_table.store_id = ? ', $currentScopeId);
+ ->where('main_table.store_id = ? ', $currentScopeId)
+ ->where('stock_index.stock_status = ?', Stock::STOCK_IN_STOCK)
+ ->group(['main_table.entity_id', 'main_table.value']);
+ $parentSelect = $this->getSelect();
+ $parentSelect->from(['main_table' => $subSelect], ['main_table.value']);
+ $select = $parentSelect;
}
- $select->joinInner(
- ['entities' => $entityIdsTable->getName()],
- 'main_table.entity_id = entities.entity_id',
- []
- );
-
return $select;
}
diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php
new file mode 100644
index 0000000000000..7099ce2502b19
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/AliasResolver.php
@@ -0,0 +1,44 @@
+getField();
+ switch ($field) {
+ case 'price':
+ $alias = 'price_index';
+ break;
+ case 'category_ids':
+ $alias = 'category_ids_index';
+ break;
+ default:
+ $alias = $field . RequestGenerator::FILTER_SUFFIX;
+ break;
+ }
+ return $alias;
+ }
+}
diff --git a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php
index 05205f04f8b99..fb579c1dce29c 100644
--- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php
+++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php
@@ -8,8 +8,11 @@
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\ResourceModel\Eav\Attribute;
+use Magento\CatalogInventory\Model\Stock;
use Magento\CatalogSearch\Model\Search\TableMapper;
use Magento\Eav\Model\Config;
+use Magento\Framework\App\Config\ScopeConfigInterface;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\App\ScopeResolverInterface;
use Magento\Framework\DB\Adapter\AdapterInterface;
@@ -17,6 +20,7 @@
use Magento\Framework\Search\Adapter\Mysql\ConditionManager;
use Magento\Framework\Search\Adapter\Mysql\Filter\PreprocessorInterface;
use Magento\Framework\Search\Request\FilterInterface;
+use Magento\Store\Model\ScopeInterface;
use Magento\Store\Model\Store;
/**
@@ -60,9 +64,14 @@ class Preprocessor implements PreprocessorInterface
private $metadataPool;
/**
- * @var TableMapper
+ * @var ScopeConfigInterface
*/
- private $tableMapper;
+ private $scopeConfig;
+
+ /**
+ * @var AliasResolver
+ */
+ private $aliasResolver;
/**
* @param ConditionManager $conditionManager
@@ -71,6 +80,9 @@ class Preprocessor implements PreprocessorInterface
* @param ResourceConnection $resource
* @param TableMapper $tableMapper
* @param string $attributePrefix
+ * @param ScopeConfigInterface $scopeConfig
+ * @param AliasResolver $aliasResolver
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __construct(
ConditionManager $conditionManager,
@@ -78,7 +90,9 @@ public function __construct(
Config $config,
ResourceConnection $resource,
TableMapper $tableMapper,
- $attributePrefix
+ $attributePrefix,
+ ScopeConfigInterface $scopeConfig = null,
+ AliasResolver $aliasResolver = null
) {
$this->conditionManager = $conditionManager;
$this->scopeResolver = $scopeResolver;
@@ -86,7 +100,16 @@ public function __construct(
$this->resource = $resource;
$this->connection = $resource->getConnection();
$this->attributePrefix = $attributePrefix;
- $this->tableMapper = $tableMapper;
+
+ if (null === $scopeConfig) {
+ $scopeConfig = ObjectManager::getInstance()->get(ScopeConfigInterface::class);
+ }
+ if (null === $aliasResolver) {
+ $aliasResolver = ObjectManager::getInstance()->get(AliasResolver::class);
+ }
+
+ $this->scopeConfig = $scopeConfig;
+ $this->aliasResolver = $aliasResolver;
}
/**
@@ -117,7 +140,7 @@ private function processQueryWithField(FilterInterface $filter, $isNegation, $qu
} elseif ($filter->getField() === 'category_ids') {
return 'category_ids_index.category_id = ' . (int) $filter->getValue();
} elseif ($attribute->isStatic()) {
- $alias = $this->tableMapper->getMappingAlias($filter);
+ $alias = $this->aliasResolver->getAlias($filter);
$resultQuery = str_replace(
$this->connection->quoteIdentifier($attribute->getAttributeCode()),
$this->connection->quoteIdentifier($alias . '.' . $attribute->getAttributeCode()),
@@ -208,7 +231,7 @@ private function processRangeNumeric(FilterInterface $filter, $query, $attribute
*/
private function processTermSelect(FilterInterface $filter, $isNegation)
{
- $alias = $this->tableMapper->getMappingAlias($filter);
+ $alias = $this->aliasResolver->getAlias($filter);
if (is_array($filter->getValue())) {
$value = sprintf(
'%s IN (%s)',
@@ -224,9 +247,31 @@ private function processTermSelect(FilterInterface $filter, $isNegation)
$value
);
+ if ($this->isAddStockFilter()) {
+ $resultQuery = sprintf(
+ '%1$s AND %2$s%3$s.stock_status = %4$s',
+ $resultQuery,
+ $alias,
+ AliasResolver::STOCK_FILTER_SUFFIX,
+ Stock::STOCK_IN_STOCK
+ );
+ }
+
return $resultQuery;
}
+ /**
+ * @return bool
+ */
+ private function isAddStockFilter()
+ {
+ $isShowOutOfStock = $this->scopeConfig->isSetFlag(
+ 'cataloginventory/options/show_out_of_stock',
+ ScopeInterface::SCOPE_STORE
+ );
+ return false === $isShowOutOfStock;
+ }
+
/**
* Get product metadata pool
*
diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php
new file mode 100644
index 0000000000000..8626b2ea15b07
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/ExclusionStrategy.php
@@ -0,0 +1,83 @@
+resourceConnection = $resourceConnection;
+ $this->storeManager = $storeManager;
+ $this->aliasResolver = $aliasResolver;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function apply(
+ \Magento\Framework\Search\Request\FilterInterface $filter,
+ \Magento\Framework\DB\Select $select
+ ) {
+ $isApplied = false;
+ $field = $filter->getField();
+ if ('price' === $field) {
+ $alias = $this->aliasResolver->getAlias($filter);
+ $tableName = $this->resourceConnection->getTableName('catalog_product_index_price');
+ $select->joinInner(
+ [
+ $alias => $tableName
+ ],
+ $this->resourceConnection->getConnection()->quoteInto(
+ 'search_index.entity_id = price_index.entity_id AND price_index.website_id = ?',
+ $this->storeManager->getWebsite()->getId()
+ ),
+ []
+ );
+ $isApplied = true;
+ } elseif ('category_ids' === $field) {
+ $alias = $this->aliasResolver->getAlias($filter);
+ $tableName = $this->resourceConnection->getTableName('catalog_category_product_index');
+ $select->joinInner(
+ [
+ $alias => $tableName
+ ],
+ 'search_index.entity_id = category_ids_index.product_id',
+ []
+ );
+ $isApplied = true;
+ }
+ return $isApplied;
+ }
+}
diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php
new file mode 100644
index 0000000000000..d244e3d5f7548
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterContext.php
@@ -0,0 +1,100 @@
+eavConfig = $eavConfig;
+ $this->aliasResolver = $aliasResolver;
+ $this->exclusionStrategy = $exclusionStrategy;
+ $this->termDropdownStrategy = $termDropdownStrategy;
+ $this->staticAttributeStrategy = $staticAttributeStrategy;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function apply(
+ \Magento\Framework\Search\Request\FilterInterface $filter,
+ \Magento\Framework\DB\Select $select
+ ) {
+ $isApplied = $this->exclusionStrategy->apply($filter, $select);
+
+ if (!$isApplied) {
+ $attribute = $this->getAttributeByCode($filter->getField());
+ if ($attribute) {
+ if ($filter->getType() === \Magento\Framework\Search\Request\FilterInterface::TYPE_TERM
+ && in_array($attribute->getFrontendInput(), ['select', 'multiselect'], true)
+ ) {
+ $isApplied = $this->termDropdownStrategy->apply($filter, $select);
+ } elseif ($attribute->getBackendType() === AbstractAttribute::TYPE_STATIC) {
+ $isApplied = $this->staticAttributeStrategy->apply($filter, $select);
+ }
+ }
+ }
+
+ return $isApplied;
+ }
+
+ /**
+ * @param string $field
+ * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ private function getAttributeByCode($field)
+ {
+ return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $field);
+ }
+}
diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterStrategyInterface.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterStrategyInterface.php
new file mode 100644
index 0000000000000..cf17f7d5132ef
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/FilterStrategyInterface.php
@@ -0,0 +1,23 @@
+resourceConnection = $resourceConnection;
+ $this->eavConfig = $eavConfig;
+ $this->aliasResolver = $aliasResolver;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function apply(
+ \Magento\Framework\Search\Request\FilterInterface $filter,
+ \Magento\Framework\DB\Select $select
+ ) {
+ $attribute = $this->getAttributeByCode($filter->getField());
+ $alias = $this->aliasResolver->getAlias($filter);
+ $select->joinInner(
+ [$alias => $attribute->getBackendTable()],
+ 'search_index.entity_id = '
+ . $this->resourceConnection->getConnection()->quoteIdentifier("$alias.entity_id"),
+ []
+ );
+ return true;
+ }
+
+ /**
+ * @param string $field
+ * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ private function getAttributeByCode($field)
+ {
+ return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $field);
+ }
+}
diff --git a/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy.php b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy.php
new file mode 100644
index 0000000000000..76828fe28f434
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Model/Search/FilterMapper/TermDropdownStrategy.php
@@ -0,0 +1,127 @@
+storeManager = $storeManager;
+ $this->resourceConnection = $resourceConnection;
+ $this->eavConfig = $eavConfig;
+ $this->scopeConfig = $scopeConfig;
+ $this->aliasResolver = $aliasResolver;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ public function apply(
+ \Magento\Framework\Search\Request\FilterInterface $filter,
+ \Magento\Framework\DB\Select $select
+ ) {
+ $alias = $this->aliasResolver->getAlias($filter);
+ $attribute = $this->getAttributeByCode($filter->getField());
+ $joinCondition = sprintf(
+ 'search_index.entity_id = %1$s.entity_id AND %1$s.attribute_id = %2$d AND %1$s.store_id = %3$d',
+ $alias,
+ $attribute->getId(),
+ $this->storeManager->getWebsite()->getId()
+ );
+ $select->joinLeft(
+ [$alias => $this->resourceConnection->getTableName('catalog_product_index_eav')],
+ $joinCondition,
+ []
+ );
+ if ($this->isAddStockFilter()) {
+ $stockAlias = $alias . AliasResolver::STOCK_FILTER_SUFFIX;
+ $select->joinLeft(
+ [
+ $stockAlias => $this->resourceConnection->getTableName('cataloginventory_stock_status'),
+ ],
+ sprintf('%2$s.product_id = %1$s.source_id', $alias, $stockAlias),
+ []
+ );
+ }
+
+ return true;
+ }
+
+ /**
+ * @param string $field
+ * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ private function getAttributeByCode($field)
+ {
+ return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $field);
+ }
+
+ /**
+ * @return bool
+ */
+ private function isAddStockFilter()
+ {
+ $isShowOutOfStock = $this->scopeConfig->isSetFlag(
+ 'cataloginventory/options/show_out_of_stock',
+ ScopeInterface::SCOPE_STORE
+ );
+
+ return false === $isShowOutOfStock;
+ }
+}
diff --git a/app/code/Magento/CatalogSearch/Model/Search/IndexBuilder.php b/app/code/Magento/CatalogSearch/Model/Search/IndexBuilder.php
index 1d30bb4a14d23..0e96e4de70025 100644
--- a/app/code/Magento/CatalogSearch/Model/Search/IndexBuilder.php
+++ b/app/code/Magento/CatalogSearch/Model/Search/IndexBuilder.php
@@ -99,6 +99,7 @@ public function __construct(
*
* @param RequestInterface $request
* @return Select
+ * @throws \LogicException
*/
public function build(RequestInterface $request)
{
@@ -132,7 +133,7 @@ public function build(RequestInterface $request)
),
[]
);
- $select->where('stock_index.stock_status = ?', Stock::DEFAULT_STOCK_ID);
+ $select->where('stock_index.stock_status = ?', Stock::STOCK_IN_STOCK);
}
return $select;
diff --git a/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php b/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php
index ca7298e1beaac..ac726192856a5 100644
--- a/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php
+++ b/app/code/Magento/CatalogSearch/Model/Search/TableMapper.php
@@ -6,10 +6,11 @@
namespace Magento\CatalogSearch\Model\Search;
-use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory;
+use Magento\CatalogSearch\Model\Adapter\Mysql\Filter\AliasResolver;
+use Magento\CatalogSearch\Model\Search\FilterMapper\FilterStrategyInterface;
use Magento\Eav\Model\Config as EavConfig;
-use Magento\Eav\Model\Entity\Attribute\AbstractAttribute;
+use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\ResourceConnection as AppResource;
use Magento\Framework\DB\Select;
@@ -21,12 +22,15 @@
use Magento\Store\Model\StoreManagerInterface;
/**
+ * Responsibility of the TableMapper is to collect all filters from the search query
+ * and pass them one by one for processing in the FilterContext,
+ * which will apply them to the Select
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class TableMapper
{
/**
- * @var Resource
+ * @var AppResource
*/
private $resource;
@@ -40,22 +44,59 @@ class TableMapper
*/
private $eavConfig;
+ /**
+ * @var ScopeConfigInterface
+ */
+ private $scopeConfig;
+
+ /**
+ * @var FilterStrategyInterface
+ */
+ private $filterStrategy;
+
+ /**
+ * @var AliasResolver
+ */
+ private $aliasResolver;
+
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
* @param AppResource $resource
* @param StoreManagerInterface $storeManager
* @param CollectionFactory $attributeCollectionFactory
* @param EavConfig $eavConfig
+ * @param ScopeConfigInterface $scopeConfig
+ * @param FilterStrategyInterface $filterStrategy
+ * @param AliasResolver $aliasResolver
*/
public function __construct(
AppResource $resource,
StoreManagerInterface $storeManager,
CollectionFactory $attributeCollectionFactory,
- EavConfig $eavConfig = null
+ EavConfig $eavConfig = null,
+ ScopeConfigInterface $scopeConfig = null,
+ FilterStrategyInterface $filterStrategy = null,
+ AliasResolver $aliasResolver = null
) {
$this->resource = $resource;
$this->storeManager = $storeManager;
- $this->eavConfig = $eavConfig !== null ? $eavConfig : ObjectManager::getInstance()->get(EavConfig::class);
+
+ if (null === $eavConfig) {
+ $eavConfig = ObjectManager::getInstance()->get(EavConfig::class);
+ }
+ if (null === $scopeConfig) {
+ $scopeConfig = ObjectManager::getInstance()->get(ScopeConfigInterface::class);
+ }
+ if (null === $filterStrategy) {
+ $filterStrategy = ObjectManager::getInstance()->get(FilterStrategyInterface::class);
+ }
+ if (null === $aliasResolver) {
+ $aliasResolver = ObjectManager::getInstance()->get(AliasResolver::class);
+ }
+ $this->eavConfig = $eavConfig;
+ $this->scopeConfig = $scopeConfig;
+ $this->filterStrategy = $filterStrategy;
+ $this->aliasResolver = $aliasResolver;
}
/**
@@ -66,111 +107,53 @@ public function __construct(
*/
public function addTables(Select $select, RequestInterface $request)
{
- $mappedTables = [];
- $filters = $this->getFilters($request->getQuery());
+ $appliedFilters = [];
+ $filters = $this->getFiltersFromQuery($request->getQuery());
foreach ($filters as $filter) {
- list($alias, $table, $mapOn, $mappedFields, $joinType) = $this->getMappingData($filter);
- if (!array_key_exists($alias, $mappedTables)) {
- switch ($joinType) {
- case \Magento\Framework\DB\Select::INNER_JOIN:
- $select->joinInner(
- [$alias => $table],
- $mapOn,
- $mappedFields
- );
- break;
- case \Magento\Framework\DB\Select::LEFT_JOIN:
- $select->joinLeft(
- [$alias => $table],
- $mapOn,
- $mappedFields
- );
- break;
- default:
- throw new \LogicException(__('Unsupported join type: %1', $joinType));
+ $alias = $this->aliasResolver->getAlias($filter);
+ if (!array_key_exists($alias, $appliedFilters)) {
+ $isApplied = $this->filterStrategy->apply($filter, $select);
+ if ($isApplied) {
+ $appliedFilters[$alias] = true;
}
- $mappedTables[$alias] = $table;
}
}
return $select;
}
/**
+ * This method is deprecated.
+ * Please use \Magento\CatalogSearch\Model\Adapter\Mysql\Filter\AliasResolver::getAlias() instead.
+ *
+ * @deprecated
+ * @see AliasResolver::getAlias()
+ *
* @param FilterInterface $filter
* @return string
*/
public function getMappingAlias(FilterInterface $filter)
{
- list($alias) = $this->getMappingData($filter);
- return $alias;
- }
-
- /**
- * Returns mapping data for field in format: [
- * 'table_alias',
- * 'table',
- * 'join_condition',
- * ['fields'],
- * 'joinType'
- * ]
- * @param FilterInterface $filter
- * @return array
- */
- private function getMappingData(FilterInterface $filter)
- {
- $alias = null;
- $table = null;
- $mapOn = null;
- $mappedFields = null;
- $field = $filter->getField();
- $joinType = \Magento\Framework\DB\Select::INNER_JOIN;
- $fieldToTableMap = $this->getFieldToTableMap($field);
- if ($fieldToTableMap) {
- list($alias, $table, $mapOn, $mappedFields) = $fieldToTableMap;
- $table = $this->resource->getTableName($table);
- } elseif ($attribute = $this->getAttributeByCode($field)) {
- if ($filter->getType() === FilterInterface::TYPE_TERM
- && in_array($attribute->getFrontendInput(), ['select', 'multiselect'], true)
- ) {
- $joinType = \Magento\Framework\DB\Select::LEFT_JOIN;
- $table = $this->resource->getTableName('catalog_product_index_eav');
- $alias = $field . RequestGenerator::FILTER_SUFFIX;
- $mapOn = sprintf(
- 'search_index.entity_id = %1$s.entity_id AND %1$s.attribute_id = %2$d AND %1$s.store_id = %3$d',
- $alias,
- $attribute->getId(),
- $this->getStoreId()
- );
- $mappedFields = [];
- } elseif ($attribute->getBackendType() === AbstractAttribute::TYPE_STATIC) {
- $table = $attribute->getBackendTable();
- $alias = $field . RequestGenerator::FILTER_SUFFIX;
- $mapOn = 'search_index.entity_id = ' . $alias . '.entity_id';
- $mappedFields = null;
- }
- }
-
- return [$alias, $table, $mapOn, $mappedFields, $joinType];
+ return $this->aliasResolver->getAlias($filter);
}
/**
* @param RequestQueryInterface $query
* @return FilterInterface[]
*/
- private function getFilters($query)
+ private function getFiltersFromQuery(RequestQueryInterface $query)
{
$filters = [];
switch ($query->getType()) {
case RequestQueryInterface::TYPE_BOOL:
/** @var \Magento\Framework\Search\Request\Query\BoolExpression $query */
foreach ($query->getMust() as $subQuery) {
- $filters = array_merge($filters, $this->getFilters($subQuery));
+ $filters = array_merge($filters, $this->getFiltersFromQuery($subQuery));
}
foreach ($query->getShould() as $subQuery) {
- $filters = array_merge($filters, $this->getFilters($subQuery));
+ $filters = array_merge($filters, $this->getFiltersFromQuery($subQuery));
}
foreach ($query->getMustNot() as $subQuery) {
- $filters = array_merge($filters, $this->getFilters($subQuery));
+ $filters = array_merge($filters, $this->getFiltersFromQuery($subQuery));
}
break;
case RequestQueryInterface::TYPE_FILTER:
@@ -219,57 +202,4 @@ private function getFiltersFromBoolFilter(BoolExpression $boolExpression)
}
return $filters;
}
-
- /**
- * @return int
- */
- private function getWebsiteId()
- {
- return $this->storeManager->getWebsite()->getId();
- }
-
- /**
- * @return int
- */
- private function getStoreId()
- {
- return $this->storeManager->getStore()->getId();
- }
-
- /**
- * @param string $field
- * @return array|null
- */
- private function getFieldToTableMap($field)
- {
- $fieldToTableMap = [
- 'price' => [
- 'price_index',
- 'catalog_product_index_price',
- $this->resource->getConnection()->quoteInto(
- 'search_index.entity_id = price_index.entity_id AND price_index.website_id = ?',
- $this->getWebsiteId()
- ),
- []
- ],
- 'category_ids' => [
- 'category_ids_index',
- 'catalog_category_product_index',
- 'search_index.entity_id = category_ids_index.product_id',
- []
- ]
- ];
- return array_key_exists($field, $fieldToTableMap) ? $fieldToTableMap[$field] : null;
- }
-
- /**
- * @param string $field
- * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute
- * @throws \Magento\Framework\Exception\LocalizedException
- */
- private function getAttributeByCode($field)
- {
- $attribute = $this->eavConfig->getAttribute(Product::ENTITY, $field);
- return $attribute;
- }
}
diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php
new file mode 100644
index 0000000000000..ab01d2553a3ed
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/AliasResolverTest.php
@@ -0,0 +1,68 @@
+aliasResolver = $objectManagerHelper->getObject(
+ \Magento\CatalogSearch\Model\Adapter\Mysql\Filter\AliasResolver::class,
+ []
+ );
+ }
+
+ /**
+ * @param string $field
+ * @param string $expectedAlias
+ * @dataProvider aliasDataProvider
+ */
+ public function testGetFilterAlias($field, $expectedAlias)
+ {
+ $filter = $this->getMockBuilder(\Magento\Framework\Search\Request\Filter\Term::class)
+ ->setMethods(['getField'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $filter->expects($this->once())
+ ->method('getField')
+ ->willReturn($field);
+ $this->assertSame($expectedAlias, $this->aliasResolver->getAlias($filter));
+ }
+
+ /**
+ * @return array
+ */
+ public function aliasDataProvider()
+ {
+ return [
+ 'general' => [
+ 'field' => 'general',
+ 'alias' => 'general' . RequestGenerator::FILTER_SUFFIX,
+ ],
+ 'price' => [
+ 'field' => 'price',
+ 'alias' => 'price_index',
+ ],
+ 'category_ids' => [
+ 'field' => 'category_ids',
+ 'alias' => 'category_ids_index',
+ ],
+ ];
+ }
+}
diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php
index 0949bf6469be9..2cd6935586bd8 100644
--- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php
+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Adapter/Mysql/Filter/PreprocessorTest.php
@@ -6,6 +6,7 @@
namespace Magento\CatalogSearch\Test\Unit\Model\Adapter\Mysql\Filter;
+use Magento\CatalogSearch\Model\Adapter\Mysql\Filter\AliasResolver;
use Magento\Framework\DB\Select;
use Magento\Framework\EntityManager\EntityMetadata;
use Magento\Framework\Search\Request\FilterInterface;
@@ -18,9 +19,9 @@
class PreprocessorTest extends \PHPUnit_Framework_TestCase
{
/**
- * @var \Magento\CatalogSearch\Model\Search\TableMapper|\PHPUnit_Framework_MockObject_MockObject
+ * @var AliasResolver|\PHPUnit_Framework_MockObject_MockObject
*/
- private $tableMapper;
+ private $aliasResolver;
/**
* @var \Magento\Framework\DB\Adapter\AdapterInterface|MockObject
@@ -141,7 +142,7 @@ function ($select) {
)
);
- $this->tableMapper = $this->getMockBuilder(\Magento\CatalogSearch\Model\Search\TableMapper::class)
+ $this->aliasResolver = $this->getMockBuilder(AliasResolver::class)
->disableOriginalConstructor()
->getMock();
$this->metadataPoolMock = $this->getMockBuilder(\Magento\Framework\EntityManager\MetadataPool::class)
@@ -164,7 +165,7 @@ function ($select) {
'resource' => $resource,
'attributePrefix' => 'attr_',
'metadataPool' => $this->metadataPoolMock,
- 'tableMapper' => $this->tableMapper,
+ 'aliasResolver' => $this->aliasResolver,
]
);
}
@@ -234,7 +235,7 @@ public function testProcessStaticAttribute()
$this->attribute->method('getAttributeCode')
->willReturn('static_attribute');
- $this->tableMapper->expects($this->once())->method('getMappingAlias')
+ $this->aliasResolver->expects($this->once())->method('getAlias')
->willReturn('attr_table_alias');
$this->filter->expects($this->exactly(3))
->method('getField')
@@ -272,7 +273,7 @@ public function testProcessTermFilter($frontendInput, $fieldValue, $isNegation,
->method('getFrontendInput')
->willReturn($frontendInput);
- $this->tableMapper->expects($this->once())->method('getMappingAlias')
+ $this->aliasResolver->expects($this->once())->method('getAlias')
->willReturn('termAttrAlias');
$this->filter->expects($this->exactly(3))
diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php
new file mode 100644
index 0000000000000..9f133b763889b
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/FilterMapper/FilterContextTest.php
@@ -0,0 +1,236 @@
+eavConfig = $this->getMockBuilder(\Magento\Eav\Model\Config::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getAttribute'])
+ ->getMock();
+ $this->aliasResolver = $this->getMockBuilder(
+ AliasResolver::class
+ )
+ ->disableOriginalConstructor()
+ ->setMethods(['getAlias'])
+ ->getMock();
+ $this->exclusionStrategy = $this->getMockBuilder(ExclusionStrategy::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['apply'])
+ ->getMock();
+ $this->termDropdownStrategy = $this->getMockBuilder(TermDropdownStrategy::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['apply'])
+ ->getMock();
+ $this->staticAttributeStrategy = $this->getMockBuilder(StaticAttributeStrategy::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['apply'])
+ ->getMock();
+ $this->select = $this->getMockBuilder(\Magento\Framework\DB\Select::class)
+ ->disableOriginalConstructor()
+ ->setMethods([])
+ ->getMock();
+ $objectManager = new ObjectManager($this);
+ $this->filterContext = $objectManager->getObject(
+ FilterContext::class,
+ [
+ 'eavConfig' => $this->eavConfig,
+ 'aliasResolver' => $this->aliasResolver,
+ 'exclusionStrategy' => $this->exclusionStrategy,
+ 'termDropdownStrategy' => $this->termDropdownStrategy,
+ 'staticAttributeStrategy' => $this->staticAttributeStrategy,
+ ]
+ );
+ }
+
+ public function testApplyOnExclusionFilter()
+ {
+ $filter = $this->createFilterMock();
+ $this->exclusionStrategy->expects($this->once())
+ ->method('apply')
+ ->with($filter, $this->select)
+ ->willReturn(true);
+ $this->eavConfig->expects($this->never())->method('getAttribute');
+ $this->assertTrue($this->filterContext->apply($filter, $this->select));
+ }
+
+ public function testApplyFilterWithoutAttribute()
+ {
+ $filter = $this->createFilterMock('some_field');
+ $this->exclusionStrategy->expects($this->once())
+ ->method('apply')
+ ->with($filter, $this->select)
+ ->willReturn(false);
+ $this->eavConfig->expects($this->once())
+ ->method('getAttribute')
+ ->with(\Magento\Catalog\Model\Product::ENTITY, 'some_field')
+ ->willReturn(null);
+ $this->assertFalse($this->filterContext->apply($filter, $this->select));
+ }
+
+ public function testApplyOnTermFilterBySelect()
+ {
+ $filter = $this->createFilterMock('select_field', FilterInterface::TYPE_TERM);
+ $attribute = $this->createAttributeMock('select');
+ $this->eavConfig->expects($this->once())
+ ->method('getAttribute')
+ ->with(\Magento\Catalog\Model\Product::ENTITY, 'select_field')
+ ->willReturn($attribute);
+ $this->exclusionStrategy->expects($this->once())
+ ->method('apply')
+ ->with($filter, $this->select)
+ ->willReturn(false);
+ $this->termDropdownStrategy->expects($this->once())
+ ->method('apply')
+ ->with($filter, $this->select)
+ ->willReturn(true);
+ $this->assertTrue($this->filterContext->apply($filter, $this->select));
+ }
+
+ public function testApplyOnTermFilterByMultiSelect()
+ {
+ $filter = $this->createFilterMock('multiselect_field', FilterInterface::TYPE_TERM);
+ $attribute = $this->createAttributeMock('multiselect');
+ $this->eavConfig->expects($this->once())
+ ->method('getAttribute')
+ ->with(\Magento\Catalog\Model\Product::ENTITY, 'multiselect_field')
+ ->willReturn($attribute);
+ $this->exclusionStrategy->expects($this->once())
+ ->method('apply')
+ ->with($filter, $this->select)
+ ->willReturn(false);
+ $this->termDropdownStrategy->expects($this->once())
+ ->method('apply')
+ ->with($filter, $this->select)
+ ->willReturn(true);
+ $this->assertTrue($this->filterContext->apply($filter, $this->select));
+ }
+
+ public function testApplyOnTermFilterByStaticAttribute()
+ {
+ $filter = $this->createFilterMock('multiselect_field', FilterInterface::TYPE_TERM);
+ $attribute = $this->createAttributeMock('text', AbstractAttribute::TYPE_STATIC);
+ $this->eavConfig->expects($this->once())
+ ->method('getAttribute')
+ ->with(\Magento\Catalog\Model\Product::ENTITY, 'multiselect_field')
+ ->willReturn($attribute);
+ $this->exclusionStrategy->expects($this->once())
+ ->method('apply')
+ ->with($filter, $this->select)
+ ->willReturn(false);
+ $this->staticAttributeStrategy->expects($this->once())
+ ->method('apply')
+ ->with($filter, $this->select)
+ ->willReturn(true);
+ $this->assertTrue($this->filterContext->apply($filter, $this->select));
+ }
+
+ public function testApplyOnTermFilterByUnknownAttributeType()
+ {
+ $filter = $this->createFilterMock('multiselect_field', FilterInterface::TYPE_TERM);
+ $attribute = $this->createAttributeMock('text', 'text');
+ $this->eavConfig->expects($this->once())
+ ->method('getAttribute')
+ ->with(\Magento\Catalog\Model\Product::ENTITY, 'multiselect_field')
+ ->willReturn($attribute);
+ $this->exclusionStrategy->expects($this->once())
+ ->method('apply')
+ ->with($filter, $this->select)
+ ->willReturn(false);
+ $this->assertFalse($this->filterContext->apply($filter, $this->select));
+ }
+
+ /**
+ * @param string $field
+ * @param string $type
+ * @return FilterInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private function createFilterMock($field = null, $type = null)
+ {
+ $filter = $this->getMockBuilder(FilterInterface::class)
+ ->setMethods(['getField', 'getType'])
+ ->getMockForAbstractClass();
+ $filter->expects($this->any())
+ ->method('getField')
+ ->willReturn($field);
+ $filter->expects($this->any())
+ ->method('getType')
+ ->willReturn($type);
+
+ return $filter;
+ }
+
+ /**
+ * @param string|null $frontendInput
+ * @param string|null $backendType
+ * @return Attribute|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private function createAttributeMock($frontendInput = null, $backendType = null)
+ {
+ $attribute = $this->getMockBuilder(Attribute::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getFrontendInput', 'getBackendType'])
+ ->getMock();
+ $attribute->expects($this->any())
+ ->method('getFrontendInput')
+ ->willReturn($frontendInput);
+ $attribute->expects($this->any())
+ ->method('getBackendType')
+ ->willReturn($backendType);
+ return $attribute;
+ }
+}
diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php
index dd9a556146ab3..dad4ad8095f5a 100644
--- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php
+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/TableMapperTest.php
@@ -6,6 +6,9 @@
namespace Magento\CatalogSearch\Test\Unit\Model\Search;
+use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection;
+use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory;
+use Magento\CatalogSearch\Model\Adapter\Mysql\Filter\AliasResolver;
use Magento\Framework\Search\Request\FilterInterface;
use Magento\Framework\Search\Request\QueryInterface;
use \Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
@@ -16,8 +19,10 @@
*/
class TableMapperTest extends \PHPUnit_Framework_TestCase
{
- const WEBSITE_ID = 4512;
- const STORE_ID = 2514;
+ /**
+ * @var AliasResolver|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $aliasResolver;
/**
* @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject
@@ -25,7 +30,7 @@ class TableMapperTest extends \PHPUnit_Framework_TestCase
private $eavConfig;
/**
- * @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection|\PHPUnit_Framework_MockObject_MockObject
+ * @var Collection|\PHPUnit_Framework_MockObject_MockObject
*/
private $attributeCollection;
@@ -59,11 +64,6 @@ class TableMapperTest extends \PHPUnit_Framework_TestCase
*/
private $resource;
- /**
- * @var \Magento\Store\Api\Data\StoreInterface|\PHPUnit_Framework_MockObject_MockObject
- */
- private $store;
-
/**
* @var \Magento\CatalogSearch\Model\Search\TableMapper
*/
@@ -76,65 +76,49 @@ protected function setUp()
$this->connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class)
->disableOriginalConstructor()
->getMock();
- $this->connection->expects($this->any())
- ->method('quoteInto')
- ->willReturnCallback(
- function ($query, $expression) {
- return str_replace('?', $expression, $query);
- }
- );
+ $this->connection->expects($this->never())->method('quoteInto');
$this->resource = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class)
->disableOriginalConstructor()
->getMock();
- $this->resource->method('getTableName')
- ->willReturnCallback(
- function ($table) {
- return 'prefix_' . $table;
- }
- );
- $this->resource->expects($this->any())
- ->method('getConnection')
- ->willReturn($this->connection);
+ $this->resource->expects($this->never())->method('getTableName');
+ $this->resource->expects($this->never())->method('getConnection');
$this->website = $this->getMockBuilder(\Magento\Store\Api\Data\WebsiteInterface::class)
->disableOriginalConstructor()
->getMockForAbstractClass();
- $this->website->expects($this->any())
- ->method('getId')
- ->willReturn(self::WEBSITE_ID);
- $this->store = $this->getMockBuilder(\Magento\Store\Api\Data\StoreInterface::class)
- ->disableOriginalConstructor()
- ->getMockForAbstractClass();
- $this->store->expects($this->any())
- ->method('getId')
- ->willReturn(self::STORE_ID);
+ $this->website->expects($this->never())->method('getId');
+
$this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class)
->disableOriginalConstructor()
->getMock();
- $this->storeManager->expects($this->any())
- ->method('getWebsite')
- ->willReturn($this->website);
- $this->storeManager->expects($this->any())
- ->method('getStore')
- ->willReturn($this->store);
- $this->attributeCollection = $this->getMockBuilder(
- \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection::class
- )
+ $this->storeManager->expects($this->never())->method('getWebsite');
+ $this->storeManager->expects($this->never())->method('getStore');
+
+ $this->attributeCollection = $this->getMockBuilder(Collection::class)
->disableOriginalConstructor()
->getMock();
- $attributeCollectionFactory = $this->getMockBuilder(
- \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory::class
- )
+ $attributeCollectionFactory = $this->getMockBuilder(CollectionFactory::class)
->setMethods(['create'])
->disableOriginalConstructor()
->getMock();
$attributeCollectionFactory->expects($this->never())
->method('create');
+
$this->eavConfig = $this->getMockBuilder(\Magento\Eav\Model\Config::class)
->setMethods(['getAttribute'])
->disableOriginalConstructor()
->getMock();
+
+ $this->aliasResolver = $this->getMockBuilder(AliasResolver::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->aliasResolver->expects($this->any())
+ ->method('getAlias')
+ ->willReturnCallback(function (FilterInterface $filter) {
+ return $filter->getField() . '_alias';
+ });
+
$this->target = $objectManager->getObject(
\Magento\CatalogSearch\Model\Search\TableMapper::class,
[
@@ -142,6 +126,7 @@ function ($table) {
'storeManager' => $this->storeManager,
'attributeCollectionFactory' => $attributeCollectionFactory,
'eavConfig' => $this->eavConfig,
+ 'aliasResolver' => $this->aliasResolver,
]
);
@@ -160,14 +145,7 @@ public function testAddPriceFilter()
$this->request->expects($this->once())
->method('getQuery')
->willReturn($query);
- $this->select->expects($this->once())
- ->method('joinInner')
- ->with(
- ['price_index' => 'prefix_catalog_product_index_price'],
- 'search_index.entity_id = price_index.entity_id AND price_index.website_id = ' . self::WEBSITE_ID,
- []
- )
- ->willReturnSelf();
+
$select = $this->target->addTables($this->select, $this->request);
$this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select');
}
@@ -176,18 +154,10 @@ public function testAddStaticAttributeFilter()
{
$priceFilter = $this->createRangeFilter('static');
$query = $this->createFilterQuery($priceFilter);
- $this->createAttributeMock('static', 'static', 'backend_table', 0, 'select');
$this->request->expects($this->once())
->method('getQuery')
->willReturn($query);
- $this->select->expects($this->once())
- ->method('joinInner')
- ->with(
- ['static_filter' => 'backend_table'],
- 'search_index.entity_id = static_filter.entity_id',
- null
- )
- ->willReturnSelf();
+
$select = $this->target->addTables($this->select, $this->request);
$this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select');
}
@@ -199,46 +169,25 @@ public function testAddCategoryIds()
$this->request->expects($this->once())
->method('getQuery')
->willReturn($query);
- $this->select->expects($this->once())
- ->method('joinInner')
- ->with(
- ['category_ids_index' => 'prefix_catalog_category_product_index'],
- 'search_index.entity_id = category_ids_index.product_id',
- []
- )
- ->willReturnSelf();
+
$select = $this->target->addTables($this->select, $this->request);
$this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select');
}
public function testAddTermFilter()
{
- $this->createAttributeMock('color', null, null, 132, 'select', 0);
$categoryIdsFilter = $this->createTermFilter('color');
$query = $this->createFilterQuery($categoryIdsFilter);
$this->request->expects($this->once())
->method('getQuery')
->willReturn($query);
- $this->select->expects($this->once())
- ->method('joinLeft')
- ->with(
- ['color_filter' => 'prefix_catalog_product_index_eav'],
- 'search_index.entity_id = color_filter.entity_id'
- . ' AND color_filter.attribute_id = 132'
- . ' AND color_filter.store_id = 2514',
- []
- )
- ->willReturnSelf();
+
$select = $this->target->addTables($this->select, $this->request);
$this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select');
}
public function testAddBoolQueryWithTermFiltersInside()
{
- $this->createAttributeMock('must1', null, null, 101, 'select', 0);
- $this->createAttributeMock('should1', null, null, 102, 'select', 1);
- $this->createAttributeMock('mustNot1', null, null, 103, 'select', 2);
-
$query = $this->createBoolQuery(
[
$this->createFilterQuery($this->createTermFilter('must1')),
@@ -253,45 +202,13 @@ public function testAddBoolQueryWithTermFiltersInside()
$this->request->expects($this->once())
->method('getQuery')
->willReturn($query);
- $this->select->expects($this->at(0))
- ->method('joinLeft')
- ->with(
- ['must1_filter' => 'prefix_catalog_product_index_eav'],
- 'search_index.entity_id = must1_filter.entity_id'
- . ' AND must1_filter.attribute_id = 101'
- . ' AND must1_filter.store_id = 2514',
- []
- )
- ->willReturnSelf();
- $this->select->expects($this->at(1))
- ->method('joinLeft')
- ->with(
- ['should1_filter' => 'prefix_catalog_product_index_eav'],
- 'search_index.entity_id = should1_filter.entity_id'
- . ' AND should1_filter.attribute_id = 102'
- . ' AND should1_filter.store_id = 2514',
- []
- )
- ->willReturnSelf();
- $this->select->expects($this->at(2))
- ->method('joinLeft')
- ->with(
- ['mustNot1_filter' => 'prefix_catalog_product_index_eav'],
- 'search_index.entity_id = mustNot1_filter.entity_id'
- . ' AND mustNot1_filter.attribute_id = 103'
- . ' AND mustNot1_filter.store_id = 2514',
- []
- )
- ->willReturnSelf();
+
$select = $this->target->addTables($this->select, $this->request);
$this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select');
}
public function testAddBoolQueryWithTermAndPriceFiltersInside()
{
- $this->createAttributeMock('must1', null, null, 101, 'select', 0);
- $this->createAttributeMock('should1', null, null, 102, 'select', 1);
- $this->createAttributeMock('mustNot1', null, null, 103, 'select', 2);
$query = $this->createBoolQuery(
[
$this->createFilterQuery($this->createTermFilter('must1')),
@@ -307,53 +224,13 @@ public function testAddBoolQueryWithTermAndPriceFiltersInside()
$this->request->expects($this->once())
->method('getQuery')
->willReturn($query);
- $this->select->expects($this->at(0))
- ->method('joinLeft')
- ->with(
- ['must1_filter' => 'prefix_catalog_product_index_eav'],
- 'search_index.entity_id = must1_filter.entity_id'
- . ' AND must1_filter.attribute_id = 101'
- . ' AND must1_filter.store_id = 2514',
- []
- )
- ->willReturnSelf();
- $this->select->expects($this->at(1))
- ->method('joinInner')
- ->with(
- ['price_index' => 'prefix_catalog_product_index_price'],
- 'search_index.entity_id = price_index.entity_id AND price_index.website_id = ' . self::WEBSITE_ID,
- []
- )
- ->willReturnSelf();
- $this->select->expects($this->at(2))
- ->method('joinLeft')
- ->with(
- ['should1_filter' => 'prefix_catalog_product_index_eav'],
- 'search_index.entity_id = should1_filter.entity_id'
- . ' AND should1_filter.attribute_id = 102'
- . ' AND should1_filter.store_id = 2514',
- []
- )
- ->willReturnSelf();
- $this->select->expects($this->at(3))
- ->method('joinLeft')
- ->with(
- ['mustNot1_filter' => 'prefix_catalog_product_index_eav'],
- 'search_index.entity_id = mustNot1_filter.entity_id'
- . ' AND mustNot1_filter.attribute_id = 103'
- . ' AND mustNot1_filter.store_id = 2514',
- []
- )
- ->willReturnSelf();
+
$select = $this->target->addTables($this->select, $this->request);
$this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select');
}
public function testAddBoolFilterWithTermFiltersInside()
{
- $this->createAttributeMock('must1', null, null, 101, 'select', 0);
- $this->createAttributeMock('should1', null, null, 102, 'select', 1);
- $this->createAttributeMock('mustNot1', null, null, 103, 'select', 2);
$query = $this->createFilterQuery(
$this->createBoolFilter(
[
@@ -370,45 +247,13 @@ public function testAddBoolFilterWithTermFiltersInside()
$this->request->expects($this->once())
->method('getQuery')
->willReturn($query);
- $this->select->expects($this->at(0))
- ->method('joinLeft')
- ->with(
- ['must1_filter' => 'prefix_catalog_product_index_eav'],
- 'search_index.entity_id = must1_filter.entity_id'
- . ' AND must1_filter.attribute_id = 101'
- . ' AND must1_filter.store_id = 2514',
- []
- )
- ->willReturnSelf();
- $this->select->expects($this->at(1))
- ->method('joinLeft')
- ->with(
- ['should1_filter' => 'prefix_catalog_product_index_eav'],
- 'search_index.entity_id = should1_filter.entity_id'
- . ' AND should1_filter.attribute_id = 102'
- . ' AND should1_filter.store_id = 2514',
- []
- )
- ->willReturnSelf();
- $this->select->expects($this->at(2))
- ->method('joinLeft')
- ->with(
- ['mustNot1_filter' => 'prefix_catalog_product_index_eav'],
- 'search_index.entity_id = mustNot1_filter.entity_id'
- . ' AND mustNot1_filter.attribute_id = 103'
- . ' AND mustNot1_filter.store_id = 2514',
- []
- )
- ->willReturnSelf();
+
$select = $this->target->addTables($this->select, $this->request);
$this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select');
}
public function testAddBoolFilterWithBoolFiltersInside()
{
- $this->createAttributeMock('must1', null, null, 101, 'select', 0);
- $this->createAttributeMock('should1', null, null, 102, 'select', 1);
- $this->createAttributeMock('mustNot1', null, null, 103, 'select', 2);
$query = $this->createFilterQuery(
$this->createBoolFilter(
[
@@ -425,36 +270,7 @@ public function testAddBoolFilterWithBoolFiltersInside()
$this->request->expects($this->once())
->method('getQuery')
->willReturn($query);
- $this->select->expects($this->at(0))
- ->method('joinLeft')
- ->with(
- ['must1_filter' => 'prefix_catalog_product_index_eav'],
- 'search_index.entity_id = must1_filter.entity_id'
- . ' AND must1_filter.attribute_id = 101'
- . ' AND must1_filter.store_id = 2514',
- []
- )
- ->willReturnSelf();
- $this->select->expects($this->at(1))
- ->method('joinLeft')
- ->with(
- ['should1_filter' => 'prefix_catalog_product_index_eav'],
- 'search_index.entity_id = should1_filter.entity_id'
- . ' AND should1_filter.attribute_id = 102'
- . ' AND should1_filter.store_id = 2514',
- []
- )
- ->willReturnSelf();
- $this->select->expects($this->at(2))
- ->method('joinLeft')
- ->with(
- ['mustNot1_filter' => 'prefix_catalog_product_index_eav'],
- 'search_index.entity_id = mustNot1_filter.entity_id'
- . ' AND mustNot1_filter.attribute_id = 103'
- . ' AND mustNot1_filter.store_id = 2514',
- []
- )
- ->willReturnSelf();
+
$select = $this->target->addTables($this->select, $this->request);
$this->assertEquals($this->select, $select, 'Returned results isn\'t equal to passed select');
}
@@ -472,6 +288,7 @@ private function createFilterQuery($filter)
->willReturn(QueryInterface::TYPE_FILTER);
$query->method('getReference')
->willReturn($filter);
+
return $query;
}
@@ -495,6 +312,7 @@ private function createBoolQuery(array $must, array $should, array $mustNot)
->willReturn($should);
$query->method('getMustNot')
->willReturn($mustNot);
+
return $query;
}
@@ -518,6 +336,7 @@ private function createBoolFilter(array $must, array $should, array $mustNot)
->willReturn($should);
$query->method('getMustNot')
->willReturn($mustNot);
+
return $query;
}
@@ -532,6 +351,7 @@ private function createRangeFilter($field)
FilterInterface::TYPE_RANGE,
$field
);
+
return $filter;
}
@@ -546,6 +366,7 @@ private function createTermFilter($field)
FilterInterface::TYPE_TERM,
$field
);
+
return $filter;
}
@@ -564,40 +385,7 @@ private function createFilterMock($class, $type, $field)
->willReturn($type);
$filter->method('getField')
->willReturn($field);
- return $filter;
- }
- /**
- * @param string $code
- * @param string $backendType
- * @param string $backendTable
- * @param int $attributeId
- * @param string $frontendInput
- * @param int $positionInCollection
- */
- private function createAttributeMock(
- $code,
- $backendType = null,
- $backendTable = null,
- $attributeId = 120,
- $frontendInput = 'select',
- $positionInCollection = 0
- ) {
- $attribute = $this->getMockBuilder(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class)
- ->setMethods(['getBackendType', 'getBackendTable', 'getId', 'getFrontendInput'])
- ->disableOriginalConstructor()
- ->getMock();
- $attribute->method('getId')
- ->willReturn($attributeId);
- $attribute->method('getBackendType')
- ->willReturn($backendType);
- $attribute->method('getBackendTable')
- ->willReturn($backendTable);
- $attribute->method('getFrontendInput')
- ->willReturn($frontendInput);
- $this->eavConfig->expects($this->at($positionInCollection))
- ->method('getAttribute')
- ->with(\Magento\Catalog\Model\Product::ENTITY, $code)
- ->willReturn($attribute);
+ return $filter;
}
}
diff --git a/app/code/Magento/CatalogSearch/etc/di.xml b/app/code/Magento/CatalogSearch/etc/di.xml
index f62b4e47767a2..7e9451f9b83e7 100644
--- a/app/code/Magento/CatalogSearch/etc/di.xml
+++ b/app/code/Magento/CatalogSearch/etc/di.xml
@@ -11,6 +11,7 @@
+
Magento\CatalogSearch\Model\ResourceModel\EngineInterface::CONFIG_ENGINE_PATH
diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php
index 4e6fa8ae5210d..fbb69798ff94f 100644
--- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php
+++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php
@@ -8,6 +8,7 @@
namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price;
use Magento\Catalog\Api\Data\ProductInterface;
+use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus;
class Configurable extends \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice
{
@@ -166,6 +167,9 @@ protected function _applyConfigurableOption()
$this->_prepareConfigurableOptionAggregateTable();
$this->_prepareConfigurableOptionPriceTable();
+ $statusAttribute = $this->_getAttribute(ProductInterface::STATUS);
+ $linkField = $metadata->getLinkField();
+
$select = $connection->select()->from(
['i' => $this->_getDefaultFinalPriceTable()],
[]
@@ -175,7 +179,7 @@ protected function _applyConfigurableOption()
['parent_id' => 'e.entity_id']
)->join(
['l' => $this->getTable('catalog_product_super_link')],
- 'l.parent_id = e.' . $metadata->getLinkField(),
+ 'l.parent_id = e.' . $linkField,
['product_id']
)->columns(
['customer_group_id', 'website_id'],
@@ -186,11 +190,21 @@ protected function _applyConfigurableOption()
[]
)->where(
'le.required_options=0'
+ )->join(
+ ['product_status' => $this->getTable($statusAttribute->getBackend()->getTable())],
+ sprintf(
+ 'le.%1$s = product_status.%1$s AND product_status.attribute_id = %2$s',
+ $linkField,
+ $statusAttribute->getAttributeId()
+ ),
+ []
+ )->where(
+ 'product_status.value=' . ProductStatus::STATUS_ENABLED
)->group(
- ['parent_id', 'i.customer_group_id', 'i.website_id', 'l.product_id']
+ ['e.entity_id', 'i.customer_group_id', 'i.website_id', 'l.product_id']
);
- $priceColumn = $this->_addAttributeToSelect($select, 'price', 'l.product_id', 0, null, true);
- $tierPriceColumn = $connection->getCheckSql("MIN(i.tier_price) IS NOT NULL", "i.tier_price", 'NULL');
+ $priceColumn = $this->_addAttributeToSelect($select, 'price', 'le.' . $linkField, 0, null, true);
+ $tierPriceColumn = $connection->getIfNullSql('MIN(i.tier_price)', 'NULL');
$select->columns(
['price' => $priceColumn, 'tier_price' => $tierPriceColumn]
diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php
index 77bf7af2a4031..59e12d72ded6c 100644
--- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php
+++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php
@@ -84,7 +84,7 @@ protected function _extractData(
$result = $metadataForm->compactData($formData);
// Re-initialize additional attributes
- $formData = array_replace($formData, $result);
+ $formData = array_replace($result, $formData);
// Unset unused attributes
$formAttributes = $metadataForm->getAttributes();
diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Save.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Save.php
index 57477b5ce0623..efa45512acc83 100644
--- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Save.php
+++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/Save.php
@@ -26,8 +26,13 @@ public function execute()
['request' => $this->getRequest()]
);
$data = $this->getRequest()->getPostValue();
+
+ $filterValues = ['from_date' => $this->_dateFilter];
+ if ($this->getRequest()->getParam('to_date')) {
+ $filterValues['to_date'] = $this->_dateFilter;
+ }
$inputFilter = new \Zend_Filter_Input(
- ['from_date' => $this->_dateFilter, 'to_date' => $this->_dateFilter],
+ $filterValues,
[],
$data
);
diff --git a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows-grid.js b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows-grid.js
index de10c065d123b..518f09fa73ba6 100644
--- a/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows-grid.js
+++ b/app/code/Magento/Ui/view/base/web/js/dynamic-rows/dynamic-rows-grid.js
@@ -52,13 +52,15 @@ define([
obj;
if (this.recordData().length && !this.update) {
- this.recordData.each(function (recordData) {
+ _.each(this.recordData(), function (recordData) {
obj = {};
obj[this.map[this.identificationProperty]] = recordData[this.identificationProperty];
insertData.push(obj);
}, this);
- this.source.set(this.dataProvider, insertData);
+ if (insertData.length) {
+ this.source.set(this.dataProvider, insertData);
+ }
}
},
@@ -178,7 +180,7 @@ define([
tmpObj = {};
if (data.length !== this.relatedData.length) {
- data.forEach(function (obj) {
+ _.each(data, function (obj) {
tmpObj[this.identificationDRProperty] = obj[this.identificationDRProperty];
if (!_.findWhere(this.relatedData, tmpObj)) {
@@ -193,7 +195,7 @@ define([
/**
* Processing insert data
*
- * @param {Array} data
+ * @param {Object} data
*/
processingInsertData: function (data) {
var changes,
diff --git a/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/templates/collapsible.html b/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/templates/collapsible.html
index f3319a05525f2..d1ec1d26df6c5 100644
--- a/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/templates/collapsible.html
+++ b/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/templates/collapsible.html
@@ -43,6 +43,7 @@
diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/Repository/BundleProduct/BundleSelections.xml b/dev/tests/functional/tests/app/Magento/Bundle/Test/Repository/BundleProduct/BundleSelections.xml
index 3e9b055c48ca5..7131aab3cffcb 100644
--- a/dev/tests/functional/tests/app/Magento/Bundle/Test/Repository/BundleProduct/BundleSelections.xml
+++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/Repository/BundleProduct/BundleSelections.xml
@@ -665,5 +665,170 @@
+
+
+
+ -
+
- Option 1
+ - Drop-down
+ - No
+ -
+
-
+
-
+
- %product_name%
+
+ -
+
- 1
+ - 1
+ - Fixed
+
+
+
+
+ -
+
- Option 2
+ - Radio Buttons
+ - No
+ -
+
-
+
-
+
- %product_name%
+
+ -
+
- 20
+ - 20
+ - Fixed
+
+
+ -
+
-
+
- %product_name%
+
+ -
+
- 21
+ - 21
+ - Fixed
+
+
+ -
+
-
+
- %product_name%
+
+ -
+
- 22
+ - 22
+ - Fixed
+
+
+
+
+ -
+
- Option 3
+ - Drop-down
+ - No
+ -
+
-
+
-
+
- %product_name%
+
+ -
+
- 3
+ - 3
+ - Fixed
+
+
+
+
+
+
+ -
+
- catalogProductSimple::default
+
+ -
+
- catalogProductSimple::default
+ - catalogProductSimple::product_100_dollar
+ - catalogProductSimple::product_without_category
+
+ -
+
- catalogProductSimple::default
+
+
+
+
+
+
+ -
+
- Option 1
+ - Drop-down
+ - No
+
+
+
+ -
+
- Option 2
+ - Radio Buttons
+ - No
+ -
+
-
+
-
+
- %product_name%
+
+ -
+
- 20
+ - 20
+ - Fixed
+
+
+ -
+
-
+
- %product_name%
+
+ -
+
- 21
+ - 21
+ - Fixed
+
+
+ -
+
-
+
- %product_name%
+
+ -
+
- 22
+ - 22
+ - Fixed
+
+
+
+
+ -
+
- Option 3
+ - Drop-down
+ - No
+ -
+
-
+
-
+
- %product_name%
+
+ -
+
- 3
+ - 3
+ - Fixed
+
+
+
+
+
+
+ -
+
- catalogProductSimple::default
+ - catalogProductSimple::product_100_dollar
+ - catalogProductSimple::product_without_category
+
+ -
+
- catalogProductSimple::default
+
+
+
diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/UpdateBundleOptionsTest.php b/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/UpdateBundleOptionsTest.php
new file mode 100644
index 0000000000000..3c17360200f44
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/UpdateBundleOptionsTest.php
@@ -0,0 +1,94 @@
+Inventory>Catalog
+ * 3. Click on Bundle product in the grid to edit it
+ * 4. Fill in some Bundle Options data according to data set
+ * 5. Delete some Bundle Options data according to data set
+ * 6. Save product
+ * 7. Verify Bundle Options in the updated product
+ *
+ * @group Bundle_Product
+ * @ZephyrId MAGETWO-26195
+ */
+class UpdateBundleOptionsTest extends Injectable
+{
+ /* tags */
+ const MVP = 'yes';
+ /* end tags */
+
+ /**
+ * Page product on backend
+ *
+ * @var CatalogProductIndex
+ */
+ protected $catalogProductIndex;
+
+ /**
+ * Edit page on backend
+ *
+ * @var CatalogProductEdit
+ */
+ protected $catalogProductEdit;
+
+ /**
+ * Injection data
+ *
+ * @param CatalogProductIndex $catalogProductIndexNewPage
+ * @param CatalogProductEdit $catalogProductEditPage
+ * @return void
+ */
+ public function __inject(
+ CatalogProductIndex $catalogProductIndexNewPage,
+ CatalogProductEdit $catalogProductEditPage
+ ) {
+ $this->catalogProductIndex = $catalogProductIndexNewPage;
+ $this->catalogProductEdit = $catalogProductEditPage;
+ }
+
+ /**
+ * Test update bundle product
+ *
+ * @param BundleProduct $product
+ * @param BundleProduct $originalProduct
+ * @return void
+ */
+ public function test(BundleProduct $product, BundleProduct $originalProduct)
+ {
+ // Preconditions
+ $originalProduct->persist();
+
+ // Steps
+ $filter = ['sku' => $originalProduct->getSku()];
+
+ $this->catalogProductIndex->open();
+ $this->catalogProductIndex->getProductGrid()->searchAndOpen($filter);
+
+ $form = $this->catalogProductEdit->getProductForm();
+ $form->openSection('bundle');
+ $container = $form->getSection('bundle');
+ $containerFields = $product->getData()['bundle_selections']['bundle_options_delete'];
+ $container->deleteFieldsData($containerFields);
+
+ $form->openSection('product-details');
+ $container = $form->getSection('product-details');
+ $containerFields = $product->getData();
+ unset($containerFields['bundle_selections']);
+ $container->setFieldsData($containerFields);
+
+ $this->catalogProductEdit->getFormPageActions()->save();
+ }
+}
diff --git a/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/UpdateBundleOptionsTest.xml b/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/UpdateBundleOptionsTest.xml
new file mode 100644
index 0000000000000..795665d66d2bf
--- /dev/null
+++ b/dev/tests/functional/tests/app/Magento/Bundle/Test/TestCase/UpdateBundleOptionsTest.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+ Update bundle product options
+ with_3_bundle_options
+ bundle_3_options_%isolation%
+ bundle_3_options_%isolation%
+ with_3_options_delete
+
+
+
+
\ No newline at end of file
diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertConfigurableProductPage.php b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertConfigurableProductPage.php
index 62d6c72efe8c6..ad9807b76d6e5 100644
--- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertConfigurableProductPage.php
+++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Constraint/AssertConfigurableProductPage.php
@@ -125,15 +125,23 @@ protected function verifyAttributesMatrix($variationsMatrix, $generatedMatrix)
protected function getLowestConfigurablePrice()
{
$price = null;
- $configurableOptions = $this->product->getConfigurableAttributesData();
-
- foreach ($configurableOptions['matrix'] as $option) {
- $price = $price === null ? $option['price'] : $price;
- if ($price > $option['price']) {
- $price = $option['price'];
+ $priceDataConfig = $this->product->getDataFieldConfig('price');
+ if (isset($priceDataConfig['source'])) {
+ $priceData = $priceDataConfig['source']->getPriceData();
+ if (isset($priceData['price_from'])) {
+ $price = $priceData['price_from'];
}
}
+ if (null === $price) {
+ $configurableOptions = $this->product->getConfigurableAttributesData();
+ foreach ($configurableOptions['matrix'] as $option) {
+ $price = $price === null ? $option['price'] : $price;
+ if ($price > $option['price']) {
+ $price = $option['price'];
+ }
+ }
+ }
return $price;
}
}
diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct/Price.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct/Price.xml
index 2e9d708dd42d8..1b6282b9432c3 100644
--- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct/Price.xml
+++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/Repository/ConfigurableProduct/Price.xml
@@ -17,5 +17,8 @@
11
+
+ 9
+
diff --git a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml
index 58435b066261e..187283da4602f 100644
--- a/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml
+++ b/dev/tests/functional/tests/app/Magento/ConfigurableProduct/Test/TestCase/CreateConfigurableProductEntityTest.xml
@@ -58,6 +58,7 @@
configurable_two_new_options_with_special_price
Configurable Product %isolation%
configurable_sku_%isolation%
+ from-9
100
9
Configurable short description
diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCartPriceRuleForm.php b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCartPriceRuleForm.php
index bfe0bc9eabddf..1f3f0e5a300b4 100644
--- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCartPriceRuleForm.php
+++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Constraint/AssertCartPriceRuleForm.php
@@ -25,8 +25,6 @@ class AssertCartPriceRuleForm extends AbstractConstraint
protected $skippedFields = [
'conditions_serialized',
'actions_serialized',
- 'from_date',
- 'to_date',
'rule_id'
];
diff --git a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml
index 13b68aca6c6eb..3a35215d841d5 100644
--- a/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml
+++ b/dev/tests/functional/tests/app/Magento/SalesRule/Test/Repository/SalesRule.xml
@@ -114,10 +114,10 @@
13
63
- - 3/25/2014
+ - 03/25/2014
- - 6/29/2024
+ - -
1
Yes
diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php
new file mode 100644
index 0000000000000..5db97cc366149
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php
@@ -0,0 +1,70 @@
+storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class);
+ $this->productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class);
+ }
+
+ /**
+ * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php
+ */
+ public function testGetProductFinalPriceIfOneOfChildIsDisabled()
+ {
+ /** @var Collection $collection */
+ $collection = Bootstrap::getObjectManager()->get(CollectionFactory::class)
+ ->create();
+ $configurableProduct = $collection
+ ->addIdFilter([1])
+ ->addMinimalPrice()
+ ->load()
+ ->getFirstItem();
+ $this->assertEquals(10, $configurableProduct->getMinimalPrice());
+
+ $childProduct = $this->productRepository->getById(10, false, null, true);
+ $childProduct->setStatus(Status::STATUS_DISABLED);
+ // update in global scope
+ $currentStoreId = $this->storeManager->getStore()->getId();
+ $this->storeManager->setCurrentStore(Store::ADMIN_CODE);
+ $this->productRepository->save($childProduct);
+ $this->storeManager->setCurrentStore($currentStoreId);
+
+ /** @var Collection $collection */
+ $collection = Bootstrap::getObjectManager()->get(CollectionFactory::class)
+ ->create();
+ $configurableProduct = $collection
+ ->addIdFilter([1])
+ ->addMinimalPrice()
+ ->load()
+ ->getFirstItem();
+ $this->assertEquals(20, $configurableProduct->getMinimalPrice());
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php
index 1576ff90cb38e..5f9b9e928b317 100644
--- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php
+++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php
@@ -313,6 +313,8 @@ public function testSaveActionExistingCustomerAndExistingAddressData()
$this->assertEquals(2, count($addresses));
$updatedAddress = $this->addressRepository->getById(1);
$this->assertEquals('update firstname', $updatedAddress->getFirstname());
+ $this->assertTrue($updatedAddress->isDefaultBilling());
+ $this->assertEquals($updatedAddress->getId(), $customer->getDefaultBilling());
$newAddress = $this->accountManagement->getDefaultShippingAddress($customerId);
$this->assertEquals('new firstname', $newAddress->getFirstname());
diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/Adapter/Mysql/AdapterTest.php b/dev/tests/integration/testsuite/Magento/Framework/Search/Adapter/Mysql/AdapterTest.php
index 9acfa6f1caab1..67a6e41697349 100644
--- a/dev/tests/integration/testsuite/Magento/Framework/Search/Adapter/Mysql/AdapterTest.php
+++ b/dev/tests/integration/testsuite/Magento/Framework/Search/Adapter/Mysql/AdapterTest.php
@@ -414,6 +414,37 @@ public function testAdvancedSearchDateField($rangeFilter, $expectedRecordsCount)
$this->assertEquals($expectedRecordsCount, $queryResponse->count());
}
+ /**
+ * @magentoDataFixture Magento/Framework/Search/_files/product_configurable.php
+ * @magentoConfigFixture current_store catalog/search/engine mysql
+ */
+ public function testAdvancedSearchCompositeProductWithOutOfStockOption()
+ {
+ /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */
+ $attribute = $this->objectManager->get(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class)
+ ->loadByCode(\Magento\Catalog\Model\Product::ENTITY, 'test_configurable');
+ /** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection $selectOptions */
+ $selectOptions = $this->objectManager
+ ->create(\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection::class)
+ ->setAttributeFilter($attribute->getId());
+
+ $firstOption = $selectOptions->getFirstItem();
+ $firstOptionId = $firstOption->getId();
+ $this->requestBuilder->bind('test_configurable', $firstOptionId);
+ $this->requestBuilder->setRequestName('filter_out_of_stock_child');
+
+ $queryResponse = $this->executeQuery();
+ $this->assertEquals(0, $queryResponse->count());
+
+ $secondOption = $selectOptions->getLastItem();
+ $secondOptionId = $secondOption->getId();
+ $this->requestBuilder->bind('test_configurable', $secondOptionId);
+ $this->requestBuilder->setRequestName('filter_out_of_stock_child');
+
+ $queryResponse = $this->executeQuery();
+ $this->assertEquals(1, $queryResponse->count());
+ }
+
public function dateDataProvider()
{
return [
diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute.php
new file mode 100644
index 0000000000000..030e0250c3ec7
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute.php
@@ -0,0 +1,61 @@
+get(\Magento\Eav\Model\Config::class);
+$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable');
+
+$eavConfig->clear();
+
+/** @var $installer \Magento\Catalog\Setup\CategorySetup */
+$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class);
+
+if (!$attribute->getId()) {
+
+ /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */
+ $attribute = Bootstrap::getObjectManager()->create(
+ \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class
+ );
+
+ /** @var AttributeRepositoryInterface $attributeRepository */
+ $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class);
+
+ $attribute->setData(
+ [
+ 'attribute_code' => 'test_configurable',
+ 'entity_type_id' => $installer->getEntityTypeId('catalog_product'),
+ 'is_global' => 1,
+ 'is_user_defined' => 1,
+ 'frontend_input' => 'select',
+ 'is_unique' => 0,
+ 'is_required' => 0,
+ 'is_searchable' => 1,
+ 'is_visible_in_advanced_search' => 1,
+ 'is_comparable' => 0,
+ 'is_filterable' => 1,
+ 'is_filterable_in_search' => 1,
+ 'is_used_for_promo_rules' => 0,
+ 'is_html_allowed_on_front' => 1,
+ 'is_visible_on_front' => 0,
+ 'used_in_product_listing' => 0,
+ 'used_for_sort_by' => 0,
+ 'frontend_label' => ['Test Configurable'],
+ 'backend_type' => 'int',
+ 'option' => [
+ 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']],
+ 'order' => ['option_0' => 1, 'option_1' => 2],
+ ],
+ ]
+ );
+
+ $attributeRepository->save($attribute);
+}
+
+/* Assign attribute to attribute set */
+$installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId());
+$eavConfig->clear();
diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute_rollback.php
new file mode 100644
index 0000000000000..bd18100f6d97b
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/configurable_attribute_rollback.php
@@ -0,0 +1,28 @@
+get(\Magento\Framework\Registry::class);
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', true);
+$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
+ ->get(\Magento\Catalog\Model\ResourceModel\Product\Collection::class);
+foreach ($productCollection as $product) {
+ $product->delete();
+}
+
+$eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class);
+$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable');
+if ($attribute instanceof \Magento\Eav\Model\Entity\Attribute\AbstractAttribute
+ && $attribute->getId()
+) {
+ $attribute->delete();
+}
+$eavConfig->clear();
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', false);
diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php
new file mode 100644
index 0000000000000..590f3c8041c6d
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable.php
@@ -0,0 +1,147 @@
+reinitialize();
+
+require __DIR__ . '/configurable_attribute.php';
+
+/** @var ProductRepositoryInterface $productRepository */
+$productRepository = Bootstrap::getObjectManager()
+ ->create(ProductRepositoryInterface::class);
+
+/** @var $installer CategorySetup */
+$installer = Bootstrap::getObjectManager()->create(CategorySetup::class);
+
+/* Create simple products per each option value*/
+/** @var AttributeOptionInterface[] $options */
+$options = $attribute->getOptions();
+
+$attributeValues = [];
+$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default');
+$associatedProductIds = [];
+$productIds = [10, 20];
+array_shift($options); //remove the first option which is empty
+
+$isFirstOption = true;
+foreach ($options as $option) {
+ /** @var $product Product */
+ $product = Bootstrap::getObjectManager()->create(Product::class);
+ $productId = array_shift($productIds);
+ $product->setTypeId(Type::TYPE_SIMPLE)
+ ->setId($productId)
+ ->setAttributeSetId($attributeSetId)
+ ->setWebsiteIds([1])
+ ->setName('Configurable Option' . $option->getLabel())
+ ->setSku('simple_' . $productId)
+ ->setPrice($productId)
+ ->setTestConfigurable($option->getValue())
+ ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE)
+ ->setStatus(Status::STATUS_ENABLED)
+ ->setStockData(
+ [
+ 'use_config_manage_stock' => 1,
+ 'qty' => 100,
+ 'is_qty_decimal' => 0,
+ 'is_in_stock' => (int)!$isFirstOption,
+ ]
+ );
+
+ $product = $productRepository->save($product);
+
+ /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */
+ $stockItem = Bootstrap::getObjectManager()->create(\Magento\CatalogInventory\Model\Stock\Item::class);
+ $stockItem->load($productId, 'product_id');
+
+ if (!$stockItem->getProductId()) {
+ $stockItem->setProductId($productId);
+ }
+ $stockItem->setUseConfigManageStock(1);
+ $stockItem->setQty(1000);
+ $stockItem->setIsQtyDecimal(0);
+ $stockItem->setIsInStock((int)!$isFirstOption);
+ $stockItem->save();
+
+ $attributeValues[] = [
+ 'label' => 'test',
+ 'attribute_id' => $attribute->getId(),
+ 'value_index' => $option->getValue(),
+ ];
+ $associatedProductIds[] = $product->getId();
+ $isFirstOption = false;
+}
+
+/** @var $product Product */
+$product = Bootstrap::getObjectManager()->create(Product::class);
+
+/** @var Factory $optionsFactory */
+$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class);
+
+$configurableAttributesData = [
+ [
+ 'attribute_id' => $attribute->getId(),
+ 'code' => $attribute->getAttributeCode(),
+ 'label' => $attribute->getStoreLabel(),
+ 'position' => '0',
+ 'values' => $attributeValues,
+ ],
+];
+
+$configurableOptions = $optionsFactory->create($configurableAttributesData);
+
+$extensionConfigurableAttributes = $product->getExtensionAttributes();
+$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions);
+$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds);
+
+$product->setExtensionAttributes($extensionConfigurableAttributes);
+
+// Remove any previously created product with the same id.
+/** @var \Magento\Framework\Registry $registry */
+$registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class);
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', true);
+try {
+ $productToDelete = $productRepository->getById(1);
+ $productRepository->delete($productToDelete);
+
+ /** @var \Magento\Quote\Model\ResourceModel\Quote\Item $itemResource */
+ $itemResource = Bootstrap::getObjectManager()->get(\Magento\Quote\Model\ResourceModel\Quote\Item::class);
+ $itemResource->getConnection()->delete(
+ $itemResource->getMainTable(),
+ 'product_id = ' . $productToDelete->getId()
+ );
+} catch (\Exception $e) {
+ // Nothing to remove
+}
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', false);
+
+$product->setTypeId(Configurable::TYPE_CODE)
+ ->setId(1)
+ ->setAttributeSetId($attributeSetId)
+ ->setWebsiteIds([1])
+ ->setName('Configurable Product')
+ ->setSku('configurable')
+ ->setVisibility(Visibility::VISIBILITY_BOTH)
+ ->setStatus(Status::STATUS_ENABLED)
+ ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]);
+
+$productRepository->save($product);
+//
+///** @var \Magento\Catalog\Model\Indexer\Product\Eav\Processor $eavIndexer */
+//$eavIndexer = Bootstrap::getObjectManager()
+// ->get(\Magento\Catalog\Model\Indexer\Product\Eav\Processor::class);
+//$eavIndexer->reindexAll();
diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_rollback.php b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_rollback.php
new file mode 100644
index 0000000000000..8ba4e3abe21cc
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/product_configurable_rollback.php
@@ -0,0 +1,36 @@
+get(\Magento\Framework\Registry::class);
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', true);
+
+/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */
+$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
+ ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class);
+
+foreach (['simple_10', 'simple_20', 'configurable'] as $sku) {
+ try {
+ $product = $productRepository->get($sku, false, null, true);
+
+ $stockStatus = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Status::class);
+ $stockStatus->load($product->getEntityId(), 'product_id');
+ $stockStatus->delete();
+
+ $productRepository->delete($product);
+ } catch (\Magento\Framework\Exception\NoSuchEntityException $e) {
+ //Product already removed
+ }
+}
+
+require __DIR__ . '/configurable_attribute_rollback.php';
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', false);
diff --git a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/requests.xml b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/requests.xml
index 0cdfa11c3b6b5..4cdc9a93e1f1f 100644
--- a/dev/tests/integration/testsuite/Magento/Framework/Search/_files/requests.xml
+++ b/dev/tests/integration/testsuite/Magento/Framework/Search/_files/requests.xml
@@ -387,4 +387,24 @@
0
10
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+ 10
+
+
diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Search/RequestConfigTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Search/RequestConfigTest.php
index 52244463a5356..ff12618b39963 100644
--- a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Search/RequestConfigTest.php
+++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Framework/Search/RequestConfigTest.php
@@ -100,7 +100,7 @@ public function testFileSchemaUsingInvalidXml($expectedErrors = null)
Element 'filterReference': The attribute 'ref' is required but missing.
Element 'filter': The attribute 'field' is required but missing.
Element 'metric', attribute 'type': [facet 'enumeration'] " .
- "The value 'sumasdasd' is not an element of the set {'sum', 'count', 'min', 'max'}.
+ "The value 'sumasdasd' is not an element of the set {'sum', 'count', 'min', 'max', 'avg'}.
Element 'metric', attribute 'type': 'sumasdasd' is not a valid value of the local atomic type.
Element 'bucket': Missing child element(s). Expected is one of ( metrics, ranges ).
Element 'request': Missing child element(s). Expected is ( from )."
diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Adapter.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Adapter.php
index 8ddb6255b9b86..3d280f1224b8f 100644
--- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Adapter.php
+++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Adapter.php
@@ -71,6 +71,7 @@ public function __construct(
/**
* {@inheritdoc}
+ * @throws \LogicException
*/
public function query(RequestInterface $request)
{
diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Metrics.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Metrics.php
index 5860591eaf702..cf544eb126e11 100644
--- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Metrics.php
+++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/Aggregation/Builder/Metrics.php
@@ -14,7 +14,7 @@ class Metrics
*
* @var string[]
*/
- private $mapMetrics = ['count', 'sum', 'min', 'max', 'avg'];
+ private $allowedMetrics = ['count', 'sum', 'min', 'max', 'avg'];
/**
* Build metrics for Select->columns
@@ -30,7 +30,7 @@ public function build(RequestBucketInterface $bucket)
foreach ($metrics as $metric) {
$metricType = $metric->getType();
- if (in_array($metricType, $this->mapMetrics)) {
+ if (in_array($metricType, $this->allowedMetrics, true)) {
$selectAggregations[$metricType] = "$metricType(main_table.value)";
}
}
diff --git a/lib/internal/Magento/Framework/Search/etc/requests.xsd b/lib/internal/Magento/Framework/Search/etc/requests.xsd
index f185699c5a5e8..294232513b7d2 100644
--- a/lib/internal/Magento/Framework/Search/etc/requests.xsd
+++ b/lib/internal/Magento/Framework/Search/etc/requests.xsd
@@ -263,6 +263,7 @@
+