escapeHtmlAttr($position) : '' ?>>
+
escapeHtmlAttr($position) : '' ?>>
isSaleable()) :?>
getAddToCartPostParams($_product); ?>
isAvailable()) :?>
-
= $block->escapeHtml(__('In stock')) ?>
+
= $escaper->escapeHtml(__('In stock')) ?>
-
= $block->escapeHtml(__('Out of stock')) ?>
+
= $escaper->escapeHtml(__('Out of stock')) ?>
-
escapeHtmlAttr($position) : '' ?>>
+
escapeHtmlAttr($position) : '' ?>>
getChildBlock('addto')) :?>
= $addToBlock->setProduct($_product)->getChildHtml() ?>
@@ -112,9 +116,9 @@ $_helper = $this->helper(Magento\Catalog\Helper\Output::class);
= /* @noEscape */ $_helper->productAttribute($_product, $_product->getShortDescription(), 'short_description') ?>
-
= $block->escapeHtml(__('Learn More')) ?>
+ class="action more">= $escaper->escapeHtml(__('Learn More')) ?>
@@ -130,7 +134,7 @@ $_helper = $this->helper(Magento\Catalog\Helper\Output::class);
{
"[data-role=tocart-form], .form.map.checkout": {
"catalogAddToCart": {
- "product_sku": "= $block->escapeJs($_product->getSku()) ?>"
+ "product_sku": "= $escaper->escapeJs($_product->getSku()) ?>"
}
}
}
diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml
index b776fd4f7e193..6cebd51284f48 100644
--- a/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml
+++ b/app/code/Magento/Catalog/view/frontend/templates/product/listing.phtml
@@ -16,7 +16,6 @@
*/
?>
getLoadedProductCollection();
$_helper = $this->helper(Magento\Catalog\Helper\Output::class);
?>
@@ -98,4 +97,3 @@ $_helper = $this->helper(Magento\Catalog\Helper\Output::class);
= $block->getToolbarHtml() ?>
-= $time_taken = microtime(true) - $start ?>
diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml
index c6d351b2a9571..25257f4bcea8a 100644
--- a/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml
+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/form.phtml
@@ -22,7 +22,7 @@
-
+
= $block->getBlockHtml('formkey') ?>
= $block->getChildHtml('form_top') ?>
hasOptions()) :?>
diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml
index eb2bde647f9b1..4bfdbb7bc24bc 100644
--- a/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml
+++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/opengraph/general.phtml
@@ -9,13 +9,16 @@
+ content="= $block->escapeHtmlAttr($block->stripTags($block->getProduct()->getName())) ?>" />
+ content="= $block->escapeHtmlAttr($block->stripTags($block->getProduct()->getShortDescription())) ?>" />
-getProduct()->getPriceInfo()->getPrice(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE)->getAmount()) :?>
+getProduct()
+ ->getPriceInfo()
+ ->getPrice(\Magento\Catalog\Pricing\Price\FinalPrice::PRICE_CODE)
+ ->getAmount()):?>
= $block->getChildHtml('meta.currency') ?>
diff --git a/app/code/Magento/Catalog/view/frontend/web/js/gallery.js b/app/code/Magento/Catalog/view/frontend/web/js/gallery.js
index f6be6fd58ca25..2b3349c25c917 100644
--- a/app/code/Magento/Catalog/view/frontend/web/js/gallery.js
+++ b/app/code/Magento/Catalog/view/frontend/web/js/gallery.js
@@ -3,18 +3,10 @@
* See COPYING.txt for license details.
*/
-(function (factory) {
- 'use strict';
-
- if (typeof define === 'function' && define.amd) {
- define([
- 'jquery',
- 'jquery-ui-modules/widget'
- ], factory);
- } else {
- factory(jQuery);
- }
-}(function ($) {
+define([
+ 'jquery',
+ 'jquery-ui-modules/widget'
+], function ($) {
'use strict';
$.widget('mage.gallery', {
@@ -49,4 +41,4 @@
});
return $.mage.gallery;
-}));
+});
diff --git a/app/code/Magento/Catalog/view/frontend/web/js/related-products.js b/app/code/Magento/Catalog/view/frontend/web/js/related-products.js
index 822dd5b9a7b13..c875dd8f5d2c7 100644
--- a/app/code/Magento/Catalog/view/frontend/web/js/related-products.js
+++ b/app/code/Magento/Catalog/view/frontend/web/js/related-products.js
@@ -26,8 +26,8 @@ define([
* @private
*/
_create: function () {
- $(this.options.selectAllLink).on('click', $.proxy(this._selectAllRelated, this));
- $(this.options.relatedCheckbox).on('click', $.proxy(this._addRelatedToProduct, this));
+ $(this.options.selectAllLink, this.element).on('click', $.proxy(this._selectAllRelated, this));
+ $(this.options.relatedCheckbox, this.element).on('click', $.proxy(this._addRelatedToProduct, this));
this._showRelatedProducts(
this.element.find(this.options.elementsSelector),
this.element.data('limit'),
diff --git a/app/code/Magento/Catalog/view/frontend/web/product/view/validation.js b/app/code/Magento/Catalog/view/frontend/web/product/view/validation.js
index ab1753e7b9ed3..3205e58297b6c 100644
--- a/app/code/Magento/Catalog/view/frontend/web/product/view/validation.js
+++ b/app/code/Magento/Catalog/view/frontend/web/product/view/validation.js
@@ -3,19 +3,11 @@
* See COPYING.txt for license details.
*/
-(function (factory) {
- 'use strict';
-
- if (typeof define === 'function' && define.amd) {
- define([
- 'jquery',
- 'jquery-ui-modules/widget',
- 'mage/validation/validation'
- ], factory);
- } else {
- factory(jQuery);
- }
-}(function ($) {
+define([
+ 'jquery',
+ 'jquery-ui-modules/widget',
+ 'mage/validation/validation'
+], function ($) {
'use strict';
$.widget('mage.validation', $.mage.validation, {
@@ -97,4 +89,4 @@
});
return $.mage.validation;
-}));
+});
diff --git a/app/code/Magento/CatalogCmsGraphQl/Model/Resolver/Category/Block.php b/app/code/Magento/CatalogCmsGraphQl/Model/Resolver/Category/Block.php
new file mode 100644
index 0000000000000..110d03cac7735
--- /dev/null
+++ b/app/code/Magento/CatalogCmsGraphQl/Model/Resolver/Category/Block.php
@@ -0,0 +1,65 @@
+blockProvider = $blockProvider;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ if (!isset($value['model'])) {
+ throw new LocalizedException(__('"model" value should be specified'));
+ }
+ /** @var Category $category */
+ $category = $value['model'];
+ $blockId = $category->getLandingPage();
+
+ if (empty($blockId)) {
+ return null;
+ }
+
+ try {
+ $block = $this->blockProvider->getData($blockId);
+ } catch (NoSuchEntityException $e) {
+ return null;
+ }
+
+ return $block;
+ }
+}
diff --git a/app/code/Magento/CatalogCmsGraphQl/README.md b/app/code/Magento/CatalogCmsGraphQl/README.md
new file mode 100644
index 0000000000000..f3b36e515ac6e
--- /dev/null
+++ b/app/code/Magento/CatalogCmsGraphQl/README.md
@@ -0,0 +1,3 @@
+# CatalogCmsGraphQl
+
+**CatalogCmsGraphQl** provides type and resolver information for GraphQL attributes that have dependencies on the Catalog and Cms modules.
\ No newline at end of file
diff --git a/app/code/Magento/CatalogCmsGraphQl/composer.json b/app/code/Magento/CatalogCmsGraphQl/composer.json
new file mode 100644
index 0000000000000..a9d6ee4d9f2f1
--- /dev/null
+++ b/app/code/Magento/CatalogCmsGraphQl/composer.json
@@ -0,0 +1,28 @@
+{
+ "name": "magento/module-catalog-cms-graph-ql",
+ "description": "N/A",
+ "type": "magento2-module",
+ "require": {
+ "php": "~7.1.3||~7.2.0||~7.3.0",
+ "magento/framework": "*",
+ "magento/module-catalog": "*",
+ "magento/module-cms-graph-ql": "*"
+ },
+ "suggest": {
+ "magento/module-graph-ql": "*",
+ "magento/module-cms": "*",
+ "magento/module-catalog-graph-ql": "*"
+ },
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Magento\\CatalogCmsGraphQl\\": ""
+ }
+ }
+}
diff --git a/app/code/Magento/CatalogCmsGraphQl/etc/graphql/di.xml b/app/code/Magento/CatalogCmsGraphQl/etc/graphql/di.xml
new file mode 100644
index 0000000000000..cc8d8f9845c51
--- /dev/null
+++ b/app/code/Magento/CatalogCmsGraphQl/etc/graphql/di.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ -
+
- landing_page
+
+
+
+
+
diff --git a/app/code/Magento/CatalogCmsGraphQl/etc/module.xml b/app/code/Magento/CatalogCmsGraphQl/etc/module.xml
new file mode 100644
index 0000000000000..40cc556bbf713
--- /dev/null
+++ b/app/code/Magento/CatalogCmsGraphQl/etc/module.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogCmsGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogCmsGraphQl/etc/schema.graphqls
new file mode 100644
index 0000000000000..0fc5f69a009a4
--- /dev/null
+++ b/app/code/Magento/CatalogCmsGraphQl/etc/schema.graphqls
@@ -0,0 +1,6 @@
+# Copyright © Magento, Inc. All rights reserved.
+# See COPYING.txt for license details.
+
+interface CategoryInterface {
+ cms_block: CmsBlock @doc(description: "Category CMS Block.") @resolver(class: "Magento\\CatalogCmsGraphQl\\Model\\Resolver\\Category\\Block")
+}
\ No newline at end of file
diff --git a/app/code/Magento/CatalogCmsGraphQl/registration.php b/app/code/Magento/CatalogCmsGraphQl/registration.php
new file mode 100644
index 0000000000000..c2b95cd9a4c5d
--- /dev/null
+++ b/app/code/Magento/CatalogCmsGraphQl/registration.php
@@ -0,0 +1,9 @@
+groupManagement = $groupManagement;
+ $this->customerRepository = $customerRepository;
+ }
+
+ /**
+ * Get customer group by id
+ *
+ * @param int|null $customerId
+ * @return int
+ * @throws GraphQlNoSuchEntityException
+ */
+ public function execute(?int $customerId): int
+ {
+ if (!$customerId) {
+ $customerGroupId = GroupManagement::NOT_LOGGED_IN_ID;
+ } else {
+ try {
+ $customer = $this->customerRepository->getById($customerId);
+ } catch (NoSuchEntityException $e) {
+ throw new GraphQlNoSuchEntityException(
+ __('Customer with id "%customer_id" does not exist.', ['customer_id' => $customerId]),
+ $e
+ );
+ }
+ $customerGroupId = $customer->getGroupId();
+ }
+ return (int)$customerGroupId;
+ }
+}
diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php
new file mode 100644
index 0000000000000..4e75139c1a882
--- /dev/null
+++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php
@@ -0,0 +1,151 @@
+valueFactory = $valueFactory;
+ $this->tiersFactory = $tiersFactory;
+ $this->getCustomerGroup = $getCustomerGroup;
+ $this->discount = $discount;
+ $this->priceProviderPool = $priceProviderPool;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ if (!isset($value['model'])) {
+ throw new LocalizedException(__('"model" value should be specified'));
+ }
+
+ if (empty($this->tiers)) {
+ $this->customerGroupId = $this->getCustomerGroup->execute($context->getUserId());
+ $this->tiers = $this->tiersFactory->create(['customerGroupId' => $this->customerGroupId]);
+ }
+
+ $product = $value['model'];
+ $productId = $product->getId();
+ $this->tiers->addProductFilter($productId);
+
+ return $this->valueFactory->create(
+ function () use ($productId, $context) {
+ /** @var StoreInterface $store */
+ $store = $context->getExtensionAttributes()->getStore();
+
+ $productPrice = $this->tiers->getProductRegularPrice($productId) ?? 0.0;
+ $tierPrices = $this->tiers->getProductTierPrices($productId) ?? [];
+
+ return $this->formatProductTierPrices($tierPrices, $productPrice, $store);
+ }
+ );
+ }
+
+ /**
+ * Format tier prices for output
+ *
+ * @param ProductTierPriceInterface[] $tierPrices
+ * @param float $productPrice
+ * @param StoreInterface $store
+ * @return array
+ */
+ private function formatProductTierPrices(array $tierPrices, float $productPrice, StoreInterface $store): array
+ {
+ $tiers = [];
+
+ foreach ($tierPrices as $tierPrice) {
+ $percentValue = $tierPrice->getExtensionAttributes()->getPercentageValue();
+ if ($percentValue && is_numeric($percentValue)) {
+ $discount = $this->discount->getDiscountByPercent($productPrice, (float)$percentValue);
+ } else {
+ $discount = $this->discount->getDiscountByDifference($productPrice, (float)$tierPrice->getValue());
+ }
+
+ $tiers[] = [
+ "discount" => $discount,
+ "quantity" => $tierPrice->getQty(),
+ "final_price" => [
+ "value" => $tierPrice->getValue(),
+ "currency" => $store->getCurrentCurrencyCode()
+ ]
+ ];
+ }
+ return $tiers;
+ }
+}
diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Product/Price/Tiers.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Product/Price/Tiers.php
new file mode 100644
index 0000000000000..73a2ba83d5096
--- /dev/null
+++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Product/Price/Tiers.php
@@ -0,0 +1,176 @@
+collectionFactory = $collectionFactory;
+ $this->productResource = $productResource;
+ $this->priceProviderPool = $priceProviderPool;
+ $this->customerGroupId = $customerGroupId;
+ }
+
+ /**
+ * Add product ID to collection filter
+ *
+ * @param int $productId
+ */
+ public function addProductFilter($productId): void
+ {
+ $this->filterProductIds[] = $productId;
+ }
+
+ /**
+ * Get tier prices for product by ID
+ *
+ * @param int $productId
+ * @return ProductTierPriceInterface[]|null
+ */
+ public function getProductTierPrices($productId): ?array
+ {
+ if (!$this->isLoaded()) {
+ $this->load();
+ }
+
+ if (empty($this->products[$productId])) {
+ return null;
+ }
+ return $this->products[$productId]->getTierPrices();
+ }
+
+ /**
+ * Get product regular price by ID
+ *
+ * @param int $productId
+ * @return float|null
+ */
+ public function getProductRegularPrice($productId): ?float
+ {
+ if (!$this->isLoaded()) {
+ $this->load();
+ }
+
+ if (empty($this->products[$productId])) {
+ return null;
+ }
+ $product = $this->products[$productId];
+ $priceProvider = $this->priceProviderPool->getProviderByProductType($product->getTypeId());
+ return $priceProvider->getRegularPrice($product)->getValue();
+ }
+
+ /**
+ * Check if collection has been loaded
+ *
+ * @return bool
+ */
+ public function isLoaded(): bool
+ {
+ $numFilterProductIds = count(array_unique($this->filterProductIds));
+ if ($numFilterProductIds > count($this->products)) {
+ //New products were added to the filter after load, so we should reload
+ return false;
+ }
+ return $this->loaded;
+ }
+
+ /**
+ * Load product collection
+ */
+ private function load(): void
+ {
+ $this->loaded = false;
+
+ $productIdField = $this->productResource->getEntityIdField();
+ /** @var Collection $productCollection */
+ $productCollection = $this->collectionFactory->create();
+ $productCollection->addFieldToFilter($productIdField, ['in' => $this->filterProductIds]);
+ $productCollection->addAttributeToSelect('price');
+ $productCollection->addAttributeToSelect('price_type');
+ $productCollection->load();
+ $productCollection->addTierPriceDataByGroupId($this->customerGroupId);
+
+ $this->setProducts($productCollection);
+ $this->loaded = true;
+ }
+
+ /**
+ * Set products from collection
+ *
+ * @param Collection $productCollection
+ */
+ private function setProducts(Collection $productCollection): void
+ {
+ $this->products = [];
+
+ foreach ($productCollection as $product) {
+ $this->products[$product->getId()] = $product;
+ }
+
+ $missingProducts = array_diff($this->filterProductIds, array_keys($this->products));
+ foreach (array_unique($missingProducts) as $missingProductId) {
+ $this->products[$missingProductId] = null;
+ }
+ }
+}
diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php
new file mode 100644
index 0000000000000..c449d0a2ba30b
--- /dev/null
+++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php
@@ -0,0 +1,97 @@
+valueFactory = $valueFactory;
+ $this->tiersFactory = $tiersFactory;
+ $this->getCustomerGroup = $getCustomerGroup;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ if (!isset($value['model'])) {
+ throw new LocalizedException(__('"model" value should be specified'));
+ }
+
+ if (null === $this->customerGroupId) {
+ $this->customerGroupId = $this->getCustomerGroup->execute($context->getUserId());
+ $this->tiers = $this->tiersFactory->create(['customerGroupId' => $this->customerGroupId]);
+ }
+
+ /** @var Product $product */
+ $product = $value['model'];
+ $productId = $product->getId();
+ $this->tiers->addProductFilter($productId);
+
+ return $this->valueFactory->create(
+ function () use ($productId, $context) {
+ $tierPrices = $this->tiers->getProductTierPrices($productId);
+
+ return $tierPrices ?? [];
+ }
+ );
+ }
+}
diff --git a/app/code/Magento/CatalogCustomerGraphQl/README.md b/app/code/Magento/CatalogCustomerGraphQl/README.md
new file mode 100644
index 0000000000000..525a1a4f76433
--- /dev/null
+++ b/app/code/Magento/CatalogCustomerGraphQl/README.md
@@ -0,0 +1,3 @@
+# CatalogCustomerGraphQl
+
+**CatalogCustomerGraphQl** provides type and resolver information for GraphQL attributes that have dependences on the Catalog and Customer modules.
\ No newline at end of file
diff --git a/app/code/Magento/CatalogCustomerGraphQl/composer.json b/app/code/Magento/CatalogCustomerGraphQl/composer.json
new file mode 100644
index 0000000000000..859a5c6235697
--- /dev/null
+++ b/app/code/Magento/CatalogCustomerGraphQl/composer.json
@@ -0,0 +1,25 @@
+{
+ "name": "magento/module-catalog-customer-graph-ql",
+ "description": "N/A",
+ "type": "magento2-module",
+ "require": {
+ "php": "~7.1.3||~7.2.0||~7.3.0",
+ "magento/framework": "*",
+ "magento/module-catalog": "*",
+ "magento/module-customer": "*",
+ "magento/module-catalog-graph-ql": "*",
+ "magento/module-store": "*"
+ },
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Magento\\CatalogCustomerGraphQl\\": ""
+ }
+ }
+}
diff --git a/app/code/Magento/CatalogCustomerGraphQl/etc/module.xml b/app/code/Magento/CatalogCustomerGraphQl/etc/module.xml
new file mode 100644
index 0000000000000..6131435258b58
--- /dev/null
+++ b/app/code/Magento/CatalogCustomerGraphQl/etc/module.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogCustomerGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogCustomerGraphQl/etc/schema.graphqls
new file mode 100644
index 0000000000000..17880584bf160
--- /dev/null
+++ b/app/code/Magento/CatalogCustomerGraphQl/etc/schema.graphqls
@@ -0,0 +1,22 @@
+# Copyright © Magento, Inc. All rights reserved.
+# See COPYING.txt for license details.
+
+interface ProductInterface {
+ tier_prices: [ProductTierPrices] @deprecated(reason: "Use price_tiers for product tier price information.") @doc(description: "An array of ProductTierPrices objects.") @resolver(class: "Magento\\CatalogCustomerGraphQl\\Model\\Resolver\\TierPrices")
+ price_tiers: [TierPrice] @doc(description: "An array of TierPrice objects.") @resolver(class: "Magento\\CatalogCustomerGraphQl\\Model\\Resolver\\PriceTiers")
+}
+
+type ProductTierPrices @doc(description: "ProductTierPrices is deprecated and has been replaced by TierPrice. The ProductTierPrices object defines a tier price, which is a quantity discount offered to a specific customer group.") {
+ customer_group_id: String @deprecated(reason: "customer_group_id is not relevant for storefront.") @doc(description: "The ID of the customer group.")
+ qty: Float @deprecated(reason: "ProductTierPrices is deprecated, use TierPrice.quantity.") @doc(description: "The number of items that must be purchased to qualify for tier pricing.")
+ value: Float @deprecated(reason: "ProductTierPrices is deprecated. Use TierPrice.final_price") @doc(description: "The price of the fixed price item.")
+ percentage_value: Float @deprecated(reason: "ProductTierPrices is deprecated. Use TierPrice.discount.") @doc(description: "The percentage discount of the item.")
+ website_id: Float @deprecated(reason: "website_id is not relevant for storefront.") @doc(description: "The ID assigned to the website.")
+}
+
+
+type TierPrice @doc(description: "A price based on the quantity purchased.") {
+ final_price: Money @doc(desription: "The price of the product at this tier.")
+ quantity: Float @doc(description: "The minimum number of items that must be purchased to qualify for this price tier.")
+ discount: ProductDiscount @doc(description: "The price discount that this tier represents.")
+}
diff --git a/app/code/Magento/CatalogCustomerGraphQl/registration.php b/app/code/Magento/CatalogCustomerGraphQl/registration.php
new file mode 100644
index 0000000000000..8176716d42ea0
--- /dev/null
+++ b/app/code/Magento/CatalogCustomerGraphQl/registration.php
@@ -0,0 +1,9 @@
+resourceConnection = $resourceConnection;
+ $this->metadataPool = $metadataPool;
+ $this->entityType = $entityType;
+ $this->linkedAttributes = $linkedAttributes;
+ $this->eavConfig = $eavConfig;
+ }
+
+ /**
+ * Form and return query to get eav entity $attributes for given $entityIds.
+ *
+ * If eav entities were not found, then data is fetching from $entityTableName.
+ *
+ * @param array $entityIds
+ * @param array $attributes
+ * @param int $storeId
+ * @return Select
+ * @throws \Zend_Db_Select_Exception
+ * @throws \Exception
+ */
+ public function getQuery(array $entityIds, array $attributes, int $storeId): Select
+ {
+ /** @var \Magento\Framework\EntityManager\EntityMetadataInterface $metadata */
+ $metadata = $this->metadataPool->getMetadata($this->entityType);
+ $entityTableName = $metadata->getEntityTable();
+
+ /** @var \Magento\Framework\DB\Adapter\AdapterInterface $connection */
+ $connection = $this->resourceConnection->getConnection();
+ $entityTableAttributes = \array_keys($connection->describeTable($entityTableName));
+
+ $attributeMetadataTable = $this->resourceConnection->getTableName('eav_attribute');
+ $eavAttributes = $this->getEavAttributeCodes($attributes, $entityTableAttributes);
+ $entityTableAttributes = \array_intersect($attributes, $entityTableAttributes);
+
+ $eavAttributesMetaData = $this->getAttributesMetaData($connection, $attributeMetadataTable, $eavAttributes);
+
+ if ($eavAttributesMetaData) {
+ $select = $this->getEavAttributes(
+ $connection,
+ $metadata,
+ $entityTableAttributes,
+ $entityIds,
+ $eavAttributesMetaData,
+ $entityTableName,
+ $storeId
+ );
+ } else {
+ $select = $this->getAttributesFromEntityTable(
+ $connection,
+ $entityTableAttributes,
+ $entityIds,
+ $entityTableName
+ );
+ }
+
+ return $select;
+ }
+
+ /**
+ * Form and return query to get entity $entityTableAttributes for given $entityIds
+ *
+ * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection
+ * @param array $entityTableAttributes
+ * @param array $entityIds
+ * @param string $entityTableName
+ * @return Select
+ */
+ private function getAttributesFromEntityTable(
+ \Magento\Framework\DB\Adapter\AdapterInterface $connection,
+ array $entityTableAttributes,
+ array $entityIds,
+ string $entityTableName
+ ): Select {
+ $select = $connection->select()
+ ->from(['e' => $entityTableName], $entityTableAttributes)
+ ->where('e.entity_id IN (?)', $entityIds);
+
+ return $select;
+ }
+
+ /**
+ * Return ids of eav attributes by $eavAttributeCodes.
+ *
+ * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection
+ * @param string $attributeMetadataTable
+ * @param array $eavAttributeCodes
+ * @return array
+ */
+ private function getAttributesMetaData(
+ \Magento\Framework\DB\Adapter\AdapterInterface $connection,
+ string $attributeMetadataTable,
+ array $eavAttributeCodes
+ ): array {
+ $eavAttributeIdsSelect = $connection->select()
+ ->from(['a' => $attributeMetadataTable], ['attribute_id', 'backend_type', 'attribute_code'])
+ ->where('a.attribute_code IN (?)', $eavAttributeCodes)
+ ->where('a.entity_type_id = ?', $this->getEntityTypeId());
+
+ return $connection->fetchAssoc($eavAttributeIdsSelect);
+ }
+
+ /**
+ * Form and return query to get eav entity $attributes for given $entityIds.
+ *
+ * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection
+ * @param \Magento\Framework\EntityManager\EntityMetadataInterface $metadata
+ * @param array $entityTableAttributes
+ * @param array $entityIds
+ * @param array $eavAttributesMetaData
+ * @param string $entityTableName
+ * @param int $storeId
+ * @return Select
+ * @throws \Zend_Db_Select_Exception
+ */
+ private function getEavAttributes(
+ \Magento\Framework\DB\Adapter\AdapterInterface $connection,
+ \Magento\Framework\EntityManager\EntityMetadataInterface $metadata,
+ array $entityTableAttributes,
+ array $entityIds,
+ array $eavAttributesMetaData,
+ string $entityTableName,
+ int $storeId
+ ): Select {
+ $selects = [];
+ $attributeValueExpression = $connection->getCheckSql(
+ $connection->getIfNullSql('store_eav.value_id', -1) . ' > 0',
+ 'store_eav.value',
+ 'eav.value'
+ );
+ $linkField = $metadata->getLinkField();
+ $attributesPerTable = $this->getAttributeCodeTables($entityTableName, $eavAttributesMetaData);
+ foreach ($attributesPerTable as $attributeTable => $eavAttributes) {
+ $attributeCodeExpression = $this->buildAttributeCodeExpression($eavAttributes);
+
+ $selects[] = $connection->select()
+ ->from(['e' => $entityTableName], $entityTableAttributes)
+ ->joinLeft(
+ ['eav' => $this->resourceConnection->getTableName($attributeTable)],
+ \sprintf('e.%1$s = eav.%1$s', $linkField) .
+ $connection->quoteInto(' AND eav.attribute_id IN (?)', \array_keys($eavAttributesMetaData)) .
+ $connection->quoteInto(' AND eav.store_id = ?', \Magento\Store\Model\Store::DEFAULT_STORE_ID),
+ []
+ )
+ ->joinLeft(
+ ['store_eav' => $this->resourceConnection->getTableName($attributeTable)],
+ \sprintf(
+ 'e.%1$s = store_eav.%1$s AND store_eav.attribute_id = ' .
+ 'eav.attribute_id and store_eav.store_id = %2$d',
+ $linkField,
+ $storeId
+ ),
+ []
+ )
+ ->where('e.entity_id IN (?)', $entityIds)
+ ->columns(
+ [
+ 'attribute_code' => $attributeCodeExpression,
+ 'value' => $attributeValueExpression
+ ]
+ );
+ }
+
+ return $connection->select()->union($selects, Select::SQL_UNION_ALL);
+ }
+
+ /**
+ * Build expression for attribute code field.
+ *
+ * An example:
+ *
+ * ```
+ * CASE
+ * WHEN eav.attribute_id = '73' THEN 'name'
+ * WHEN eav.attribute_id = '121' THEN 'url_key'
+ * END
+ * ```
+ *
+ * @param array $eavAttributes
+ * @return \Zend_Db_Expr
+ */
+ private function buildAttributeCodeExpression(array $eavAttributes): \Zend_Db_Expr
+ {
+ $dbConnection = $this->resourceConnection->getConnection();
+ $expressionParts = ['CASE'];
+
+ foreach ($eavAttributes as $attribute) {
+ $expressionParts[]=
+ $dbConnection->quoteInto('WHEN eav.attribute_id = ?', $attribute['attribute_id'], \Zend_Db::INT_TYPE) .
+ $dbConnection->quoteInto(' THEN ?', $attribute['attribute_code'], 'string');
+ }
+
+ $expressionParts[]= 'END';
+
+ return new \Zend_Db_Expr(implode(' ', $expressionParts));
+ }
+
+ /**
+ * Get list of attribute tables.
+ *
+ * Returns result in the following format: *
+ * ```
+ * $attributeAttributeCodeTables = [
+ * 'm2_catalog_product_entity_varchar' =>
+ * '45' => [
+ * 'attribute_id' => 45,
+ * 'backend_type' => 'varchar',
+ * 'name' => attribute_code,
+ * ]
+ * ]
+ * ];
+ * ```
+ *
+ * @param string $entityTable
+ * @param array $eavAttributesMetaData
+ * @return array
+ */
+ private function getAttributeCodeTables($entityTable, $eavAttributesMetaData): array
+ {
+ $attributeAttributeCodeTables = [];
+ $metaTypes = \array_unique(\array_column($eavAttributesMetaData, 'backend_type'));
+
+ foreach ($metaTypes as $type) {
+ if (\in_array($type, self::SUPPORTED_BACKEND_TYPES, true)) {
+ $tableName = \sprintf('%s_%s', $entityTable, $type);
+ $attributeAttributeCodeTables[$tableName] = array_filter(
+ $eavAttributesMetaData,
+ function ($attribute) use ($type) {
+ return $attribute['backend_type'] === $type;
+ }
+ );
+ }
+ }
+
+ return $attributeAttributeCodeTables;
+ }
+
+ /**
+ * Get EAV attribute codes
+ * Remove attributes from entity table and attributes from exclude list
+ * Add linked attributes to output
+ *
+ * @param array $attributes
+ * @param array $entityTableAttributes
+ * @return array
+ */
+ private function getEavAttributeCodes($attributes, $entityTableAttributes): array
+ {
+ $attributes = \array_diff($attributes, $entityTableAttributes);
+ $unusedAttributeList = [];
+ $newAttributes = [];
+ foreach ($this->linkedAttributes as $attribute => $linkedAttributes) {
+ if (null === $linkedAttributes) {
+ $unusedAttributeList[] = $attribute;
+ } elseif (\is_array($linkedAttributes) && \in_array($attribute, $attributes, true)) {
+ $newAttributes[] = $linkedAttributes;
+ }
+ }
+ $attributes = \array_diff($attributes, $unusedAttributeList);
+
+ return \array_unique(\array_merge($attributes, ...$newAttributes));
+ }
+
+ /**
+ * Retrieve entity type id
+ *
+ * @return int
+ * @throws \Exception
+ */
+ private function getEntityTypeId(): int
+ {
+ if (!isset($this->entityTypeIdMap[$this->entityType])) {
+ $this->entityTypeIdMap[$this->entityType] = (int)$this->eavConfig->getEntityType(
+ $this->metadataPool->getMetadata($this->entityType)->getEavEntityType()
+ )->getId();
+ }
+
+ return $this->entityTypeIdMap[$this->entityType];
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Category/Query/CategoryAttributeQuery.php b/app/code/Magento/CatalogGraphQl/DataProvider/Category/Query/CategoryAttributeQuery.php
new file mode 100644
index 0000000000000..e3dfa38c78258
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Category/Query/CategoryAttributeQuery.php
@@ -0,0 +1,60 @@
+attributeQueryFactory = $attributeQueryFactory;
+ }
+
+ /**
+ * Form and return query to get eav attributes for given categories
+ *
+ * @param array $categoryIds
+ * @param array $categoryAttributes
+ * @param int $storeId
+ * @return Select
+ * @throws \Zend_Db_Select_Exception
+ */
+ public function getQuery(array $categoryIds, array $categoryAttributes, int $storeId): Select
+ {
+ $categoryAttributes = \array_merge($categoryAttributes, self::$requiredAttributes);
+
+ $attributeQuery = $this->attributeQueryFactory->create(
+ [
+ 'entityType' => CategoryInterface::class
+ ]
+ );
+
+ return $attributeQuery->getQuery($categoryIds, $categoryAttributes, $storeId);
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/CategoryAttributesMapper.php b/app/code/Magento/CatalogGraphQl/DataProvider/CategoryAttributesMapper.php
new file mode 100644
index 0000000000000..ea3c0b608d212
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/CategoryAttributesMapper.php
@@ -0,0 +1,117 @@
+graphqlConfig = $graphqlConfig;
+ }
+
+ /**
+ * Returns attribute values for given attribute codes.
+ *
+ * @param array $fetchResult
+ * @return array
+ */
+ public function getAttributesValues(array $fetchResult): array
+ {
+ $attributes = [];
+
+ foreach ($fetchResult as $row) {
+ if (!isset($attributes[$row['entity_id']])) {
+ $attributes[$row['entity_id']] = $row;
+ //TODO: do we need to introduce field mapping?
+ $attributes[$row['entity_id']]['id'] = $row['entity_id'];
+ }
+ if (isset($row['attribute_code'])) {
+ $attributes[$row['entity_id']][$row['attribute_code']] = $row['value'];
+ }
+ }
+
+ return $this->formatAttributes($attributes);
+ }
+
+ /**
+ * Format attributes that should be converted to array type
+ *
+ * @param array $attributes
+ * @return array
+ */
+ private function formatAttributes(array $attributes): array
+ {
+ $arrayTypeAttributes = $this->getFieldsOfArrayType();
+
+ return $arrayTypeAttributes
+ ? array_map(
+ function ($data) use ($arrayTypeAttributes) {
+ foreach ($arrayTypeAttributes as $attributeCode) {
+ $data[$attributeCode] = $this->valueToArray($data[$attributeCode] ?? null);
+ }
+ return $data;
+ },
+ $attributes
+ )
+ : $attributes;
+ }
+
+ /**
+ * Cast string to array
+ *
+ * @param string|null $value
+ * @return array
+ */
+ private function valueToArray($value): array
+ {
+ return $value ? \explode(',', $value) : [];
+ }
+
+ /**
+ * Get fields that should be converted to array type
+ *
+ * @return array
+ */
+ private function getFieldsOfArrayType(): array
+ {
+ $categoryTreeSchema = $this->graphqlConfig->getConfigElement('CategoryTree');
+ if (!$categoryTreeSchema instanceof Type) {
+ throw new \LogicException('CategoryTree type not defined in schema.');
+ }
+
+ $fields = [];
+ foreach ($categoryTreeSchema->getInterfaces() as $interface) {
+ /** @var InterfaceType $configElement */
+ $configElement = $this->graphqlConfig->getConfigElement($interface['interface']);
+
+ foreach ($configElement->getFields() as $field) {
+ if ($field->isList()) {
+ $fields[] = $field->getName();
+ }
+ }
+ }
+
+ return $fields;
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php
new file mode 100644
index 0000000000000..320e0adc29b9f
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php
@@ -0,0 +1,116 @@
+ [
+ * attribute_code => code,
+ * attribute_label => attribute label,
+ * option_label => option label,
+ * options => [option_id => 'option label', ...],
+ * ]
+ * ...
+ * ]
+ */
+class AttributeOptionProvider
+{
+ /**
+ * @var ResourceConnection
+ */
+ private $resourceConnection;
+
+ /**
+ * @param ResourceConnection $resourceConnection
+ */
+ public function __construct(ResourceConnection $resourceConnection)
+ {
+ $this->resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Get option data. Return list of attributes with option data
+ *
+ * @param array $optionIds
+ * @param array $attributeCodes
+ * @return array
+ * @throws \Zend_Db_Statement_Exception
+ */
+ public function getOptions(array $optionIds, array $attributeCodes = []): array
+ {
+ if (!$optionIds) {
+ return [];
+ }
+
+ $connection = $this->resourceConnection->getConnection();
+ $select = $connection->select()
+ ->from(
+ ['a' => $this->resourceConnection->getTableName('eav_attribute')],
+ [
+ 'attribute_id' => 'a.attribute_id',
+ 'attribute_code' => 'a.attribute_code',
+ 'attribute_label' => 'a.frontend_label',
+ ]
+ )
+ ->joinLeft(
+ ['options' => $this->resourceConnection->getTableName('eav_attribute_option')],
+ 'a.attribute_id = options.attribute_id',
+ []
+ )
+ ->joinLeft(
+ ['option_value' => $this->resourceConnection->getTableName('eav_attribute_option_value')],
+ 'options.option_id = option_value.option_id',
+ [
+ 'option_label' => 'option_value.value',
+ 'option_id' => 'option_value.option_id',
+ ]
+ );
+
+ $select->where('option_value.option_id IN (?)', $optionIds);
+
+ if (!empty($attributeCodes)) {
+ $select->orWhere(
+ 'a.attribute_code in (?) AND a.frontend_input = \'boolean\'',
+ $attributeCodes
+ );
+ }
+
+ return $this->formatResult($select);
+ }
+
+ /**
+ * Format result
+ *
+ * @param \Magento\Framework\DB\Select $select
+ * @return array
+ * @throws \Zend_Db_Statement_Exception
+ */
+ private function formatResult(\Magento\Framework\DB\Select $select): array
+ {
+ $statement = $this->resourceConnection->getConnection()->query($select);
+
+ $result = [];
+ while ($option = $statement->fetch()) {
+ if (!isset($result[$option['attribute_code']])) {
+ $result[$option['attribute_code']] = [
+ 'attribute_id' => $option['attribute_id'],
+ 'attribute_code' => $option['attribute_code'],
+ 'attribute_label' => $option['attribute_label'],
+ 'options' => [],
+ ];
+ }
+ $result[$option['attribute_code']]['options'][$option['option_id']] = $option['option_label'];
+ }
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php
new file mode 100644
index 0000000000000..0ec65c88024f2
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php
@@ -0,0 +1,159 @@
+attributeOptionProvider = $attributeOptionProvider;
+ $this->layerFormatter = $layerFormatter;
+ $this->bucketNameFilter = \array_merge($this->bucketNameFilter, $bucketNameFilter);
+ }
+
+ /**
+ * @inheritdoc
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ * @throws \Zend_Db_Statement_Exception
+ */
+ public function build(AggregationInterface $aggregation, ?int $storeId): array
+ {
+ $attributeOptions = $this->getAttributeOptions($aggregation);
+
+ // build layer per attribute
+ $result = [];
+ foreach ($this->getAttributeBuckets($aggregation) as $bucket) {
+ $bucketName = $bucket->getName();
+ $attributeCode = \preg_replace('~_bucket$~', '', $bucketName);
+ $attribute = $attributeOptions[$attributeCode] ?? [];
+
+ $result[$bucketName] = $this->layerFormatter->buildLayer(
+ $attribute['attribute_label'] ?? $bucketName,
+ \count($bucket->getValues()),
+ $attribute['attribute_code'] ?? $bucketName
+ );
+
+ foreach ($bucket->getValues() as $value) {
+ $metrics = $value->getMetrics();
+ $result[$bucketName]['options'][] = $this->layerFormatter->buildItem(
+ $attribute['options'][$metrics['value']] ?? $metrics['value'],
+ $metrics['value'],
+ $metrics['count']
+ );
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get attribute buckets excluding specified bucket names
+ *
+ * @param AggregationInterface $aggregation
+ * @return \Generator|BucketInterface[]
+ */
+ private function getAttributeBuckets(AggregationInterface $aggregation)
+ {
+ foreach ($aggregation->getBuckets() as $bucket) {
+ if (\in_array($bucket->getName(), $this->bucketNameFilter, true)) {
+ continue;
+ }
+ if ($this->isBucketEmpty($bucket)) {
+ continue;
+ }
+ yield $bucket;
+ }
+ }
+
+ /**
+ * Check that bucket contains data
+ *
+ * @param BucketInterface|null $bucket
+ * @return bool
+ */
+ private function isBucketEmpty(?BucketInterface $bucket): bool
+ {
+ return null === $bucket || !$bucket->getValues();
+ }
+
+ /**
+ * Get list of attributes with options
+ *
+ * @param AggregationInterface $aggregation
+ * @return array
+ * @throws \Zend_Db_Statement_Exception
+ */
+ private function getAttributeOptions(AggregationInterface $aggregation): array
+ {
+ $attributeOptionIds = [];
+ $attributes = [];
+ foreach ($this->getAttributeBuckets($aggregation) as $bucket) {
+ $attributes[] = \preg_replace('~_bucket$~', '', $bucket->getName());
+ $attributeOptionIds[] = \array_map(
+ function (AggregationValueInterface $value) {
+ return $value->getValue();
+ },
+ $bucket->getValues()
+ );
+ }
+
+ if (!$attributeOptionIds) {
+ return [];
+ }
+
+ return $this->attributeOptionProvider->getOptions(\array_merge(...$attributeOptionIds), $attributes);
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php
new file mode 100644
index 0000000000000..b0e67d72e25ba
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php
@@ -0,0 +1,151 @@
+ [
+ 'request_name' => 'category_id',
+ 'label' => 'Category'
+ ],
+ ];
+
+ /**
+ * @var CategoryAttributeQuery
+ */
+ private $categoryAttributeQuery;
+
+ /**
+ * @var CategoryAttributesMapper
+ */
+ private $attributesMapper;
+
+ /**
+ * @var ResourceConnection
+ */
+ private $resourceConnection;
+
+ /**
+ * @var RootCategoryProvider
+ */
+ private $rootCategoryProvider;
+
+ /**
+ * @var LayerFormatter
+ */
+ private $layerFormatter;
+
+ /**
+ * @param CategoryAttributeQuery $categoryAttributeQuery
+ * @param CategoryAttributesMapper $attributesMapper
+ * @param RootCategoryProvider $rootCategoryProvider
+ * @param ResourceConnection $resourceConnection
+ * @param LayerFormatter $layerFormatter
+ */
+ public function __construct(
+ CategoryAttributeQuery $categoryAttributeQuery,
+ CategoryAttributesMapper $attributesMapper,
+ RootCategoryProvider $rootCategoryProvider,
+ ResourceConnection $resourceConnection,
+ LayerFormatter $layerFormatter
+ ) {
+ $this->categoryAttributeQuery = $categoryAttributeQuery;
+ $this->attributesMapper = $attributesMapper;
+ $this->resourceConnection = $resourceConnection;
+ $this->rootCategoryProvider = $rootCategoryProvider;
+ $this->layerFormatter = $layerFormatter;
+ }
+
+ /**
+ * @inheritdoc
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Zend_Db_Select_Exception
+ */
+ public function build(AggregationInterface $aggregation, ?int $storeId): array
+ {
+ $bucket = $aggregation->getBucket(self::CATEGORY_BUCKET);
+ if ($this->isBucketEmpty($bucket)) {
+ return [];
+ }
+
+ $categoryIds = \array_map(
+ function (AggregationValueInterface $value) {
+ return (int)$value->getValue();
+ },
+ $bucket->getValues()
+ );
+
+ $categoryIds = \array_diff($categoryIds, [$this->rootCategoryProvider->getRootCategory($storeId)]);
+ $categoryLabels = \array_column(
+ $this->attributesMapper->getAttributesValues(
+ $this->resourceConnection->getConnection()->fetchAll(
+ $this->categoryAttributeQuery->getQuery($categoryIds, ['name'], $storeId)
+ )
+ ),
+ 'name',
+ 'entity_id'
+ );
+
+ if (!$categoryLabels) {
+ return [];
+ }
+
+ $result = $this->layerFormatter->buildLayer(
+ self::$bucketMap[self::CATEGORY_BUCKET]['label'],
+ \count($categoryIds),
+ self::$bucketMap[self::CATEGORY_BUCKET]['request_name']
+ );
+
+ foreach ($bucket->getValues() as $value) {
+ $categoryId = $value->getValue();
+ if (!\in_array($categoryId, $categoryIds, true)) {
+ continue ;
+ }
+ $result['options'][] = $this->layerFormatter->buildItem(
+ $categoryLabels[$categoryId] ?? $categoryId,
+ $categoryId,
+ $value->getMetrics()['count']
+ );
+ }
+
+ return [$result];
+ }
+
+ /**
+ * Check that bucket contains data
+ *
+ * @param BucketInterface|null $bucket
+ * @return bool
+ */
+ private function isBucketEmpty(?BucketInterface $bucket): bool
+ {
+ return null === $bucket || !$bucket->getValues();
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php
new file mode 100644
index 0000000000000..02b638edbdce8
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php
@@ -0,0 +1,88 @@
+ [
+ 'request_name' => 'price',
+ 'label' => 'Price'
+ ],
+ ];
+
+ /**
+ * @param LayerFormatter $layerFormatter
+ */
+ public function __construct(
+ LayerFormatter $layerFormatter
+ ) {
+ $this->layerFormatter = $layerFormatter;
+ }
+
+ /**
+ * @inheritdoc
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function build(AggregationInterface $aggregation, ?int $storeId): array
+ {
+ $bucket = $aggregation->getBucket(self::PRICE_BUCKET);
+ if ($this->isBucketEmpty($bucket)) {
+ return [];
+ }
+
+ $result = $this->layerFormatter->buildLayer(
+ self::$bucketMap[self::PRICE_BUCKET]['label'],
+ \count($bucket->getValues()),
+ self::$bucketMap[self::PRICE_BUCKET]['request_name']
+ );
+
+ foreach ($bucket->getValues() as $value) {
+ $metrics = $value->getMetrics();
+ $result['options'][] = $this->layerFormatter->buildItem(
+ \str_replace('_', '-', $metrics['value']),
+ $metrics['value'],
+ $metrics['count']
+ );
+ }
+
+ return [$result];
+ }
+
+ /**
+ * Check that bucket contains data
+ *
+ * @param BucketInterface|null $bucket
+ * @return bool
+ */
+ private function isBucketEmpty(?BucketInterface $bucket): bool
+ {
+ return null === $bucket || !$bucket->getValues();
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php
new file mode 100644
index 0000000000000..48a1265b10fc3
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php
@@ -0,0 +1,48 @@
+ $layerName,
+ 'count' => $itemsCount,
+ 'attribute_code' => $requestName
+ ];
+ }
+
+ /**
+ * Format layer item data
+ *
+ * @param string $label
+ * @param string|int $value
+ * @param string|int $count
+ * @return array
+ */
+ public function buildItem($label, $value, $count): array
+ {
+ return [
+ 'label' => $label,
+ 'value' => $value,
+ 'count' => $count,
+ ];
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php
new file mode 100644
index 0000000000000..ff661236be62f
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php
@@ -0,0 +1,43 @@
+builders = $builders;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function build(AggregationInterface $aggregation, ?int $storeId): array
+ {
+ $layers = [];
+ foreach ($this->builders as $builder) {
+ $layers[] = $builder->build($aggregation, $storeId);
+ }
+ $layers = \array_merge(...$layers);
+
+ return \array_filter($layers);
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilderInterface.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilderInterface.php
new file mode 100644
index 0000000000000..bd55bc6938b39
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilderInterface.php
@@ -0,0 +1,40 @@
+ 'layer name',
+ * 'filter_items_count' => 'filter items count',
+ * 'request_var' => 'filter name in request',
+ * 'filter_items' => [
+ * 'label' => 'item name',
+ * 'value_string' => 'item value, e.g. category ID',
+ * 'items_count' => 'product count',
+ * ],
+ * ],
+ * ...
+ * ];
+ */
+interface LayerBuilderInterface
+{
+ /**
+ * Build layer data
+ *
+ * @param AggregationInterface $aggregation
+ * @param int|null $storeId
+ * @return array [[{layer data}], ...]
+ */
+ public function build(AggregationInterface $aggregation, ?int $storeId): array;
+}
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/RootCategoryProvider.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/RootCategoryProvider.php
new file mode 100644
index 0000000000000..4b8a4a31b3c35
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/RootCategoryProvider.php
@@ -0,0 +1,55 @@
+resourceConnection = $resourceConnection;
+ }
+
+ /**
+ * Get root category for specified store id
+ *
+ * @param int $storeId
+ * @return int
+ */
+ public function getRootCategory(int $storeId): int
+ {
+ $connection = $this->resourceConnection->getConnection();
+
+ $select = $connection->select()
+ ->from(
+ ['store' => $this->resourceConnection->getTableName('store')],
+ []
+ )
+ ->join(
+ ['store_group' => $this->resourceConnection->getTableName('store_group')],
+ 'store.group_id = store_group.group_id',
+ ['root_category_id' => 'store_group.root_category_id']
+ )
+ ->where('store.store_id = ?', $storeId);
+
+ return (int)$connection->fetchOne($select);
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php
new file mode 100644
index 0000000000000..0e92bbbab4259
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php
@@ -0,0 +1,208 @@
+scopeConfig = $scopeConfig;
+ $this->filterBuilder = $filterBuilder;
+ $this->filterGroupBuilder = $filterGroupBuilder;
+ $this->builder = $builder;
+ $this->visibility = $visibility;
+ $this->sortOrderBuilder = $sortOrderBuilder;
+ }
+
+ /**
+ * Build search criteria
+ *
+ * @param array $args
+ * @param bool $includeAggregation
+ * @return SearchCriteriaInterface
+ */
+ public function build(array $args, bool $includeAggregation): SearchCriteriaInterface
+ {
+ $searchCriteria = $this->builder->build('products', $args);
+ $isSearch = !empty($args['search']);
+ $this->updateRangeFilters($searchCriteria);
+
+ if ($includeAggregation) {
+ $this->preparePriceAggregation($searchCriteria);
+ $requestName = 'graphql_product_search_with_aggregation';
+ } else {
+ $requestName = 'graphql_product_search';
+ }
+ $searchCriteria->setRequestName($requestName);
+
+ if ($isSearch) {
+ $this->addFilter($searchCriteria, 'search_term', $args['search']);
+ }
+
+ if (!$searchCriteria->getSortOrders()) {
+ $this->addDefaultSortOrder($searchCriteria, $isSearch);
+ }
+
+ $this->addVisibilityFilter($searchCriteria, $isSearch, !empty($args['filter']));
+
+ $searchCriteria->setCurrentPage($args['currentPage']);
+ $searchCriteria->setPageSize($args['pageSize']);
+
+ return $searchCriteria;
+ }
+
+ /**
+ * Add filter by visibility
+ *
+ * @param SearchCriteriaInterface $searchCriteria
+ * @param bool $isSearch
+ * @param bool $isFilter
+ */
+ private function addVisibilityFilter(SearchCriteriaInterface $searchCriteria, bool $isSearch, bool $isFilter): void
+ {
+ if ($isFilter && $isSearch) {
+ // Index already contains products filtered by visibility: catalog, search, both
+ return ;
+ }
+ $visibilityIds = $isSearch
+ ? $this->visibility->getVisibleInSearchIds()
+ : $this->visibility->getVisibleInCatalogIds();
+
+ $this->addFilter($searchCriteria, 'visibility', $visibilityIds);
+ }
+
+ /**
+ * Prepare price aggregation algorithm
+ *
+ * @param SearchCriteriaInterface $searchCriteria
+ * @return void
+ */
+ private function preparePriceAggregation(SearchCriteriaInterface $searchCriteria): void
+ {
+ $priceRangeCalculation = $this->scopeConfig->getValue(
+ \Magento\Catalog\Model\Layer\Filter\Dynamic\AlgorithmFactory::XML_PATH_RANGE_CALCULATION,
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE
+ );
+ if ($priceRangeCalculation) {
+ $this->addFilter($searchCriteria, 'price_dynamic_algorithm', $priceRangeCalculation);
+ }
+ }
+
+ /**
+ * Add filter to search criteria
+ *
+ * @param SearchCriteriaInterface $searchCriteria
+ * @param string $field
+ * @param mixed $value
+ */
+ private function addFilter(SearchCriteriaInterface $searchCriteria, string $field, $value): void
+ {
+ $filter = $this->filterBuilder
+ ->setField($field)
+ ->setValue($value)
+ ->create();
+ $this->filterGroupBuilder->addFilter($filter);
+ $filterGroups = $searchCriteria->getFilterGroups();
+ $filterGroups[] = $this->filterGroupBuilder->create();
+ $searchCriteria->setFilterGroups($filterGroups);
+ }
+
+ /**
+ * Sort by relevance DESC by default
+ *
+ * @param SearchCriteriaInterface $searchCriteria
+ * @param bool $isSearch
+ */
+ private function addDefaultSortOrder(SearchCriteriaInterface $searchCriteria, $isSearch = false): void
+ {
+ $sortField = $isSearch ? 'relevance' : EavAttributeInterface::POSITION;
+ $sortDirection = $isSearch ? SortOrder::SORT_DESC : SortOrder::SORT_ASC;
+ $defaultSortOrder = $this->sortOrderBuilder
+ ->setField($sortField)
+ ->setDirection($sortDirection)
+ ->create();
+
+ $searchCriteria->setSortOrders([$defaultSortOrder]);
+ }
+
+ /**
+ * Format range filters so replacement works
+ *
+ * Range filter fields in search request must replace value like '%field.from%' or '%field.to%'
+ *
+ * @param SearchCriteriaInterface $searchCriteria
+ */
+ private function updateRangeFilters(SearchCriteriaInterface $searchCriteria): void
+ {
+ $filterGroups = $searchCriteria->getFilterGroups();
+ foreach ($filterGroups as $filterGroup) {
+ $filters = $filterGroup->getFilters();
+ foreach ($filters as $filter) {
+ if (in_array($filter->getConditionType(), ['from', 'to'])) {
+ $filter->setField($filter->getField() . '.' . $filter->getConditionType());
+ }
+ }
+ }
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/AggregationOptionTypeResolver.php b/app/code/Magento/CatalogGraphQl/Model/AggregationOptionTypeResolver.php
new file mode 100644
index 0000000000000..3a532a1a6c760
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/AggregationOptionTypeResolver.php
@@ -0,0 +1,29 @@
+typeResolvers = $typeResolvers;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolveType(array $data) : string
+ {
+ /** @var TypeResolverInterface $typeResolver */
+ foreach ($this->typeResolvers as $typeResolver) {
+ $resolvedType = $typeResolver->resolveType($data);
+ if ($resolvedType) {
+ return $resolvedType;
+ }
+ }
+ throw new GraphQlInputException(__('Cannot resolve aggregation option type'));
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php
index d57154c429920..b3a9672a47010 100644
--- a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php
+++ b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php
@@ -20,6 +20,24 @@ class AttributesJoiner
*/
private $queryFields = [];
+ /**
+ * Field to attribute mapping
+ *
+ * For fields that are not named the same as their attribute, or require extra attributes to resolve
+ * e.g. ['field' => ['attr1', 'attr2'], 'other_field' => ['other_attr']]
+ *
+ * @var array
+ */
+ private $fieldToAttributeMap = [];
+
+ /**
+ * @param array $fieldToAttributeMap
+ */
+ public function __construct(array $fieldToAttributeMap = [])
+ {
+ $this->fieldToAttributeMap = $fieldToAttributeMap;
+ }
+
/**
* Join fields attached to field node to collection's select.
*
@@ -30,9 +48,7 @@ class AttributesJoiner
public function join(FieldNode $fieldNode, AbstractCollection $collection) : void
{
foreach ($this->getQueryFields($fieldNode) as $field) {
- if (!$collection->isAttributeAdded($field)) {
- $collection->addAttributeToSelect($field);
- }
+ $this->addFieldToCollection($collection, $field);
}
}
@@ -42,7 +58,7 @@ public function join(FieldNode $fieldNode, AbstractCollection $collection) : voi
* @param FieldNode $fieldNode
* @return string[]
*/
- public function getQueryFields(FieldNode $fieldNode)
+ public function getQueryFields(FieldNode $fieldNode): array
{
if (!isset($this->queryFields[$fieldNode->name->value])) {
$this->queryFields[$fieldNode->name->value] = [];
@@ -58,4 +74,29 @@ public function getQueryFields(FieldNode $fieldNode)
return $this->queryFields[$fieldNode->name->value];
}
+
+ /**
+ * Add field to collection select
+ *
+ * Add a query field to the collection, using mapped attribute names if they are set
+ *
+ * @param AbstractCollection $collection
+ * @param string $field
+ */
+ private function addFieldToCollection(AbstractCollection $collection, string $field)
+ {
+ $attribute = isset($this->fieldToAttributeMap[$field]) ? $this->fieldToAttributeMap[$field] : $field;
+
+ if (is_array($attribute)) {
+ foreach ($attribute as $attributeName) {
+ if (!$collection->isAttributeAdded($attributeName)) {
+ $collection->addAttributeToSelect($attributeName);
+ }
+ }
+ } else {
+ if (!$collection->isAttributeAdded($attribute)) {
+ $collection->addAttributeToSelect($attribute);
+ }
+ }
+ }
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php
new file mode 100644
index 0000000000000..2c03550404ae0
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php
@@ -0,0 +1,102 @@
+scopeConfig = $scopeConfig;
+ }
+
+ /**
+ * Filter for filtering the requested categories id's based on url_key, ids, name in the result.
+ *
+ * @param array $args
+ * @param Collection $categoryCollection
+ * @param StoreInterface $store
+ * @throws InputException
+ */
+ public function applyFilters(array $args, Collection $categoryCollection, StoreInterface $store)
+ {
+ $categoryCollection->addAttributeToFilter(CategoryInterface::KEY_IS_ACTIVE, ['eq' => 1]);
+ foreach ($args['filters'] as $field => $cond) {
+ foreach ($cond as $condType => $value) {
+ if ($field === 'ids') {
+ $categoryCollection->addIdFilter($value);
+ } else {
+ $this->addAttributeFilter($categoryCollection, $field, $condType, $value, $store);
+ }
+ }
+ }
+ }
+
+ /**
+ * Add filter to category collection
+ *
+ * @param Collection $categoryCollection
+ * @param string $field
+ * @param string $condType
+ * @param string|array $value
+ * @param StoreInterface $store
+ * @throws InputException
+ */
+ private function addAttributeFilter($categoryCollection, $field, $condType, $value, $store)
+ {
+ if ($condType === 'match') {
+ $this->addMatchFilter($categoryCollection, $field, $value, $store);
+ return;
+ }
+ $categoryCollection->addAttributeToFilter($field, [$condType => $value]);
+ }
+
+ /**
+ * Add match filter to collection
+ *
+ * @param Collection $categoryCollection
+ * @param string $field
+ * @param string $value
+ * @param StoreInterface $store
+ * @throws InputException
+ */
+ private function addMatchFilter($categoryCollection, $field, $value, $store)
+ {
+ $minQueryLength = $this->scopeConfig->getValue(
+ Query::XML_PATH_MIN_QUERY_LENGTH,
+ ScopeInterface::SCOPE_STORE,
+ $store
+ );
+ $searchValue = str_replace('%', '', $value);
+ $matchLength = strlen($searchValue);
+ if ($matchLength < $minQueryLength) {
+ throw new InputException(__('Invalid match filter'));
+ }
+
+ $categoryCollection->addAttributeToFilter($field, ['like' => "%{$searchValue}%"]);
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Config/CategoryAttributeReader.php b/app/code/Magento/CatalogGraphQl/Model/Config/CategoryAttributeReader.php
index 0ca72d9ff9519..cd11c3c68f794 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Config/CategoryAttributeReader.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Config/CategoryAttributeReader.php
@@ -51,16 +51,24 @@ class CategoryAttributeReader implements ReaderInterface
*/
private $collectionFactory;
+ /**
+ * @var array
+ */
+ private $categoryAttributeResolvers;
+
/**
* @param Type $typeLocator
* @param CollectionFactory $collectionFactory
+ * @param array $categoryAttributeResolvers
*/
public function __construct(
Type $typeLocator,
- CollectionFactory $collectionFactory
+ CollectionFactory $collectionFactory,
+ array $categoryAttributeResolvers = []
) {
$this->typeLocator = $typeLocator;
$this->collectionFactory = $collectionFactory;
+ $this->categoryAttributeResolvers = $categoryAttributeResolvers;
}
/**
@@ -93,6 +101,9 @@ public function read($scope = null) : array
$data['fields'][$attributeCode]['name'] = $attributeCode;
$data['fields'][$attributeCode]['type'] = $locatedType;
$data['fields'][$attributeCode]['arguments'] = [];
+ if (isset($this->categoryAttributeResolvers[$attributeCode])) {
+ $data['fields'][$attributeCode]['resolver'] = $this->categoryAttributeResolvers[$attributeCode];
+ }
}
$config['CategoryInterface'] = $data;
diff --git a/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php b/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php
new file mode 100644
index 0000000000000..4f3a88cc788df
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Config/FilterAttributeReader.php
@@ -0,0 +1,134 @@
+mapper = $mapper;
+ $this->collectionFactory = $collectionFactory;
+ $this->exactMatchAttributes = array_merge($this->exactMatchAttributes, $exactMatchAttributes);
+ }
+
+ /**
+ * Read configuration scope
+ *
+ * @param string|null $scope
+ * @return array
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function read($scope = null) : array
+ {
+ $typeNames = $this->mapper->getMappedTypes(self::ENTITY_TYPE);
+ $config = [];
+
+ foreach ($this->getAttributeCollection() as $attribute) {
+ $attributeCode = $attribute->getAttributeCode();
+
+ foreach ($typeNames as $typeName) {
+ $config[$typeName]['fields'][$attributeCode] = [
+ 'name' => $attributeCode,
+ 'type' => $this->getFilterType($attribute),
+ 'arguments' => [],
+ 'required' => false,
+ 'description' => sprintf('Attribute label: %s', $attribute->getDefaultFrontendLabel())
+ ];
+ }
+ }
+
+ return $config;
+ }
+
+ /**
+ * Map attribute type to filter type
+ *
+ * @param Attribute $attribute
+ * @return string
+ */
+ private function getFilterType(Attribute $attribute): string
+ {
+ if (in_array($attribute->getAttributeCode(), $this->exactMatchAttributes)) {
+ return self::FILTER_EQUAL_TYPE;
+ }
+
+ $filterTypeMap = [
+ 'price' => self::FILTER_RANGE_TYPE,
+ 'date' => self::FILTER_RANGE_TYPE,
+ 'select' => self::FILTER_EQUAL_TYPE,
+ 'multiselect' => self::FILTER_EQUAL_TYPE,
+ 'boolean' => self::FILTER_EQUAL_TYPE,
+ 'text' => self::FILTER_MATCH_TYPE,
+ 'textarea' => self::FILTER_MATCH_TYPE,
+ ];
+
+ return $filterTypeMap[$attribute->getFrontendInput()] ?? self::FILTER_MATCH_TYPE;
+ }
+
+ /**
+ * Create attribute collection
+ *
+ * @return Collection|\Magento\Catalog\Model\ResourceModel\Eav\Attribute[]
+ */
+ private function getAttributeCollection()
+ {
+ return $this->collectionFactory->create()
+ ->addHasOptionsFilter()
+ ->addIsSearchableFilter()
+ ->addDisplayInAdvancedSearchFilter();
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php b/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php
new file mode 100644
index 0000000000000..215b28be0579c
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php
@@ -0,0 +1,79 @@
+mapper = $mapper;
+ $this->attributesCollection = $attributesCollection;
+ }
+
+ /**
+ * Read configuration scope
+ *
+ * @param string|null $scope
+ * @return array
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function read($scope = null) : array
+ {
+ $map = $this->mapper->getMappedTypes(self::ENTITY_TYPE);
+ $config =[];
+ $attributes = $this->attributesCollection->addSearchableAttributeFilter()->addFilter('used_for_sort_by', 1);
+ /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */
+ foreach ($attributes as $attribute) {
+ $attributeCode = $attribute->getAttributeCode();
+ $attributeLabel = $attribute->getDefaultFrontendLabel();
+ foreach ($map as $type) {
+ $config[$type]['fields'][$attributeCode] = [
+ 'name' => $attributeCode,
+ 'type' => self::FIELD_TYPE,
+ 'arguments' => [],
+ 'required' => false,
+ 'description' => __('Attribute label: ') . $attributeLabel
+ ];
+ }
+ }
+
+ return $config;
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Product/Option/DateType.php b/app/code/Magento/CatalogGraphQl/Model/Product/Option/DateType.php
index e1106a3f696e4..cd582ffda9244 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Product/Option/DateType.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Product/Option/DateType.php
@@ -10,9 +10,12 @@
use Magento\Catalog\Model\Product\Option\Type\Date as ProductDateOptionType;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Stdlib\DateTime;
+use Magento\Framework\GraphQl\Exception\GraphQlInputException;
/**
- * @inheritdoc
+ * CatalogGraphQl product option date type
+ *
+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
*/
class DateType extends ProductDateOptionType
{
@@ -43,6 +46,13 @@ private function formatValues($values)
if (isset($values[$this->getOption()->getId()])) {
$value = $values[$this->getOption()->getId()];
$dateTime = \DateTime::createFromFormat(DateTime::DATETIME_PHP_FORMAT, $value);
+
+ if ($dateTime === false) {
+ throw new GraphQlInputException(
+ __('Invalid format provided. Please use \'Y-m-d H:i:s\' format.')
+ );
+ }
+
$values[$this->getOption()->getId()] = [
'date' => $value,
'year' => $dateTime->format('Y'),
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php
new file mode 100644
index 0000000000000..47a1d1f977f9b
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php
@@ -0,0 +1,68 @@
+filtersDataProvider = $filtersDataProvider;
+ $this->layerBuilder = $layerBuilder;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ if (!isset($value['layer_type']) || !isset($value['search_result'])) {
+ return null;
+ }
+
+ $aggregations = $value['search_result']->getSearchAggregation();
+
+ if ($aggregations) {
+ /** @var StoreInterface $store */
+ $store = $context->getExtensionAttributes()->getStore();
+ $storeId = (int)$store->getId();
+ return $this->layerBuilder->build($aggregations, $storeId);
+ } else {
+ return [];
+ }
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CanonicalUrl.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CanonicalUrl.php
new file mode 100644
index 0000000000000..3cce413ff93f3
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/CanonicalUrl.php
@@ -0,0 +1,59 @@
+categoryHelper = $categoryHelper;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ if (!isset($value['model'])) {
+ throw new LocalizedException(__('"model" value should be specified'));
+ }
+
+ /* @var Category $category */
+ $category = $value['model'];
+ /** @var StoreInterface $store */
+ $store = $context->getExtensionAttributes()->getStore();
+ if ($this->categoryHelper->canUseCanonicalTag($store)) {
+ $baseUrl = $category->getUrlInstance()->getBaseUrl();
+ return str_replace($baseUrl, '', $category->getUrl());
+ }
+ return null;
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php
index 9e23c4f1e9736..863e621bd8df3 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php
@@ -29,8 +29,11 @@ public function __construct(
}
/**
+ * Get breadcrumbs data
+ *
* @param string $categoryPath
* @return array
+ * @throws \Magento\Framework\Exception\LocalizedException
*/
public function getData(string $categoryPath): array
{
@@ -41,7 +44,7 @@ public function getData(string $categoryPath): array
if (count($parentCategoryIds)) {
$collection = $this->collectionFactory->create();
- $collection->addAttributeToSelect(['name', 'url_key']);
+ $collection->addAttributeToSelect(['name', 'url_key', 'url_path']);
$collection->addAttributeToFilter('entity_id', $parentCategoryIds);
foreach ($collection as $category) {
@@ -50,6 +53,7 @@ public function getData(string $categoryPath): array
'category_name' => $category->getName(),
'category_level' => $category->getLevel(),
'category_url_key' => $category->getUrlKey(),
+ 'category_url_path' => $category->getUrlPath(),
];
}
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Image.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Image.php
new file mode 100644
index 0000000000000..a06a8252d5a5e
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Image.php
@@ -0,0 +1,65 @@
+directoryList = $directoryList;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ if (!isset($value['model'])) {
+ throw new LocalizedException(__('"model" value should be specified'));
+ }
+ /** @var \Magento\Catalog\Model\Category $category */
+ $category = $value['model'];
+ $imagePath = $category->getImage();
+ if (empty($imagePath)) {
+ return null;
+ }
+ /** @var StoreInterface $store */
+ $store = $context->getExtensionAttributes()->getStore();
+ $baseUrl = $store->getBaseUrl('media');
+
+ $mediaPath = $this->directoryList->getUrlPath('media');
+ $pos = strpos($imagePath, $mediaPath);
+ if ($pos !== false) {
+ $imagePath = substr($imagePath, $pos + strlen($mediaPath), strlen($baseUrl));
+ }
+ $imageUrl = rtrim($baseUrl, '/') . '/' . ltrim($imagePath, '/');
+
+ return $imageUrl;
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php
index e0580213ddea7..abc5ae7e1da7f 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php
@@ -8,6 +8,9 @@
namespace Magento\CatalogGraphQl\Model\Resolver\Category;
use Magento\Catalog\Api\ProductRepositoryInterface;
+use Magento\CatalogGraphQl\DataProvider\Product\SearchCriteriaBuilder;
+use Magento\CatalogGraphQl\Model\Resolver\Products\Query\Search;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
@@ -27,27 +30,46 @@ class Products implements ResolverInterface
/**
* @var Builder
+ * @deprecated
*/
private $searchCriteriaBuilder;
/**
* @var Filter
+ * @deprecated
*/
private $filterQuery;
+ /**
+ * @var Search
+ */
+ private $searchQuery;
+
+ /**
+ * @var SearchCriteriaBuilder
+ */
+ private $searchApiCriteriaBuilder;
+
/**
* @param ProductRepositoryInterface $productRepository
* @param Builder $searchCriteriaBuilder
* @param Filter $filterQuery
+ * @param Search $searchQuery
+ * @param SearchCriteriaBuilder $searchApiCriteriaBuilder
*/
public function __construct(
ProductRepositoryInterface $productRepository,
Builder $searchCriteriaBuilder,
- Filter $filterQuery
+ Filter $filterQuery,
+ Search $searchQuery = null,
+ SearchCriteriaBuilder $searchApiCriteriaBuilder = null
) {
$this->productRepository = $productRepository;
$this->searchCriteriaBuilder = $searchCriteriaBuilder;
$this->filterQuery = $filterQuery;
+ $this->searchQuery = $searchQuery ?? ObjectManager::getInstance()->get(Search::class);
+ $this->searchApiCriteriaBuilder = $searchApiCriteriaBuilder ??
+ ObjectManager::getInstance()->get(SearchCriteriaBuilder::class);
}
/**
@@ -60,21 +82,20 @@ public function resolve(
array $value = null,
array $args = null
) {
- $args['filter'] = [
- 'category_id' => [
- 'eq' => $value['id']
- ]
- ];
- $searchCriteria = $this->searchCriteriaBuilder->build($field->getName(), $args);
if ($args['currentPage'] < 1) {
throw new GraphQlInputException(__('currentPage value must be greater than 0.'));
}
if ($args['pageSize'] < 1) {
throw new GraphQlInputException(__('pageSize value must be greater than 0.'));
}
- $searchCriteria->setCurrentPage($args['currentPage']);
- $searchCriteria->setPageSize($args['pageSize']);
- $searchResult = $this->filterQuery->getResult($searchCriteria, $info);
+
+ $args['filter'] = [
+ 'category_id' => [
+ 'eq' => $value['id']
+ ]
+ ];
+ $searchCriteria = $this->searchApiCriteriaBuilder->build($args, false);
+ $searchResult = $this->searchQuery->getResult($searchCriteria, $info);
//possible division by 0
if ($searchCriteria->getPageSize()) {
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php
new file mode 100644
index 0000000000000..6b8949d612829
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php
@@ -0,0 +1,114 @@
+categoryTree = $categoryTree;
+ $this->extractDataFromCategoryTree = $extractDataFromCategoryTree;
+ $this->categoryFilter = $categoryFilter;
+ $this->collectionFactory = $collectionFactory;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null)
+ {
+ if (isset($value[$field->getName()])) {
+ return $value[$field->getName()];
+ }
+ $store = $context->getExtensionAttributes()->getStore();
+
+ $rootCategoryIds = [];
+ if (!isset($args['filters'])) {
+ $rootCategoryIds[] = (int)$store->getRootCategoryId();
+ } else {
+ $categoryCollection = $this->collectionFactory->create();
+ try {
+ $this->categoryFilter->applyFilters($args, $categoryCollection, $store);
+ } catch (InputException $e) {
+ return [];
+ }
+
+ foreach ($categoryCollection as $category) {
+ $rootCategoryIds[] = (int)$category->getId();
+ }
+ }
+
+ $result = $this->fetchCategories($rootCategoryIds, $info);
+ return $result;
+ }
+
+ /**
+ * Fetch category tree data
+ *
+ * @param array $categoryIds
+ * @param ResolveInfo $info
+ * @return array
+ * @throws GraphQlNoSuchEntityException
+ */
+ private function fetchCategories(array $categoryIds, ResolveInfo $info)
+ {
+ $fetchedCategories = [];
+ foreach ($categoryIds as $categoryId) {
+ $categoryTree = $this->categoryTree->getTree($info, $categoryId);
+ if (empty($categoryTree)) {
+ continue;
+ }
+ $fetchedCategories[] = current($this->extractDataFromCategoryTree->execute($categoryTree));
+ }
+
+ return $fetchedCategories;
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php
index 89d3805383e1a..4284aed610848 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryTree.php
@@ -10,11 +10,11 @@
use Magento\Catalog\Model\Category;
use Magento\CatalogGraphQl\Model\Resolver\Category\CheckCategoryIsActive;
use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ExtractDataFromCategoryTree;
-use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Framework\GraphQl\Config\Element\Field;
-use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException;
use Magento\Framework\GraphQl\Query\ResolverInterface;
+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
+use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree as CategoryTreeDataProvider;
/**
* Category tree field resolver, used for GraphQL request processing.
@@ -27,7 +27,7 @@ class CategoryTree implements ResolverInterface
const CATEGORY_INTERFACE = 'CategoryInterface';
/**
- * @var \Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree
+ * @var CategoryTreeDataProvider
*/
private $categoryTree;
@@ -42,12 +42,12 @@ class CategoryTree implements ResolverInterface
private $checkCategoryIsActive;
/**
- * @param \Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree $categoryTree
+ * @param CategoryTreeDataProvider $categoryTree
* @param ExtractDataFromCategoryTree $extractDataFromCategoryTree
* @param CheckCategoryIsActive $checkCategoryIsActive
*/
public function __construct(
- \Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CategoryTree $categoryTree,
+ CategoryTreeDataProvider $categoryTree,
ExtractDataFromCategoryTree $extractDataFromCategoryTree,
CheckCategoryIsActive $checkCategoryIsActive
) {
@@ -56,22 +56,6 @@ public function __construct(
$this->checkCategoryIsActive = $checkCategoryIsActive;
}
- /**
- * Get category id
- *
- * @param array $args
- * @return int
- * @throws GraphQlInputException
- */
- private function getCategoryId(array $args) : int
- {
- if (!isset($args['id'])) {
- throw new GraphQlInputException(__('"id for category should be specified'));
- }
-
- return (int)$args['id'];
- }
-
/**
* @inheritdoc
*/
@@ -81,7 +65,9 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
return $value[$field->getName()];
}
- $rootCategoryId = $this->getCategoryId($args);
+ $rootCategoryId = isset($args['id']) ? (int)$args['id'] :
+ (int)$context->getExtensionAttributes()->getStore()->getRootCategoryId();
+
if ($rootCategoryId !== Category::TREE_ROOT_ID) {
$this->checkCategoryIsActive->execute($rootCategoryId);
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Layer/DataProvider/Filters.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Layer/DataProvider/Filters.php
index 786d4f1ab867c..f6d8edf1fe9b5 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Layer/DataProvider/Filters.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Layer/DataProvider/Filters.php
@@ -9,6 +9,7 @@
use Magento\Catalog\Model\Layer\Filter\AbstractFilter;
use Magento\CatalogGraphQl\Model\Resolver\Layer\FiltersProvider;
+use Magento\Catalog\Model\Layer\Filter\Item;
/**
* Layered navigation filters data provider.
@@ -20,6 +21,11 @@ class Filters
*/
private $filtersProvider;
+ /**
+ * @var array
+ */
+ private $mappings;
+
/**
* Filters constructor.
* @param FiltersProvider $filtersProvider
@@ -28,26 +34,31 @@ public function __construct(
FiltersProvider $filtersProvider
) {
$this->filtersProvider = $filtersProvider;
+ $this->mappings = [
+ 'Category' => 'category'
+ ];
}
/**
* Get layered navigation filters data
*
* @param string $layerType
+ * @param array|null $attributesToFilter
* @return array
+ * @throws \Magento\Framework\Exception\LocalizedException
*/
- public function getData(string $layerType) : array
+ public function getData(string $layerType, array $attributesToFilter = null) : array
{
$filtersData = [];
/** @var AbstractFilter $filter */
foreach ($this->filtersProvider->getFilters($layerType) as $filter) {
- if ($filter->getItemsCount()) {
+ if ($this->isNeedToAddFilter($filter, $attributesToFilter)) {
$filterGroup = [
'name' => (string)$filter->getName(),
'filter_items_count' => $filter->getItemsCount(),
'request_var' => $filter->getRequestVar(),
];
- /** @var \Magento\Catalog\Model\Layer\Filter\Item $filterItem */
+ /** @var Item $filterItem */
foreach ($filter->getItems() as $filterItem) {
$filterGroup['filter_items'][] = [
'label' => (string)$filterItem->getLabel(),
@@ -60,4 +71,32 @@ public function getData(string $layerType) : array
}
return $filtersData;
}
+
+ /**
+ * Check for adding filter to the list
+ *
+ * @param AbstractFilter $filter
+ * @param array $attributesToFilter
+ * @return bool
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ private function isNeedToAddFilter(AbstractFilter $filter, array $attributesToFilter): bool
+ {
+ if ($attributesToFilter === null) {
+ $result = (bool)$filter->getItemsCount();
+ } else {
+ if ($filter->hasAttributeModel()) {
+ $filterAttribute = $filter->getAttributeModel();
+ $result = in_array($filterAttribute->getAttributeCode(), $attributesToFilter);
+ } else {
+ $name = (string)$filter->getName();
+ if (array_key_exists($name, $this->mappings)) {
+ $result = in_array($this->mappings[$name], $attributesToFilter);
+ } else {
+ $result = true;
+ }
+ }
+ }
+ return $result;
+ }
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php
index 0ec7e12e42d55..78ac45a1ad001 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/LayerFilters.php
@@ -44,6 +44,29 @@ public function resolve(
return null;
}
- return $this->filtersDataProvider->getData($value['layer_type']);
+ $attributes = $this->prepareAttributesResults($value);
+ return $this->filtersDataProvider->getData($value['layer_type'], $attributes);
+ }
+
+ /**
+ * Get attributes available to filtering from the search result
+ *
+ * @param array $value
+ * @return array|null
+ */
+ private function prepareAttributesResults(array $value): ?array
+ {
+ $attributes = [];
+ if (!empty($value['search_result'])) {
+ $buckets = $value['search_result']->getSearchAggregation()->getBuckets();
+ foreach ($buckets as $bucket) {
+ if (!empty($bucket->getValues())) {
+ $attributes[] = str_replace('_bucket', '', $bucket->getName());
+ }
+ }
+ } else {
+ $attributes = null;
+ }
+ return $attributes;
}
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php
index 86137990cc57d..889735a5f4d88 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php
@@ -14,6 +14,7 @@
use Magento\Framework\GraphQl\Query\Resolver\ValueFactory;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
+use Magento\Framework\Exception\LocalizedException;
/**
* @inheritdoc
@@ -63,10 +64,13 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
$this->productDataProvider->addEavAttributes($fields);
$result = function () use ($value) {
- $data = $this->productDataProvider->getProductBySku($value['sku']);
+ $data = $value['product'] ?? $this->productDataProvider->getProductBySku($value['sku']);
if (empty($data)) {
return null;
}
+ if (!isset($data['model'])) {
+ throw new LocalizedException(__('"model" value should be specified'));
+ }
$productModel = $data['model'];
/** @var \Magento\Catalog\Model\Product $productModel */
$data = $productModel->getData();
@@ -79,10 +83,8 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
}
}
}
-
return array_replace($value, $data);
};
-
return $this->valueFactory->create($result);
}
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php
new file mode 100644
index 0000000000000..14732ecf37c63
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php
@@ -0,0 +1,81 @@
+getValue();
+ if (empty($value['model'])) {
+ throw new LocalizedException(__('"model" value should be specified'));
+ }
+ /** @var \Magento\Catalog\Model\Product $product */
+ $product = $value['model'];
+
+ return new ListCriteria((string)$product->getId(), self::$linkTypes, $product);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function convertFromServiceResult($result, ResolveRequestInterface $request)
+ {
+ /** @var \Magento\Catalog\Model\ProductLink\Data\ListResultInterface $result */
+ if ($result->getError()) {
+ //If model isn't there previous method would've thrown an exception.
+ /** @var \Magento\Catalog\Model\Product $product */
+ $product = $request->getValue()['model'];
+ throw new LocalizedException(
+ __('Failed to retrieve product links for "%1"', $product->getSku()),
+ $result->getError()
+ );
+ }
+
+ return array_filter(
+ array_map(
+ function (ProductLinkInterface $link) {
+ return [
+ 'sku' => $link->getSku(),
+ 'link_type' => $link->getLinkType(),
+ 'linked_product_sku' => $link->getLinkedProductSku(),
+ 'linked_product_type' => $link->getLinkedProductType(),
+ 'position' => $link->getPosition()
+ ];
+ },
+ $result->getResult()
+ )
+ );
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CanonicalUrl.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CanonicalUrl.php
index 9047eaee4b568..0f764d1daa5e7 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CanonicalUrl.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CanonicalUrl.php
@@ -12,12 +12,25 @@
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
+use Magento\Catalog\Helper\Product as ProductHelper;
+use Magento\Store\Api\Data\StoreInterface;
/**
* Resolve data for product canonical URL
*/
class CanonicalUrl implements ResolverInterface
{
+ /** @var ProductHelper */
+ private $productHelper;
+
+ /**
+ * @param Product $productHelper
+ */
+ public function __construct(ProductHelper $productHelper)
+ {
+ $this->productHelper = $productHelper;
+ }
+
/**
* @inheritdoc
*/
@@ -32,10 +45,14 @@ public function resolve(
throw new LocalizedException(__('"model" value should be specified'));
}
- /* @var $product Product */
+ /* @var Product $product */
$product = $value['model'];
- $url = $product->getUrlModel()->getUrl($product, ['_ignore_category' => true]);
-
- return $url;
+ /** @var StoreInterface $store */
+ $store = $context->getExtensionAttributes()->getStore();
+ if ($this->productHelper->canUseCanonicalTag($store)) {
+ $product->getUrlModel()->getUrl($product, ['_ignore_category' => true]);
+ return $product->getRequestPath();
+ }
+ return null;
}
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Label.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Label.php
deleted file mode 100644
index 4ec76fe59ca88..0000000000000
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Label.php
+++ /dev/null
@@ -1,87 +0,0 @@
-productResource = $productResource;
- }
-
- /**
- * @inheritdoc
- */
- public function resolve(
- Field $field,
- $context,
- ResolveInfo $info,
- array $value = null,
- array $args = null
- ) {
-
- if (isset($value['label'])) {
- return $value['label'];
- }
-
- if (!isset($value['model'])) {
- throw new LocalizedException(__('"model" value should be specified'));
- }
-
- /** @var Product $product */
- $product = $value['model'];
- $productId = (int)$product->getEntityId();
- /** @var StoreInterface $store */
- $store = $context->getExtensionAttributes()->getStore();
- $storeId = (int)$store->getId();
- if (!isset($value['image_type'])) {
- return $this->getAttributeValue($productId, 'name', $storeId);
- }
- $imageType = $value['image_type'];
- $imageLabel = $this->getAttributeValue($productId, $imageType . '_label', $storeId);
- if ($imageLabel == null) {
- $imageLabel = $this->getAttributeValue($productId, 'name', $storeId);
- }
-
- return $imageLabel;
- }
-
- /**
- * Get attribute value
- *
- * @param int $productId
- * @param string $attributeCode
- * @param int $storeId
- * @return null|string Null if attribute value is not exists
- */
- private function getAttributeValue(int $productId, string $attributeCode, int $storeId): ?string
- {
- $value = $this->productResource->getAttributeRawValue($productId, $attributeCode, $storeId);
- return is_array($value) && empty($value) ? null : $value;
- }
-}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php
index eaab159cddae6..359d295095667 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php
@@ -24,11 +24,17 @@ class Url implements ResolverInterface
* @var ImageFactory
*/
private $productImageFactory;
+
/**
* @var PlaceholderProvider
*/
private $placeholderProvider;
+ /**
+ * @var string[]
+ */
+ private $placeholderCache = [];
+
/**
* @param ImageFactory $productImageFactory
* @param PlaceholderProvider $placeholderProvider
@@ -64,12 +70,8 @@ public function resolve(
if (isset($value['image_type'])) {
$imagePath = $product->getData($value['image_type']);
return $this->getImageUrl($value['image_type'], $imagePath);
- }
- if (isset($value['file'])) {
- $image = $this->productImageFactory->create();
- $image->setDestinationSubdir('image')->setBaseFile($value['file']);
- $imageUrl = $image->getUrl();
- return $imageUrl;
+ } elseif (isset($value['file'])) {
+ return $this->getImageUrl('image', $value['file']);
}
return [];
}
@@ -84,12 +86,16 @@ public function resolve(
*/
private function getImageUrl(string $imageType, ?string $imagePath): string
{
+ if (empty($imagePath) && !empty($this->placeholderCache[$imageType])) {
+ return $this->placeholderCache[$imageType];
+ }
$image = $this->productImageFactory->create();
$image->setDestinationSubdir($imageType)
->setBaseFile($imagePath);
if ($image->isBaseFilePlaceholder()) {
- return $this->placeholderProvider->getPlaceholder($imageType);
+ $this->placeholderCache[$imageType] = $this->placeholderProvider->getPlaceholder($imageType);
+ return $this->placeholderCache[$imageType];
}
return $image->getUrl();
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/Discount.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/Discount.php
new file mode 100644
index 0000000000000..c56e05bf267a4
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/Discount.php
@@ -0,0 +1,98 @@
+ $this->getPriceDifferenceAsValue($regularPrice, $finalPrice),
+ 'percent_off' => $this->getPriceDifferenceAsPercent($regularPrice, $finalPrice)
+ ];
+ }
+
+ /**
+ * Get formatted discount based on percent off
+ *
+ * @param float $regularPrice
+ * @param float $percentOff
+ * @return array
+ */
+ public function getDiscountByPercent(float $regularPrice, float $percentOff): array
+ {
+ return [
+ 'amount_off' => $this->getPercentDiscountAsValue($regularPrice, $percentOff),
+ 'percent_off' => $percentOff
+ ];
+ }
+
+ /**
+ * Get value difference between two prices
+ *
+ * @param float $regularPrice
+ * @param float $finalPrice
+ * @return float
+ */
+ private function getPriceDifferenceAsValue(float $regularPrice, float $finalPrice): float
+ {
+ $difference = $regularPrice - $finalPrice;
+ if ($difference <= $this->zeroThreshold) {
+ return 0;
+ }
+ return round($difference, 2);
+ }
+
+ /**
+ * Get percent difference between two prices
+ *
+ * @param float $regularPrice
+ * @param float $finalPrice
+ * @return float
+ */
+ private function getPriceDifferenceAsPercent(float $regularPrice, float $finalPrice): float
+ {
+ $difference = $this->getPriceDifferenceAsValue($regularPrice, $finalPrice);
+
+ if ($difference <= $this->zeroThreshold || $regularPrice <= $this->zeroThreshold) {
+ return 0;
+ }
+
+ return round(($difference / $regularPrice) * 100, 2);
+ }
+
+ /**
+ * Get amount difference that percentOff represents
+ *
+ * @param float $regularPrice
+ * @param float $percentOff
+ * @return float
+ */
+ private function getPercentDiscountAsValue(float $regularPrice, float $percentOff): float
+ {
+ $percentDecimal = $percentOff / 100;
+ $valueDiscount = $regularPrice * $percentDecimal;
+
+ return round($valueDiscount, 2);
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/Provider.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/Provider.php
new file mode 100644
index 0000000000000..67dbcf861170f
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/Provider.php
@@ -0,0 +1,63 @@
+getPriceInfo()->getPrice(FinalPrice::PRICE_CODE);
+ return $finalPrice->getMinimalPrice();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getMinimalRegularPrice(SaleableInterface $product): AmountInterface
+ {
+ return $this->getRegularPrice($product);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getMaximalFinalPrice(SaleableInterface $product): AmountInterface
+ {
+ /** @var FinalPrice $finalPrice */
+ $finalPrice = $product->getPriceInfo()->getPrice(FinalPrice::PRICE_CODE);
+ return $finalPrice->getMaximalPrice();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getMaximalRegularPrice(SaleableInterface $product): AmountInterface
+ {
+ return $this->getRegularPrice($product);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getRegularPrice(SaleableInterface $product): AmountInterface
+ {
+ return $product->getPriceInfo()->getPrice(RegularPrice::PRICE_CODE)->getAmount();
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/ProviderInterface.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/ProviderInterface.php
new file mode 100644
index 0000000000000..99459daf045a5
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/ProviderInterface.php
@@ -0,0 +1,57 @@
+providers = $providers;
+ }
+
+ /**
+ * Get price provider by product type
+ *
+ * @param string $productType
+ * @return ProviderInterface
+ */
+ public function getProviderByProductType(string $productType): ProviderInterface
+ {
+ if (isset($this->providers[$productType])) {
+ return $this->providers[$productType];
+ }
+ return $this->providers[self::DEFAULT];
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php
new file mode 100644
index 0000000000000..9396b1f02b975
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php
@@ -0,0 +1,133 @@
+priceProviderPool = $priceProviderPool;
+ $this->discount = $discount;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ if (!isset($value['model'])) {
+ throw new LocalizedException(__('"model" value should be specified'));
+ }
+ /** @var StoreInterface $store */
+ $store = $context->getExtensionAttributes()->getStore();
+
+ /** @var Product $product */
+ $product = $value['model'];
+ $product->unsetData('minimal_price');
+
+ $requestedFields = $info->getFieldSelection(10);
+ $returnArray = [];
+
+ if (isset($requestedFields['minimum_price'])) {
+ $returnArray['minimum_price'] = $this->getMinimumProductPrice($product, $store);
+ }
+ if (isset($requestedFields['maximum_price'])) {
+ $returnArray['maximum_price'] = $this->getMaximumProductPrice($product, $store);
+ }
+ return $returnArray;
+ }
+
+ /**
+ * Get formatted minimum product price
+ *
+ * @param SaleableInterface $product
+ * @param StoreInterface $store
+ * @return array
+ */
+ private function getMinimumProductPrice(SaleableInterface $product, StoreInterface $store): array
+ {
+ $priceProvider = $this->priceProviderPool->getProviderByProductType($product->getTypeId());
+ $regularPrice = $priceProvider->getMinimalRegularPrice($product)->getValue();
+ $finalPrice = $priceProvider->getMinimalFinalPrice($product)->getValue();
+ $minPriceArray = $this->formatPrice($regularPrice, $finalPrice, $store);
+ $minPriceArray['model'] = $product;
+ return $minPriceArray;
+ }
+
+ /**
+ * Get formatted maximum product price
+ *
+ * @param SaleableInterface $product
+ * @param StoreInterface $store
+ * @return array
+ */
+ private function getMaximumProductPrice(SaleableInterface $product, StoreInterface $store): array
+ {
+ $priceProvider = $this->priceProviderPool->getProviderByProductType($product->getTypeId());
+ $regularPrice = $priceProvider->getMaximalRegularPrice($product)->getValue();
+ $finalPrice = $priceProvider->getMaximalFinalPrice($product)->getValue();
+ $maxPriceArray = $this->formatPrice($regularPrice, $finalPrice, $store);
+ $maxPriceArray['model'] = $product;
+ return $maxPriceArray;
+ }
+
+ /**
+ * Format price for GraphQl output
+ *
+ * @param float $regularPrice
+ * @param float $finalPrice
+ * @param StoreInterface $store
+ * @return array
+ */
+ private function formatPrice(float $regularPrice, float $finalPrice, StoreInterface $store): array
+ {
+ return [
+ 'regular_price' => [
+ 'value' => $regularPrice,
+ 'currency' => $store->getCurrentCurrencyCode()
+ ],
+ 'final_price' => [
+ 'value' => $finalPrice,
+ 'currency' => $store->getCurrentCurrencyCode()
+ ],
+ 'discount' => $this->discount->getDiscountByDifference($regularPrice, $finalPrice),
+ ];
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage.php
index d1566162472b0..7c08f91c922bd 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductImage.php
@@ -18,6 +18,25 @@
*/
class ProductImage implements ResolverInterface
{
+ /** @var array */
+ private static $catalogImageLabelTypes = [
+ 'image' => 'image_label',
+ 'small_image' => 'small_image_label',
+ 'thumbnail' => 'thumbnail_label'
+ ];
+
+ /** @var array */
+ private $imageTypeLabels;
+
+ /**
+ * @param array $imageTypeLabels
+ */
+ public function __construct(
+ array $imageTypeLabels = []
+ ) {
+ $this->imageTypeLabels = array_replace(self::$catalogImageLabelTypes, $imageTypeLabels);
+ }
+
/**
* @inheritdoc
*/
@@ -34,11 +53,16 @@ public function resolve(
/** @var Product $product */
$product = $value['model'];
- $imageType = $field->getName();
+ $label = $value['name'] ?? null;
+ if (isset($this->imageTypeLabels[$info->fieldName])
+ && !empty($value[$this->imageTypeLabels[$info->fieldName]])) {
+ $label = $value[$this->imageTypeLabels[$info->fieldName]];
+ }
return [
'model' => $product,
- 'image_type' => $imageType,
+ 'image_type' => $field->getName(),
+ 'label' => $label
];
}
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/TierPrices.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/TierPrices.php
deleted file mode 100644
index 726ef91c56880..0000000000000
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/TierPrices.php
+++ /dev/null
@@ -1,63 +0,0 @@
-getTierPrices()) {
- $tierPrices = [];
- /** @var TierPrice $tierPrice */
- foreach ($product->getTierPrices() as $tierPrice) {
- $tierPrices[] = $tierPrice->getData();
- }
- }
-
- return $tierPrices;
- }
-}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php
index a75a9d2cf50a0..691f93e4148bc 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php
@@ -16,6 +16,7 @@
use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\SearchFilter;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Catalog\Model\Layer\Resolver;
+use Magento\CatalogGraphQl\DataProvider\Product\SearchCriteriaBuilder;
/**
* Products field resolver, used for GraphQL request processing.
@@ -24,6 +25,7 @@ class Products implements ResolverInterface
{
/**
* @var Builder
+ * @deprecated
*/
private $searchCriteriaBuilder;
@@ -34,30 +36,41 @@ class Products implements ResolverInterface
/**
* @var Filter
+ * @deprecated
*/
private $filterQuery;
/**
* @var SearchFilter
+ * @deprecated
*/
private $searchFilter;
+ /**
+ * @var SearchCriteriaBuilder
+ */
+ private $searchApiCriteriaBuilder;
+
/**
* @param Builder $searchCriteriaBuilder
* @param Search $searchQuery
* @param Filter $filterQuery
* @param SearchFilter $searchFilter
+ * @param SearchCriteriaBuilder|null $searchApiCriteriaBuilder
*/
public function __construct(
Builder $searchCriteriaBuilder,
Search $searchQuery,
Filter $filterQuery,
- SearchFilter $searchFilter
+ SearchFilter $searchFilter,
+ SearchCriteriaBuilder $searchApiCriteriaBuilder = null
) {
$this->searchCriteriaBuilder = $searchCriteriaBuilder;
$this->searchQuery = $searchQuery;
$this->filterQuery = $filterQuery;
$this->searchFilter = $searchFilter;
+ $this->searchApiCriteriaBuilder = $searchApiCriteriaBuilder ??
+ \Magento\Framework\App\ObjectManager::getInstance()->get(SearchCriteriaBuilder::class);
}
/**
@@ -70,40 +83,29 @@ public function resolve(
array $value = null,
array $args = null
) {
- $searchCriteria = $this->searchCriteriaBuilder->build($field->getName(), $args);
if ($args['currentPage'] < 1) {
throw new GraphQlInputException(__('currentPage value must be greater than 0.'));
}
if ($args['pageSize'] < 1) {
throw new GraphQlInputException(__('pageSize value must be greater than 0.'));
}
- $searchCriteria->setCurrentPage($args['currentPage']);
- $searchCriteria->setPageSize($args['pageSize']);
if (!isset($args['search']) && !isset($args['filter'])) {
throw new GraphQlInputException(
__("'search' or 'filter' input argument is required.")
);
- } elseif (isset($args['search'])) {
- $layerType = Resolver::CATALOG_LAYER_SEARCH;
- $this->searchFilter->add($args['search'], $searchCriteria);
- $searchResult = $this->searchQuery->getResult($searchCriteria, $info);
- } else {
- $layerType = Resolver::CATALOG_LAYER_CATEGORY;
- $searchResult = $this->filterQuery->getResult($searchCriteria, $info);
- }
- //possible division by 0
- if ($searchCriteria->getPageSize()) {
- $maxPages = ceil($searchResult->getTotalCount() / $searchCriteria->getPageSize());
- } else {
- $maxPages = 0;
}
- $currentPage = $searchCriteria->getCurrentPage();
- if ($searchCriteria->getCurrentPage() > $maxPages && $searchResult->getTotalCount() > 0) {
+ //get product children fields queried
+ $productFields = (array)$info->getFieldSelection(1);
+ $includeAggregations = isset($productFields['filters']) || isset($productFields['aggregations']);
+ $searchCriteria = $this->searchApiCriteriaBuilder->build($args, $includeAggregations);
+ $searchResult = $this->searchQuery->getResult($searchCriteria, $info, $args);
+
+ if ($searchResult->getCurrentPage() > $searchResult->getTotalPages() && $searchResult->getTotalCount() > 0) {
throw new GraphQlInputException(
__(
'currentPage value %1 specified is greater than the %2 page(s) available.',
- [$currentPage, $maxPages]
+ [$searchResult->getCurrentPage(), $searchResult->getTotalPages()]
)
);
}
@@ -112,11 +114,12 @@ public function resolve(
'total_count' => $searchResult->getTotalCount(),
'items' => $searchResult->getProductsSearchResult(),
'page_info' => [
- 'page_size' => $searchCriteria->getPageSize(),
- 'current_page' => $currentPage,
- 'total_pages' => $maxPages
+ 'page_size' => $searchResult->getPageSize(),
+ 'current_page' => $searchResult->getCurrentPage(),
+ 'total_pages' => $searchResult->getTotalPages()
],
- 'layer_type' => $layerType
+ 'search_result' => $searchResult,
+ 'layer_type' => isset($args['search']) ? Resolver::CATALOG_LAYER_SEARCH : Resolver::CATALOG_LAYER_CATEGORY,
];
return $data;
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php
index 3525ccbb6a2d1..b38a2c9bb04d9 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ExtractDataFromCategoryTree.php
@@ -48,24 +48,57 @@ public function __construct(
public function execute(\Iterator $iterator): array
{
$tree = [];
+ /** @var CategoryInterface $rootCategory */
+ $rootCategory = $iterator->current();
while ($iterator->valid()) {
- /** @var CategoryInterface $category */
- $category = $iterator->current();
+ /** @var CategoryInterface $currentCategory */
+ $currentCategory = $iterator->current();
$iterator->next();
- $pathElements = explode("/", $category->getPath());
- if (empty($tree)) {
- $this->startCategoryFetchLevel = count($pathElements) - 1;
+ if ($this->areParentsActive($currentCategory, $rootCategory, (array)$iterator)) {
+ $pathElements = explode("/", $currentCategory->getPath());
+ if (empty($tree)) {
+ $this->startCategoryFetchLevel = count($pathElements) - 1;
+ }
+ $this->iteratingCategory = $currentCategory;
+ $currentLevelTree = $this->explodePathToArray($pathElements, $this->startCategoryFetchLevel);
+ if (empty($tree)) {
+ $tree = $currentLevelTree;
+ }
+ $tree = $this->mergeCategoriesTrees($currentLevelTree, $tree);
}
- $this->iteratingCategory = $category;
- $currentLevelTree = $this->explodePathToArray($pathElements, $this->startCategoryFetchLevel);
- if (empty($tree)) {
- $tree = $currentLevelTree;
- }
- $tree = $this->mergeCategoriesTrees($currentLevelTree, $tree);
}
return $tree;
}
+ /**
+ * Test that all parents of the current category are active.
+ *
+ * Assumes that $categoriesArray are key-pair values and key is the ID of the category and
+ * all categories in this list are queried as active.
+ *
+ * @param CategoryInterface $currentCategory
+ * @param CategoryInterface $rootCategory
+ * @param array $categoriesArray
+ * @return bool
+ */
+ private function areParentsActive(
+ CategoryInterface $currentCategory,
+ CategoryInterface $rootCategory,
+ array $categoriesArray
+ ): bool {
+ if ($currentCategory === $rootCategory) {
+ return true;
+ } elseif (array_key_exists($currentCategory->getParentId(), $categoriesArray)) {
+ return $this->areParentsActive(
+ $categoriesArray[$currentCategory->getParentId()],
+ $rootCategory,
+ $categoriesArray
+ );
+ } else {
+ return false;
+ }
+ }
+
/**
* Merge together complex categories trees
*
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php
index e5e0d1aea4285..2076ec6726988 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php
@@ -8,6 +8,7 @@
namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider;
use Magento\Catalog\Model\Product\Visibility;
+use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionPostProcessor;
use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory;
@@ -32,7 +33,12 @@ class Product
/**
* @var CollectionProcessorInterface
*/
- private $collectionProcessor;
+ private $collectionPreProcessor;
+
+ /**
+ * @var CollectionPostProcessor
+ */
+ private $collectionPostProcessor;
/**
* @var Visibility
@@ -44,17 +50,20 @@ class Product
* @param ProductSearchResultsInterfaceFactory $searchResultsFactory
* @param Visibility $visibility
* @param CollectionProcessorInterface $collectionProcessor
+ * @param CollectionPostProcessor $collectionPostProcessor
*/
public function __construct(
CollectionFactory $collectionFactory,
ProductSearchResultsInterfaceFactory $searchResultsFactory,
Visibility $visibility,
- CollectionProcessorInterface $collectionProcessor
+ CollectionProcessorInterface $collectionProcessor,
+ CollectionPostProcessor $collectionPostProcessor
) {
$this->collectionFactory = $collectionFactory;
$this->searchResultsFactory = $searchResultsFactory;
$this->visibility = $visibility;
- $this->collectionProcessor = $collectionProcessor;
+ $this->collectionPreProcessor = $collectionProcessor;
+ $this->collectionPostProcessor = $collectionPostProcessor;
}
/**
@@ -75,7 +84,7 @@ public function getList(
/** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $collection */
$collection = $this->collectionFactory->create();
- $this->collectionProcessor->process($collection, $searchCriteria, $attributes);
+ $this->collectionPreProcessor->process($collection, $searchCriteria, $attributes);
if (!$isChildSearch) {
$visibilityIds = $isSearch
@@ -83,18 +92,9 @@ public function getList(
: $this->visibility->getVisibleInCatalogIds();
$collection->setVisibility($visibilityIds);
}
- $collection->load();
- // Methods that perform extra fetches post-load
- if (in_array('media_gallery_entries', $attributes)) {
- $collection->addMediaGalleryData();
- }
- if (in_array('media_gallery', $attributes)) {
- $collection->addMediaGalleryData();
- }
- if (in_array('options', $attributes)) {
- $collection->addOptionsToResult();
- }
+ $collection->load();
+ $this->collectionPostProcessor->process($collection, $attributes);
$searchResult = $this->searchResultsFactory->create();
$searchResult->setSearchCriteria($searchCriteria);
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionPostProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionPostProcessor.php
new file mode 100644
index 0000000000000..fadf22e7643af
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionPostProcessor.php
@@ -0,0 +1,42 @@
+isLoaded()) {
+ $collection->load();
+ }
+ // Methods that perform extra fetches post-load
+ if (in_array('media_gallery_entries', $attributeNames)) {
+ $collection->addMediaGalleryData();
+ }
+ if (in_array('media_gallery', $attributeNames)) {
+ $collection->addMediaGalleryData();
+ }
+ if (in_array('options', $attributeNames)) {
+ $collection->addOptionsToResult();
+ }
+
+ return $collection;
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/AttributeProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/AttributeProcessor.php
index f4cefeb3f3638..fef224b12acfc 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/AttributeProcessor.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/AttributeProcessor.php
@@ -19,7 +19,22 @@
class AttributeProcessor implements CollectionProcessorInterface
{
/**
- * {@inheritdoc}
+ * Map GraphQl input fields to product attributes
+ *
+ * @var array
+ */
+ private $fieldToAttributeMap = [];
+
+ /**
+ * @param array $fieldToAttributeMap
+ */
+ public function __construct($fieldToAttributeMap = [])
+ {
+ $this->fieldToAttributeMap = array_merge($this->fieldToAttributeMap, $fieldToAttributeMap);
+ }
+
+ /**
+ * @inheritdoc
*/
public function process(
Collection $collection,
@@ -27,9 +42,86 @@ public function process(
array $attributeNames
): Collection {
foreach ($attributeNames as $name) {
- $collection->addAttributeToSelect($name);
+ $this->addAttribute($collection, $name);
}
return $collection;
}
+
+ /**
+ * Add attribute to collection select
+ *
+ * Add attributes to the collection where graphql fields names don't match attributes names, or if attributes exist
+ * on a nested level and they need to be loaded.
+ *
+ * Format of the attribute can be string or array while array can have different formats.
+ * Example: [
+ * 'price_range' =>
+ * [
+ * 'price' => 'price',
+ * 'price_type' => 'price_type',
+ * ],
+ * 'thumbnail' => //complex array where more than one attribute is needed to compute a value
+ * [
+ * 'label' =>
+ * [
+ * 'attribute' => 'thumbnail_label', // the actual attribute
+ * 'fallback_attribute' => 'name', //used as default value in case attribute value is null
+ * ],
+ * 'url' => 'thumbnail',
+ * ]
+ * ]
+ *
+ * @param Collection $collection
+ * @param string $attribute
+ */
+ private function addAttribute(Collection $collection, string $attribute): void
+ {
+ if (isset($this->fieldToAttributeMap[$attribute])) {
+ $attributeMap = $this->fieldToAttributeMap[$attribute];
+ if (is_array($attributeMap)) {
+ $this->addAttributeAsArray($collection, $attributeMap);
+ } else {
+ $collection->addAttributeToSelect($attributeMap);
+ }
+
+ } else {
+ $collection->addAttributeToSelect($attribute);
+ }
+ }
+
+ /**
+ * Add an array defined attribute to the collection
+ *
+ * @param Collection $collection
+ * @param array $attributeMap
+ * @return void
+ */
+ private function addAttributeAsArray(Collection $collection, array $attributeMap): void
+ {
+ foreach ($attributeMap as $attribute) {
+ if (is_array($attribute)) {
+ $this->addAttributeComplexArrayToCollection($collection, $attribute);
+ } else {
+ $collection->addAttributeToSelect($attribute);
+ }
+ }
+ }
+
+ /**
+ * Add a complex array defined attribute to the collection
+ *
+ * @param Collection $collection
+ * @param array $attribute
+ * @return void
+ */
+ private function addAttributeComplexArrayToCollection(Collection $collection, array $attribute): void
+ {
+ if (isset($attribute['attribute'])) {
+ $collection->addAttributeToSelect($attribute['attribute']);
+ }
+ if (isset($attribute['fallback_attribute'])) {
+ $collection->addAttributeToSelect($attribute['fallback_attribute']);
+ }
+ }
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php
new file mode 100644
index 0000000000000..ff845f4796763
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php
@@ -0,0 +1,144 @@
+collectionFactory = $collectionFactory;
+ $this->searchResultsFactory = $searchResultsFactory;
+ $this->collectionPreProcessor = $collectionPreProcessor;
+ $this->collectionPostProcessor = $collectionPostProcessor;
+ $this->searchResultApplierFactory = $searchResultsApplierFactory;
+ }
+
+ /**
+ * Get list of product data with full data set. Adds eav attributes to result set from passed in array
+ *
+ * @param SearchCriteriaInterface $searchCriteria
+ * @param SearchResultInterface $searchResult
+ * @param array $attributes
+ * @return SearchResultsInterface
+ */
+ public function getList(
+ SearchCriteriaInterface $searchCriteria,
+ SearchResultInterface $searchResult,
+ array $attributes = []
+ ): SearchResultsInterface {
+ /** @var Collection $collection */
+ $collection = $this->collectionFactory->create();
+
+ //Join search results
+ $this->getSearchResultsApplier($searchResult, $collection, $this->getSortOrderArray($searchCriteria))->apply();
+
+ $this->collectionPreProcessor->process($collection, $searchCriteria, $attributes);
+ $collection->load();
+ $this->collectionPostProcessor->process($collection, $attributes);
+
+ $searchResults = $this->searchResultsFactory->create();
+ $searchResults->setSearchCriteria($searchCriteria);
+ $searchResults->setItems($collection->getItems());
+ $searchResults->setTotalCount($searchResult->getTotalCount());
+ return $searchResults;
+ }
+
+ /**
+ * Create searchResultApplier
+ *
+ * @param SearchResultInterface $searchResult
+ * @param Collection $collection
+ * @param array $orders
+ * @return SearchResultApplierInterface
+ */
+ private function getSearchResultsApplier(
+ SearchResultInterface $searchResult,
+ Collection $collection,
+ array $orders
+ ): SearchResultApplierInterface {
+ return $this->searchResultApplierFactory->create(
+ [
+ 'collection' => $collection,
+ 'searchResult' => $searchResult,
+ 'orders' => $orders
+ ]
+ );
+ }
+
+ /**
+ * Format sort orders into associative array
+ *
+ * E.g. ['field1' => 'DESC', 'field2' => 'ASC", ...]
+ *
+ * @param SearchCriteriaInterface $searchCriteria
+ * @return array
+ */
+ private function getSortOrderArray(SearchCriteriaInterface $searchCriteria)
+ {
+ $ordersArray = [];
+ $sortOrders = $searchCriteria->getSortOrders();
+ if (is_array($sortOrders)) {
+ foreach ($sortOrders as $sortOrder) {
+ $ordersArray[$sortOrder->getField()] = $sortOrder->getDirection();
+ }
+ }
+
+ return $ordersArray;
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/FilterArgument/ProductEntityAttributesForAst.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/FilterArgument/ProductEntityAttributesForAst.php
index a547f63b217fe..973b8fbcd6b0f 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/FilterArgument/ProductEntityAttributesForAst.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/FilterArgument/ProductEntityAttributesForAst.php
@@ -23,13 +23,15 @@ class ProductEntityAttributesForAst implements FieldEntityAttributesInterface
private $config;
/**
+ * Additional attributes that are not retrieved by getting fields from ProductInterface
+ *
* @var array
*/
private $additionalAttributes = ['min_price', 'max_price', 'category_id'];
/**
* @param ConfigInterface $config
- * @param array $additionalAttributes
+ * @param string[] $additionalAttributes
*/
public function __construct(
ConfigInterface $config,
@@ -40,7 +42,12 @@ public function __construct(
}
/**
- * {@inheritdoc}
+ * @inheritdoc
+ *
+ * Gather all the product entity attributes that can be filtered by search criteria.
+ * Example format ['attributeNameInGraphQl' => ['type' => 'String'. 'fieldName' => 'attributeNameInSearchCriteria']]
+ *
+ * @return array
*/
public function getEntityAttributes() : array
{
@@ -55,14 +62,20 @@ public function getEntityAttributes() : array
$configElement = $this->config->getConfigElement($interface['interface']);
foreach ($configElement->getFields() as $field) {
- $fields[$field->getName()] = 'String';
+ $fields[$field->getName()] = [
+ 'type' => 'String',
+ 'fieldName' => $field->getName(),
+ ];
}
}
- foreach ($this->additionalAttributes as $attribute) {
- $fields[$attribute] = 'String';
+ foreach ($this->additionalAttributes as $attributeName) {
+ $fields[$attributeName] = [
+ 'type' => 'String',
+ 'fieldName' => $attributeName,
+ ];
}
- return array_keys($fields);
+ return $fields;
}
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/FieldSelection.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/FieldSelection.php
new file mode 100644
index 0000000000000..ffa0a3e6848e1
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/FieldSelection.php
@@ -0,0 +1,93 @@
+fieldTranslator = $fieldTranslator;
+ }
+
+ /**
+ * Get requested fields from products query
+ *
+ * @param ResolveInfo $resolveInfo
+ * @return string[]
+ */
+ public function getProductsFieldSelection(ResolveInfo $resolveInfo): array
+ {
+ return $this->getProductFields($resolveInfo);
+ }
+
+ /**
+ * Return field names for all requested product fields.
+ *
+ * @param ResolveInfo $info
+ * @return string[]
+ */
+ private function getProductFields(ResolveInfo $info): array
+ {
+ $fieldNames = [];
+ foreach ($info->fieldNodes as $node) {
+ if ($node->name->value !== 'products' && $node->name->value !== 'variants') {
+ continue;
+ }
+ foreach ($node->selectionSet->selections as $selection) {
+ if ($selection->name->value !== 'items' && $selection->name->value !== 'product') {
+ continue;
+ }
+ $fieldNames[] = $this->collectProductFieldNames($selection, $fieldNames);
+ }
+ }
+ if (!empty($fieldNames)) {
+ $fieldNames = array_merge(...$fieldNames);
+ }
+ return $fieldNames;
+ }
+
+ /**
+ * Collect field names for each node in selection
+ *
+ * @param SelectionNode $selection
+ * @param array $fieldNames
+ * @return array
+ */
+ private function collectProductFieldNames(SelectionNode $selection, array $fieldNames = []): array
+ {
+ foreach ($selection->selectionSet->selections as $itemSelection) {
+ if ($itemSelection->kind === 'InlineFragment') {
+ foreach ($itemSelection->selectionSet->selections as $inlineSelection) {
+ if ($inlineSelection->kind === 'InlineFragment') {
+ continue;
+ }
+ $fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value);
+ }
+ continue;
+ }
+ $fieldNames[] = $this->fieldTranslator->translate($itemSelection->name->value);
+ }
+
+ return $fieldNames;
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php
index 62e2f0c488c6c..cc25af44fdfbe 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php
@@ -12,7 +12,6 @@
use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product;
use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult;
use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResultFactory;
-use Magento\Framework\GraphQl\Query\FieldTranslator;
/**
* Retrieve filtered product data based off given search criteria in a format that GraphQL can interpret.
@@ -30,31 +29,31 @@ class Filter
private $productDataProvider;
/**
- * @var FieldTranslator
+ * @var \Magento\Catalog\Model\Layer\Resolver
*/
- private $fieldTranslator;
+ private $layerResolver;
/**
- * @var \Magento\Catalog\Model\Layer\Resolver
+ * FieldSelection
*/
- private $layerResolver;
+ private $fieldSelection;
/**
* @param SearchResultFactory $searchResultFactory
* @param Product $productDataProvider
* @param \Magento\Catalog\Model\Layer\Resolver $layerResolver
- * @param FieldTranslator $fieldTranslator
+ * @param FieldSelection $fieldSelection
*/
public function __construct(
SearchResultFactory $searchResultFactory,
Product $productDataProvider,
\Magento\Catalog\Model\Layer\Resolver $layerResolver,
- FieldTranslator $fieldTranslator
+ FieldSelection $fieldSelection
) {
$this->searchResultFactory = $searchResultFactory;
$this->productDataProvider = $productDataProvider;
- $this->fieldTranslator = $fieldTranslator;
$this->layerResolver = $layerResolver;
+ $this->fieldSelection = $fieldSelection;
}
/**
@@ -70,7 +69,7 @@ public function getResult(
ResolveInfo $info,
bool $isSearch = false
): SearchResult {
- $fields = $this->getProductFields($info);
+ $fields = $this->fieldSelection->getProductsFieldSelection($info);
$products = $this->productDataProvider->getList($searchCriteria, $fields, $isSearch);
$productArray = [];
/** @var \Magento\Catalog\Model\Product $product */
@@ -79,42 +78,11 @@ public function getResult(
$productArray[$product->getId()]['model'] = $product;
}
- return $this->searchResultFactory->create($products->getTotalCount(), $productArray);
- }
-
- /**
- * Return field names for all requested product fields.
- *
- * @param ResolveInfo $info
- * @return string[]
- */
- private function getProductFields(ResolveInfo $info) : array
- {
- $fieldNames = [];
- foreach ($info->fieldNodes as $node) {
- if ($node->name->value !== 'products') {
- continue;
- }
- foreach ($node->selectionSet->selections as $selection) {
- if ($selection->name->value !== 'items') {
- continue;
- }
-
- foreach ($selection->selectionSet->selections as $itemSelection) {
- if ($itemSelection->kind === 'InlineFragment') {
- foreach ($itemSelection->selectionSet->selections as $inlineSelection) {
- if ($inlineSelection->kind === 'InlineFragment') {
- continue;
- }
- $fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value);
- }
- continue;
- }
- $fieldNames[] = $this->fieldTranslator->translate($itemSelection->name->value);
- }
- }
- }
-
- return $fieldNames;
+ return $this->searchResultFactory->create(
+ [
+ 'totalCount' => $products->getTotalCount(),
+ 'productsSearchResult' => $productArray
+ ]
+ );
}
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php
index bc40c664425ff..ef83cc6132ecc 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php
@@ -7,12 +7,13 @@
namespace Magento\CatalogGraphQl\Model\Resolver\Products\Query;
+use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\ProductSearch;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Framework\Api\Search\SearchCriteriaInterface;
-use Magento\CatalogGraphQl\Model\Resolver\Products\SearchCriteria\Helper\Filter as FilterHelper;
use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult;
use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResultFactory;
use Magento\Search\Api\SearchInterface;
+use Magento\Framework\Api\Search\SearchCriteriaInterfaceFactory;
/**
* Full text search for catalog using given search criteria.
@@ -25,52 +26,52 @@ class Search
private $search;
/**
- * @var FilterHelper
+ * @var SearchResultFactory
*/
- private $filterHelper;
+ private $searchResultFactory;
/**
- * @var Filter
+ * @var \Magento\Search\Model\Search\PageSizeProvider
*/
- private $filterQuery;
+ private $pageSizeProvider;
/**
- * @var SearchResultFactory
+ * @var SearchCriteriaInterfaceFactory
*/
- private $searchResultFactory;
+ private $searchCriteriaFactory;
/**
- * @var \Magento\Framework\EntityManager\MetadataPool
+ * @var FieldSelection
*/
- private $metadataPool;
+ private $fieldSelection;
/**
- * @var \Magento\Search\Model\Search\PageSizeProvider
+ * @var ProductSearch
*/
- private $pageSizeProvider;
+ private $productsProvider;
/**
* @param SearchInterface $search
- * @param FilterHelper $filterHelper
- * @param Filter $filterQuery
* @param SearchResultFactory $searchResultFactory
- * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool
* @param \Magento\Search\Model\Search\PageSizeProvider $pageSize
+ * @param SearchCriteriaInterfaceFactory $searchCriteriaFactory
+ * @param FieldSelection $fieldSelection
+ * @param ProductSearch $productsProvider
*/
public function __construct(
SearchInterface $search,
- FilterHelper $filterHelper,
- Filter $filterQuery,
SearchResultFactory $searchResultFactory,
- \Magento\Framework\EntityManager\MetadataPool $metadataPool,
- \Magento\Search\Model\Search\PageSizeProvider $pageSize
+ \Magento\Search\Model\Search\PageSizeProvider $pageSize,
+ SearchCriteriaInterfaceFactory $searchCriteriaFactory,
+ FieldSelection $fieldSelection,
+ ProductSearch $productsProvider
) {
$this->search = $search;
- $this->filterHelper = $filterHelper;
- $this->filterQuery = $filterQuery;
$this->searchResultFactory = $searchResultFactory;
- $this->metadataPool = $metadataPool;
$this->pageSizeProvider = $pageSize;
+ $this->searchCriteriaFactory = $searchCriteriaFactory;
+ $this->fieldSelection = $fieldSelection;
+ $this->productsProvider = $productsProvider;
}
/**
@@ -81,11 +82,12 @@ public function __construct(
* @return SearchResult
* @throws \Exception
*/
- public function getResult(SearchCriteriaInterface $searchCriteria, ResolveInfo $info) : SearchResult
- {
- $idField = $this->metadataPool->getMetadata(
- \Magento\Catalog\Api\Data\ProductInterface::class
- )->getIdentifierField();
+ public function getResult(
+ SearchCriteriaInterface $searchCriteria,
+ ResolveInfo $info
+ ): SearchResult {
+ $queryFields = $this->fieldSelection->getProductsFieldSelection($info);
+
$realPageSize = $searchCriteria->getPageSize();
$realCurrentPage = $searchCriteria->getCurrentPage();
// Current page must be set to 0 and page size to max for search to grab all ID's as temporary workaround
@@ -94,64 +96,39 @@ public function getResult(SearchCriteriaInterface $searchCriteria, ResolveInfo $
$searchCriteria->setCurrentPage(0);
$itemsResults = $this->search->search($searchCriteria);
- $ids = [];
- $searchIds = [];
- foreach ($itemsResults->getItems() as $item) {
- $ids[$item->getId()] = null;
- $searchIds[] = $item->getId();
- }
-
- $filter = $this->filterHelper->generate($idField, 'in', $searchIds);
- $searchCriteria = $this->filterHelper->remove($searchCriteria, 'search_term');
- $searchCriteria = $this->filterHelper->add($searchCriteria, $filter);
- $searchResult = $this->filterQuery->getResult($searchCriteria, $info, true);
-
- $searchCriteria->setPageSize($realPageSize);
- $searchCriteria->setCurrentPage($realCurrentPage);
- $paginatedProducts = $this->paginateList($searchResult, $searchCriteria);
-
- $products = [];
- if (!isset($searchCriteria->getSortOrders()[0])) {
- foreach ($paginatedProducts as $product) {
- if (in_array($product[$idField], $searchIds)) {
- $ids[$product[$idField]] = $product;
- }
- }
- $products = array_filter($ids);
- } else {
- foreach ($paginatedProducts as $product) {
- $productId = isset($product['entity_id']) ? $product['entity_id'] : $product[$idField];
- if (in_array($productId, $searchIds)) {
- $products[] = $product;
- }
- }
- }
+ //Create copy of search criteria without conditions (conditions will be applied by joining search result)
+ $searchCriteriaCopy = $this->searchCriteriaFactory->create()
+ ->setSortOrders($searchCriteria->getSortOrders())
+ ->setPageSize($realPageSize)
+ ->setCurrentPage($realCurrentPage);
- return $this->searchResultFactory->create($searchResult->getTotalCount(), $products);
- }
+ $searchResults = $this->productsProvider->getList($searchCriteriaCopy, $itemsResults, $queryFields);
- /**
- * Paginate an array of Ids that get pulled back in search based off search criteria and total count.
- *
- * @param SearchResult $searchResult
- * @param SearchCriteriaInterface $searchCriteria
- * @return int[]
- */
- private function paginateList(SearchResult $searchResult, SearchCriteriaInterface $searchCriteria) : array
- {
- $length = $searchCriteria->getPageSize();
- // Search starts pages from 0
- $offset = $length * ($searchCriteria->getCurrentPage() - 1);
-
- if ($searchCriteria->getPageSize()) {
- $maxPages = ceil($searchResult->getTotalCount() / $searchCriteria->getPageSize());
+ //possible division by 0
+ if ($realPageSize) {
+ $maxPages = (int)ceil($searchResults->getTotalCount() / $realPageSize);
} else {
$maxPages = 0;
}
+ $searchCriteria->setPageSize($realPageSize);
+ $searchCriteria->setCurrentPage($realCurrentPage);
- if ($searchCriteria->getCurrentPage() > $maxPages && $searchResult->getTotalCount() > 0) {
- $offset = (int)$maxPages;
+ $productArray = [];
+ /** @var \Magento\Catalog\Model\Product $product */
+ foreach ($searchResults->getItems() as $product) {
+ $productArray[$product->getId()] = $product->getData();
+ $productArray[$product->getId()]['model'] = $product;
}
- return array_slice($searchResult->getProductsSearchResult(), $offset, $length);
+
+ return $this->searchResultFactory->create(
+ [
+ 'totalCount' => $searchResults->getTotalCount(),
+ 'productsSearchResult' => $productArray,
+ 'searchAggregation' => $itemsResults->getAggregations(),
+ 'pageSize' => $realPageSize,
+ 'currentPage' => $realCurrentPage,
+ 'totalPages' => $maxPages,
+ ]
+ );
}
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResult.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResult.php
index 6e229bdc38a31..e4a137413b4c5 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResult.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResult.php
@@ -7,31 +7,21 @@
namespace Magento\CatalogGraphQl\Model\Resolver\Products;
-use Magento\Framework\Api\SearchResultsInterface;
+use Magento\Framework\Api\Search\AggregationInterface;
/**
* Container for a product search holding the item result and the array in the GraphQL-readable product type format.
*/
class SearchResult
{
- /**
- * @var SearchResultsInterface
- */
- private $totalCount;
-
- /**
- * @var array
- */
- private $productsSearchResult;
+ private $data;
/**
- * @param int $totalCount
- * @param array $productsSearchResult
+ * @param array $data
*/
- public function __construct(int $totalCount, array $productsSearchResult)
+ public function __construct(array $data)
{
- $this->totalCount = $totalCount;
- $this->productsSearchResult = $productsSearchResult;
+ $this->data = $data;
}
/**
@@ -41,7 +31,7 @@ public function __construct(int $totalCount, array $productsSearchResult)
*/
public function getTotalCount() : int
{
- return $this->totalCount;
+ return $this->data['totalCount'] ?? 0;
}
/**
@@ -51,6 +41,46 @@ public function getTotalCount() : int
*/
public function getProductsSearchResult() : array
{
- return $this->productsSearchResult;
+ return $this->data['productsSearchResult'] ?? [];
+ }
+
+ /**
+ * Retrieve aggregated search results
+ *
+ * @return AggregationInterface|null
+ */
+ public function getSearchAggregation(): ?AggregationInterface
+ {
+ return $this->data['searchAggregation'] ?? null;
+ }
+
+ /**
+ * Retrieve the page size for the search
+ *
+ * @return int
+ */
+ public function getPageSize(): int
+ {
+ return $this->data['pageSize'] ?? 0;
+ }
+
+ /**
+ * Retrieve the current page for the search
+ *
+ * @return int
+ */
+ public function getCurrentPage(): int
+ {
+ return $this->data['currentPage'] ?? 0;
+ }
+
+ /**
+ * Retrieve total pages for the search
+ *
+ * @return int
+ */
+ public function getTotalPages(): int
+ {
+ return $this->data['totalPages'] ?? 0;
}
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResultFactory.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResultFactory.php
index aec9362f47c3a..479e6a3f96235 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResultFactory.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchResultFactory.php
@@ -30,15 +30,15 @@ public function __construct(ObjectManagerInterface $objectManager)
/**
* Instantiate SearchResult
*
- * @param int $totalCount
- * @param array $productsSearchResult
+ * @param array $data
* @return SearchResult
*/
- public function create(int $totalCount, array $productsSearchResult) : SearchResult
- {
+ public function create(
+ array $data
+ ): SearchResult {
return $this->objectManager->create(
SearchResult::class,
- ['totalCount' => $totalCount, 'productsSearchResult' => $productsSearchResult]
+ ['data' => $data]
);
}
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryId.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryId.php
new file mode 100644
index 0000000000000..4b3e0a1a58dfd
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/RootCategoryId.php
@@ -0,0 +1,26 @@
+getExtensionAttributes()->getStore()->getRootCategoryId();
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Plugin/DesignLoader.php b/app/code/Magento/CatalogGraphQl/Plugin/DesignLoader.php
new file mode 100644
index 0000000000000..cfb99ce270c21
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Plugin/DesignLoader.php
@@ -0,0 +1,71 @@
+designLoader = $designLoader;
+ $this->messageManager = $messageManager;
+ }
+
+ /**
+ * Before create load the design files
+ *
+ * @param ImageFactory $subject
+ * @param Product $product
+ * @param string $imageId
+ * @param array|null $attributes
+ *
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function beforeCreate(
+ ImageFactory $subject,
+ Product $product,
+ string $imageId,
+ array $attributes = null
+ ) {
+ try {
+ $this->designLoader->load();
+ } catch (\Magento\Framework\Exception\LocalizedException $e) {
+ if ($e->getPrevious() instanceof \Magento\Framework\Config\Dom\ValidationException) {
+ /** @var MessageInterface $message */
+ $message = $this->messageManager
+ ->createMessage(MessageInterface::TYPE_ERROR)
+ ->setText($e->getMessage());
+ $this->messageManager->addUniqueMessages([$message]);
+ }
+ }
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php b/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php
new file mode 100644
index 0000000000000..992ab50467c72
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Plugin/Search/Request/ConfigReader.php
@@ -0,0 +1,286 @@
+generatorResolver = $generatorResolver;
+ $this->productAttributeCollectionFactory = $productAttributeCollectionFactory;
+ $this->exactMatchAttributes = array_merge($this->exactMatchAttributes, $exactMatchAttributes);
+ }
+
+ /**
+ * Merge reader's value with generated
+ *
+ * @param \Magento\Framework\Config\ReaderInterface $subject
+ * @param array $result
+ * @return array
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterRead(
+ \Magento\Framework\Config\ReaderInterface $subject,
+ array $result
+ ) {
+ $searchRequestNameWithAggregation = $this->generateRequest();
+ $searchRequest = $searchRequestNameWithAggregation;
+ $searchRequest['queries'][$this->requestName] = $searchRequest['queries'][$this->requestNameWithAggregation];
+ unset($searchRequest['queries'][$this->requestNameWithAggregation], $searchRequest['aggregations']);
+
+ return array_merge_recursive(
+ $result,
+ [
+ $this->requestNameWithAggregation => $searchRequestNameWithAggregation,
+ $this->requestName => $searchRequest,
+ ]
+ );
+ }
+
+ /**
+ * Retrieve searchable attributes
+ *
+ * @return Attribute[]
+ */
+ private function getSearchableAttributes(): array
+ {
+ $attributes = [];
+ /** @var Collection $productAttributes */
+ $productAttributes = $this->productAttributeCollectionFactory->create();
+ $productAttributes->addFieldToFilter(
+ ['is_searchable', 'is_visible_in_advanced_search', 'is_filterable', 'is_filterable_in_search'],
+ [1, 1, [1, 2], 1]
+ );
+
+ /** @var Attribute $attribute */
+ foreach ($productAttributes->getItems() as $attribute) {
+ $attributes[$attribute->getAttributeCode()] = $attribute;
+ }
+
+ return $attributes;
+ }
+
+ /**
+ * Generate search request for search products via GraphQL
+ *
+ * @return array
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ */
+ private function generateRequest()
+ {
+ $request = [];
+ foreach ($this->getSearchableAttributes() as $attribute) {
+ if (\in_array($attribute->getAttributeCode(), ['price', 'visibility', 'category_ids'])) {
+ //some fields have special semantics
+ continue;
+ }
+ $queryName = $attribute->getAttributeCode() . '_query';
+ $filterName = $attribute->getAttributeCode() . RequestGenerator::FILTER_SUFFIX;
+ $request['queries'][$this->requestNameWithAggregation]['queryReference'][] = [
+ 'clause' => 'must',
+ 'ref' => $queryName,
+ ];
+
+ switch ($attribute->getBackendType()) {
+ case 'static':
+ case 'text':
+ case 'varchar':
+ if ($this->isExactMatchAttribute($attribute)) {
+ $request['queries'][$queryName] = $this->generateFilterQuery($queryName, $filterName);
+ $request['filters'][$filterName] = $this->generateTermFilter($filterName, $attribute);
+ } else {
+ $request['queries'][$queryName] = $this->generateMatchQuery($queryName, $attribute);
+ }
+ break;
+ case 'decimal':
+ case 'datetime':
+ case 'date':
+ $request['queries'][$queryName] = $this->generateFilterQuery($queryName, $filterName);
+ $request['filters'][$filterName] = $this->generateRangeFilter($filterName, $attribute);
+ break;
+ default:
+ $request['queries'][$queryName] = $this->generateFilterQuery($queryName, $filterName);
+ $request['filters'][$filterName] = $this->generateTermFilter($filterName, $attribute);
+ }
+ $generator = $this->generatorResolver->getGeneratorForType($attribute->getBackendType());
+
+ if ($attribute->getData(EavAttributeInterface::IS_FILTERABLE)) {
+ $bucketName = $attribute->getAttributeCode() . self::BUCKET_SUFFIX;
+ $request['aggregations'][$bucketName] = $generator->getAggregationData($attribute, $bucketName);
+ }
+
+ $this->addSearchAttributeToFullTextSearch($attribute, $request);
+ }
+
+ return $request;
+ }
+
+ /**
+ * Add attribute with specified boost to "search" query used in full text search
+ *
+ * @param Attribute $attribute
+ * @param array $request
+ * @return void
+ */
+ private function addSearchAttributeToFullTextSearch(Attribute $attribute, &$request): void
+ {
+ // Match search by custom price attribute isn't supported
+ if ($attribute->getFrontendInput() !== 'price') {
+ $request['queries']['search']['match'][] = [
+ 'field' => $attribute->getAttributeCode(),
+ 'boost' => $attribute->getSearchWeight() ?: 1,
+ ];
+ }
+ }
+
+ /**
+ * Return array representation of range filter
+ *
+ * @param string $filterName
+ * @param Attribute $attribute
+ * @return array
+ */
+ private function generateRangeFilter(string $filterName, Attribute $attribute)
+ {
+ return [
+ 'field' => $attribute->getAttributeCode(),
+ 'name' => $filterName,
+ 'type' => FilterInterface::TYPE_RANGE,
+ 'from' => '$' . $attribute->getAttributeCode() . '.from$',
+ 'to' => '$' . $attribute->getAttributeCode() . '.to$',
+ ];
+ }
+
+ /**
+ * Return array representation of term filter
+ *
+ * @param string $filterName
+ * @param Attribute $attribute
+ * @return array
+ */
+ private function generateTermFilter(string $filterName, Attribute $attribute)
+ {
+ return [
+ 'type' => FilterInterface::TYPE_TERM,
+ 'name' => $filterName,
+ 'field' => $attribute->getAttributeCode(),
+ 'value' => '$' . $attribute->getAttributeCode() . '$',
+ ];
+ }
+
+ /**
+ * Return array representation of query based on filter
+ *
+ * @param string $queryName
+ * @param string $filterName
+ * @return array
+ */
+ private function generateFilterQuery(string $queryName, string $filterName)
+ {
+ return [
+ 'name' => $queryName,
+ 'type' => QueryInterface::TYPE_FILTER,
+ 'filterReference' => [
+ [
+ 'ref' => $filterName,
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Return array representation of match query
+ *
+ * @param string $queryName
+ * @param Attribute $attribute
+ * @return array
+ */
+ private function generateMatchQuery(string $queryName, Attribute $attribute)
+ {
+ return [
+ 'name' => $queryName,
+ 'type' => 'matchQuery',
+ 'value' => '$' . $attribute->getAttributeCode() . '$',
+ 'match' => [
+ [
+ 'field' => $attribute->getAttributeCode(),
+ 'boost' => $attribute->getSearchWeight() ?: 1,
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Check if attribute's filter should use exact match
+ *
+ * @param Attribute $attribute
+ * @return bool
+ */
+ private function isExactMatchAttribute(Attribute $attribute)
+ {
+ if (in_array($attribute->getFrontendInput(), ['select', 'multiselect'])) {
+ return true;
+ }
+ if (in_array($attribute->getAttributeCode(), $this->exactMatchAttributes)) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Product/Price/DiscountTest.php b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Product/Price/DiscountTest.php
new file mode 100644
index 0000000000000..5ebb48f761c06
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Product/Price/DiscountTest.php
@@ -0,0 +1,61 @@
+discount = new Discount();
+ }
+
+ /**
+ * @dataProvider priceDataProvider
+ * @param $regularPrice
+ * @param $finalPrice
+ * @param $expectedAmountOff
+ * @param $expectedPercentOff
+ */
+ public function testGetPriceDiscount($regularPrice, $finalPrice, $expectedAmountOff, $expectedPercentOff)
+ {
+ $discountResult = $this->discount->getDiscountByDifference($regularPrice, $finalPrice);
+
+ $this->assertEquals($expectedAmountOff, $discountResult['amount_off']);
+ $this->assertEquals($expectedPercentOff, $discountResult['percent_off']);
+ }
+
+ /**
+ * Price data provider
+ *
+ * [regularPrice, finalPrice, expectedAmountOff, expectedPercentOff]
+ *
+ * @return array
+ */
+ public function priceDataProvider()
+ {
+ return [
+ [100, 50, 50, 50],
+ [.1, .05, .05, 50],
+ [12.50, 10, 2.5, 20],
+ [99.99, 84.99, 15.0, 15],
+ [9999999999.01, 8999999999.11, 999999999.9, 10],
+ [0, 0, 0, 0],
+ [0, 10, 0, 0],
+ [9.95, 9.95, 0, 0],
+ [21.05, 0, 21.05, 100]
+ ];
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/composer.json b/app/code/Magento/CatalogGraphQl/composer.json
index 13fcbe9a7d357..1582f29c25951 100644
--- a/app/code/Magento/CatalogGraphQl/composer.json
+++ b/app/code/Magento/CatalogGraphQl/composer.json
@@ -10,6 +10,7 @@
"magento/module-search": "*",
"magento/module-store": "*",
"magento/module-eav-graph-ql": "*",
+ "magento/module-catalog-search": "*",
"magento/framework": "*"
},
"suggest": {
diff --git a/app/code/Magento/CatalogGraphQl/etc/di.xml b/app/code/Magento/CatalogGraphQl/etc/di.xml
index a5006355ed265..1fe62fc442ecf 100644
--- a/app/code/Magento/CatalogGraphQl/etc/di.xml
+++ b/app/code/Magento/CatalogGraphQl/etc/di.xml
@@ -19,6 +19,8 @@
- Magento\CatalogGraphQl\Model\Config\AttributeReader
- Magento\CatalogGraphQl\Model\Config\CategoryAttributeReader
+ - Magento\CatalogGraphQl\Model\Config\SortAttributeReader
+ - Magento\CatalogGraphQl\Model\Config\FilterAttributeReader
@@ -55,4 +57,18 @@
Magento\Catalog\Model\Api\SearchCriteria\ProductCollectionProcessor
+
+
+
+
+ - sku
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml
index 2292004f3cf01..066a7b38d8967 100644
--- a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml
+++ b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml
@@ -28,6 +28,13 @@
+
+
+
+ - Magento\CatalogGraphQl\Model\AggregationOptionTypeResolver
+
+
+
@@ -48,6 +55,12 @@
- CustomizableRadioOption
- CustomizableCheckboxOption
+ -
+
- ProductAttributeSortInput
+
+ -
+
- ProductAttributeFilterInput
+
@@ -95,4 +108,72 @@
+
+
+
+
+ - Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Price
+ - Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Category
+ - Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Attribute
+
+
+
+
+
+
+
+
+ - Magento\CatalogGraphQl\Model\Resolver\Product\Price\Provider
+
+
+
+
+
+
+
+ -
+
- price
+
+ -
+
-
+
- thumbnail_label
+ - name
+
+ - thumbnail
+
+ -
+
-
+
- small_image_label
+ - name
+
+ - small_image
+
+ -
+
-
+
- image_label
+ - name
+
+ - image
+
+ -
+
-
+
- image_label
+ - name
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - Magento\CatalogGraphQl\Model\Resolver\Category\Image
+
+
+
diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls
index ea56faf94408e..f70a32a1b549e 100644
--- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls
@@ -4,33 +4,36 @@
type Query {
products (
search: String @doc(description: "Performs a full-text search using the specified key words."),
- filter: ProductFilterInput @doc(description: "Identifies which product attributes to search for and return."),
+ filter: ProductAttributeFilterInput @doc(description: "Identifies which product attributes to search for and return."),
pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. This attribute is optional."),
currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."),
- sort: ProductSortInput @doc(description: "Specifies which attribute to sort on, and whether to return the results in ascending or descending order.")
+ sort: ProductAttributeSortInput @doc(description: "Specifies which attributes to sort on, and whether to return the results in ascending or descending order.")
): Products
@resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Products") @doc(description: "The products query searches for products that match the criteria specified in the search and filter attributes.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity")
category (
- id: Int @doc(description: "Id of the category.")
+ id: Int @doc(description: "Id of the category.")
): CategoryTree
- @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") @doc(description: "The category query searches for categories that match the criteria specified in the search and filter attributes.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryTreeIdentity")
+ @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryTree") @doc(description: "The category query searches for categories that match the criteria specified in the search and filter attributes.") @deprecated(reason: "Use 'categoryList' query instead of 'category' query") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoryTreeIdentity")
+ categoryList(
+ filters: CategoryFilterInput @doc(description: "Identifies which Category filter inputs to search for and return.")
+ ): [CategoryTree] @doc(description: "Returns an array of categories based on the specified filters.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryList") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoriesIdentity")
}
-type Price @doc(description: "The Price object defines the price of a product as well as any tax-related adjustments.") {
- amount: Money @doc(description: "The price of a product plus a three-letter currency code.")
- adjustments: [PriceAdjustment] @doc(description: "An array that provides information about tax, weee, or weee_tax adjustments.")
+type Price @doc(description: "Price is deprecated, replaced by ProductPrice. The Price object defines the price of a product as well as any tax-related adjustments.") {
+ amount: Money @deprecated(reason: "Price is deprecated, use ProductPrice.") @doc(description: "The price of a product plus a three-letter currency code.")
+ adjustments: [PriceAdjustment] @deprecated(reason: "Price is deprecated, use ProductPrice.") @doc(description: "An array that provides information about tax, weee, or weee_tax adjustments.")
}
-type PriceAdjustment @doc(description: "The PricedAdjustment object defines the amount of money to apply as an adjustment, the type of adjustment to apply, and whether the item is included or excluded from the adjustment.") {
+type PriceAdjustment @doc(description: "PriceAdjustment is deprecated. Taxes will be included or excluded in the price. The PricedAdjustment object defines the amount of money to apply as an adjustment, the type of adjustment to apply, and whether the item is included or excluded from the adjustment.") {
amount: Money @doc(description: "The amount of the price adjustment and its currency code.")
- code: PriceAdjustmentCodesEnum @doc(description: "Indicates whether the adjustment involves tax, weee, or weee_tax.")
- description: PriceAdjustmentDescriptionEnum @doc(description: "Indicates whether the entity described by the code attribute is included or excluded from the adjustment.")
+ code: PriceAdjustmentCodesEnum @deprecated(reason: "PriceAdjustment is deprecated.") @doc(description: "Indicates whether the adjustment involves tax, weee, or weee_tax.")
+ description: PriceAdjustmentDescriptionEnum @deprecated(reason: "PriceAdjustment is deprecated.") @doc(description: "Indicates whether the entity described by the code attribute is included or excluded from the adjustment.")
}
-enum PriceAdjustmentCodesEnum @doc(description: "Note: This enumeration contains values defined in modules other than the Catalog module.") {
+enum PriceAdjustmentCodesEnum @doc(description: "PriceAdjustment.code is deprecated. This enumeration contains values defined in modules other than the Catalog module.") {
}
-enum PriceAdjustmentDescriptionEnum @doc(description: "This enumeration states whether a price adjustment is included or excluded.") {
+enum PriceAdjustmentDescriptionEnum @doc(description: "PriceAdjustmentDescriptionEnum is deprecated. This enumeration states whether a price adjustment is included or excluded.") {
INCLUDED
EXCLUDED
}
@@ -41,10 +44,26 @@ enum PriceTypeEnum @doc(description: "This enumeration the price type.") {
DYNAMIC
}
-type ProductPrices @doc(description: "The ProductPrices object contains the regular price of an item, as well as its minimum and maximum prices. Only composite products, which include bundle, configurable, and grouped products, can contain a minimum and maximum price.") {
- minimalPrice: Price @doc(description: "The lowest possible final price for all the options defined within a composite product. If you are specifying a price range, this would be the from value.")
- maximalPrice: Price @doc(description: "The highest possible final price for all the options defined within a composite product. If you are specifying a price range, this would be the to value.")
- regularPrice: Price @doc(description: "The base price of a product.")
+type ProductPrices @doc(description: "ProductPrices is deprecated, replaced by PriceRange. The ProductPrices object contains the regular price of an item, as well as its minimum and maximum prices. Only composite products, which include bundle, configurable, and grouped products, can contain a minimum and maximum price.") {
+ minimalPrice: Price @deprecated(reason: "Use PriceRange.minimum_price.") @doc(description: "The lowest possible final price for all the options defined within a composite product. If you are specifying a price range, this would be the from value.")
+ maximalPrice: Price @deprecated(reason: "Use PriceRange.maximum_price.") @doc(description: "The highest possible final price for all the options defined within a composite product. If you are specifying a price range, this would be the to value.")
+ regularPrice: Price @deprecated(reason: "Use regular_price from PriceRange.minimum_price or PriceRange.maximum_price.") @doc(description: "The base price of a product.")
+}
+
+type PriceRange @doc(description: "Price range for a product. If the product has a single price, the minimum and maximum price will be the same."){
+ minimum_price: ProductPrice! @doc(description: "The lowest possible price for the product.")
+ maximum_price: ProductPrice @doc(description: "The highest possible price for the product.")
+}
+
+type ProductPrice @doc(description: "Represents a product price.") {
+ regular_price: Money! @doc(description: "The regular price of the product.")
+ final_price: Money! @doc(description: "The final price of the product after discounts applied.")
+ discount: ProductDiscount @doc(description: "The price discount. Represents the difference between the regular and final price.")
+}
+
+type ProductDiscount @doc(description: "A discount applied to a product price.") {
+ percent_off: Float @doc(description: "The discount expressed a percentage.")
+ amount_off: Float @doc(description: "The actual value of the discount.")
}
type ProductLinks implements ProductLinksInterface @doc(description: "ProductLinks is an implementation of ProductLinksInterface.") {
@@ -58,14 +77,6 @@ interface ProductLinksInterface @typeResolver(class: "Magento\\CatalogGraphQl\\M
position: Int @doc(description: "The position within the list of product links.")
}
-type ProductTierPrices @doc(description: "The ProductTierPrices object defines a tier price, which is a quantity discount offered to a specific customer group.") {
- customer_group_id: String @doc(description: "The ID of the customer group.")
- qty: Float @doc(description: "The number of items that must be purchased to qualify for tier pricing.")
- value: Float @doc(description: "The price of the fixed price item.")
- percentage_value: Float @doc(description: "The percentage discount of the item.")
- website_id: Float @doc(description: "The ID assigned to the website.")
-}
-
interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "The ProductInterface contains attributes that are common to all types of products. Note that descriptions may not be available for custom and EAV attributes.") {
id: Int @doc(description: "The ID number assigned to the product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\EntityIdToId")
name: String @doc(description: "The product name. Customers use this name to identify the product.")
@@ -84,21 +95,21 @@ interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\
thumbnail: ProductImage @doc(description: "The relative path to the product's thumbnail image.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage")
new_from_date: String @doc(description: "The beginning date for new product listings, and determines if the product is featured as a new product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo")
new_to_date: String @doc(description: "The end date for new product listings.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo")
- tier_price: Float @doc(description: "The price when tier pricing is in effect and the items purchased threshold has been reached.")
+ tier_price: Float @deprecated(reason: "Use price_tiers for product tier price information.") @doc(description: "The price when tier pricing is in effect and the items purchased threshold has been reached.")
options_container: String @doc(description: "If the product has multiple options, determines where they appear on the product page.")
created_at: String @doc(description: "Timestamp indicating when the product was created.")
updated_at: String @doc(description: "Timestamp indicating when the product was updated.")
country_of_manufacture: String @doc(description: "The product's country of origin.")
type_id: String @doc(description: "One of simple, virtual, bundle, downloadable, grouped, or configurable.")
websites: [Website] @doc(description: "An array of websites in which the product is available.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Websites")
- product_links: [ProductLinksInterface] @doc(description: "An array of ProductLinks objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductLinks")
+ product_links: [ProductLinksInterface] @doc(description: "An array of ProductLinks objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\BatchProductLinks")
media_gallery_entries: [MediaGalleryEntry] @deprecated(reason: "Use product's `media_gallery` instead") @doc(description: "An array of MediaGalleryEntry objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGalleryEntries")
- tier_prices: [ProductTierPrices] @doc(description: "An array of ProductTierPrices objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\TierPrices")
- price: ProductPrices @doc(description: "A ProductPrices object, indicating the price of an item.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Price")
+ price: ProductPrices @deprecated(reason: "Use price_range for product price information.") @doc(description: "A ProductPrices object, indicating the price of an item.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Price")
+ price_range: PriceRange! @doc(description: "A PriceRange object, indicating the range of prices for the product") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\PriceRange")
gift_message_available: String @doc(description: "Indicates whether a gift message is available.")
manufacturer: Int @doc(description: "A number representing the product's manufacturer.")
categories: [CategoryInterface] @doc(description: "The categories assigned to a product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Categories") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoriesIdentity")
- canonical_url: String @doc(description: "Canonical URL.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CanonicalUrl")
+ canonical_url: String @doc(description: "Relative canonical URL. This value is returned only if the system setting 'Use Canonical Link Meta Tag For Products' is enabled") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CanonicalUrl")
media_gallery: [MediaGalleryInterface] @doc(description: "An array of Media Gallery objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGallery")
}
@@ -187,7 +198,7 @@ type CustomizableFileValue @doc(description: "CustomizableFileValue defines the
interface MediaGalleryInterface @doc(description: "Contains basic information about a product image or video.") @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\MediaGalleryTypeResolver") {
url: String @doc(description: "The URL of the product image or video.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGallery\\Url")
- label: String @doc(description: "The label of the product image or video.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGallery\\Label")
+ label: String @doc(description: "The label of the product image or video.")
}
type ProductImage implements MediaGalleryInterface @doc(description: "Product image information. Contains the image URL and label.") {
@@ -212,6 +223,7 @@ interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model
path_in_store: String @doc(description: "Category path in store.")
url_key: String @doc(description: "The url key assigned to the category.")
url_path: String @doc(description: "The url path assigned to the category.")
+ canonical_url: String @doc(description: "Relative canonical URL. This value is returned only if the system setting 'Use Canonical Link Meta Tag For Categories' is enabled") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CanonicalUrl")
position: Int @doc(description: "The position of the category relative to other categories at the same level in tree.")
level: Int @doc(description: "Indicates the depth of the category within the tree.")
created_at: String @doc(description: "Timestamp indicating when the category was created.")
@@ -221,7 +233,7 @@ interface CategoryInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model
products(
pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. This attribute is optional."),
currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."),
- sort: ProductSortInput @doc(description: "Specifies which attribute to sort on, and whether to return the results in ascending or descending order.")
+ sort: ProductAttributeSortInput @doc(description: "Specifies which attributes to sort on, and whether to return the results in ascending or descending order.")
): CategoryProducts @doc(description: "The list of products assigned to the category.") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\Identity") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Products")
breadcrumbs: [Breadcrumb] @doc(description: "Breadcrumbs, parent categories info.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\Breadcrumbs")
}
@@ -231,6 +243,7 @@ type Breadcrumb @doc(description: "Breadcrumb item."){
category_name: String @doc(description: "Category name.")
category_level: Int @doc(description: "Category level.")
category_url_key: String @doc(description: "Category URL key.")
+ category_url_path: String @doc(description: "Category URL path.")
}
type CustomizableRadioOption implements CustomizableOptionInterface @doc(description: "CustomizableRadioOption contains information about a set of radio buttons that are defined as part of a customizable option.") {
@@ -270,7 +283,8 @@ type Products @doc(description: "The Products object is the top-level object ret
items: [ProductInterface] @doc(description: "An array of products that match the specified search criteria.")
page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query.")
total_count: Int @doc(description: "The number of products returned.")
- filters: [LayerFilter] @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\LayerFilters") @doc(description: "Layered navigation filters array.")
+ filters: [LayerFilter] @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\LayerFilters") @doc(description: "Layered navigation filters array.") @deprecated(reason: "Use aggregations instead")
+ aggregations: [Aggregation] @doc(description: "Layered navigation aggregations.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Aggregations")
sort_fields: SortFields @doc(description: "An object that includes the default sort field and all available sort fields.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\SortFields")
}
@@ -280,7 +294,18 @@ type CategoryProducts @doc(description: "The category products object returned i
total_count: Int @doc(description: "The number of products returned.")
}
-input ProductFilterInput @doc(description: "ProductFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") {
+input ProductAttributeFilterInput @doc(description: "ProductAttributeFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") {
+ category_id: FilterEqualTypeInput @doc(description: "Filter product by category id")
+}
+
+input CategoryFilterInput @doc(description: "CategoryFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.")
+{
+ ids: FilterEqualTypeInput @doc(description: "Filter by category ID that uniquely identifies the category.")
+ url_key: FilterEqualTypeInput @doc(description: "Filter by the part of the URL that identifies the category")
+ name: FilterMatchTypeInput @doc(description: "Filter by the display name of the category.")
+}
+
+input ProductFilterInput @doc(description: "ProductFilterInput is deprecated, use @ProductAttributeFilterInput instead. ProductFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") {
name: FilterTypeInput @doc(description: "The product name. Customers use this name to identify the product.")
sku: FilterTypeInput @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.")
description: FilterTypeInput @doc(description: "Detailed information about the product. The value can include simple HTML tags.")
@@ -333,7 +358,7 @@ type ProductMediaGalleryEntriesVideoContent @doc(description: "ProductMediaGalle
video_metadata: String @doc(description: "Optional data about the video.")
}
-input ProductSortInput @doc(description: "ProductSortInput specifies the attribute to use for sorting search results and indicates whether the results are sorted in ascending or descending order.") {
+input ProductSortInput @doc(description: "ProductSortInput is deprecated, use @ProductAttributeSortInput instead. ProductSortInput specifies the attribute to use for sorting search results and indicates whether the results are sorted in ascending or descending order.") {
name: SortEnum @doc(description: "The product name. Customers use this name to identify the product.")
sku: SortEnum @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.")
description: SortEnum @doc(description: "Detailed information about the product. The value can include simple HTML tags.")
@@ -367,6 +392,12 @@ input ProductSortInput @doc(description: "ProductSortInput specifies the attribu
gift_message_available: SortEnum @doc(description: "Indicates whether a gift message is available.")
}
+input ProductAttributeSortInput @doc(description: "ProductAttributeSortInput specifies the attribute to use for sorting search results and indicates whether the results are sorted in ascending or descending order. It's possible to sort products using searchable attributes with enabled 'Use in Filter Options' option")
+{
+ relevance: SortEnum @doc(description: "Sort by the search relevance score (default).")
+ position: SortEnum @doc(description: "Sort by the position assigned to each product.")
+}
+
type MediaGalleryEntry @doc(description: "MediaGalleryEntry defines characteristics about images and videos associated with a specific product.") {
id: Int @doc(description: "The identifier assigned to the object.")
media_type: String @doc(description: "image or video.")
@@ -380,22 +411,39 @@ type MediaGalleryEntry @doc(description: "MediaGalleryEntry defines characterist
}
type LayerFilter {
- name: String @doc(description: "Layered navigation filter name.")
- request_var: String @doc(description: "Request variable name for filter query.")
- filter_items_count: Int @doc(description: "Count of filter items in filter group.")
- filter_items: [LayerFilterItemInterface] @doc(description: "Array of filter items.")
+ name: String @doc(description: "Layered navigation filter name.") @deprecated(reason: "Use Aggregation.label instead.")
+ request_var: String @doc(description: "Request variable name for filter query.") @deprecated(reason: "Use Aggregation.attribute_code instead.")
+ filter_items_count: Int @doc(description: "Count of filter items in filter group.") @deprecated(reason: "Use Aggregation.count instead.")
+ filter_items: [LayerFilterItemInterface] @doc(description: "Array of filter items.") @deprecated(reason: "Use Aggregation.options instead.")
}
interface LayerFilterItemInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\LayerFilterItemTypeResolverComposite") {
- label: String @doc(description: "Filter label.")
- value_string: String @doc(description: "Value for filter request variable to be used in query.")
- items_count: Int @doc(description: "Count of items by filter.")
+ label: String @doc(description: "Filter label.") @deprecated(reason: "Use AggregationOption.label instead.")
+ value_string: String @doc(description: "Value for filter request variable to be used in query.") @deprecated(reason: "Use AggregationOption.value instead.")
+ items_count: Int @doc(description: "Count of items by filter.") @deprecated(reason: "Use AggregationOption.count instead.")
}
type LayerFilterItem implements LayerFilterItemInterface {
}
+type Aggregation @doc(description: "A bucket that contains information for each filterable option (such as price, category ID, and custom attributes).") {
+ count: Int @doc(description: "The number of options in the aggregation group.")
+ label: String @doc(description: "The aggregation display name.")
+ attribute_code: String! @doc(description: "Attribute code of the aggregation group.")
+ options: [AggregationOption] @doc(description: "Array of options for the aggregation.")
+}
+
+interface AggregationOptionInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\AggregationOptionTypeResolverComposite") {
+ count: Int @doc(description: "The number of items that match the aggregation option.")
+ label: String @doc(description: "Aggregation option display label.")
+ value: String! @doc(description: "The internal ID that represents the value of the option.")
+}
+
+type AggregationOption implements AggregationOptionInterface {
+
+}
+
type SortField {
value: String @doc(description: "Attribute code of sort field.")
label: String @doc(description: "Label of sort field.")
@@ -416,6 +464,7 @@ type StoreConfig @doc(description: "The type contains information about a store
grid_per_page : Int @doc(description: "Products per Page on Grid Default Value.")
list_per_page : Int @doc(description: "Products per Page on List Default Value.")
catalog_default_sort_by : String @doc(description: "Default Sort By.")
+ root_category_id: Int @doc(description: "The ID of the root category") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\RootCategoryId")
}
type ProductVideo @doc(description: "Contains information about a product video.") implements MediaGalleryInterface {
diff --git a/app/code/Magento/CatalogGraphQl/etc/search_request.xml b/app/code/Magento/CatalogGraphQl/etc/search_request.xml
new file mode 100644
index 0000000000000..ab1eea9eb6fda
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/etc/search_request.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+ 10000
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+ 10000
+
+
diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php
index 428c61c7fec0f..5baa4b4274be5 100644
--- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php
+++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php
@@ -484,7 +484,9 @@ protected function initTypeModels()
}
if ($model->isSuitable()) {
$this->_productTypeModels[$productTypeName] = $model;
+ // phpcs:ignore Magento2.Performance.ForeachArrayMerge
$this->_disabledAttrs = array_merge($this->_disabledAttrs, $model->getDisabledAttrs());
+ // phpcs:ignore Magento2.Performance.ForeachArrayMerge
$this->_indexValueAttributes = array_merge(
$this->_indexValueAttributes,
$model->getIndexValueAttributes()
@@ -526,7 +528,7 @@ protected function getMediaGallery(array $productIds)
if (empty($productIds)) {
return [];
}
-
+
$productEntityJoinField = $this->getProductEntityLinkField();
$select = $this->_connection->select()->from(
@@ -710,6 +712,21 @@ public function _getHeaderColumns()
return $this->_customHeadersMapping($this->rowCustomizer->addHeaderColumns($this->_headerColumns));
}
+ /**
+ * Return non-system attributes
+
+ * @return array
+ */
+ private function getNonSystemAttributes(): array
+ {
+ $attrKeys = [];
+ foreach ($this->filterAttributeCollection($this->getAttributeCollection()) as $attribute) {
+ $attrKeys[] = $attribute->getAttributeCode();
+ }
+
+ return array_diff($this->_getExportMainAttrCodes(), $this->_customHeadersMapping($attrKeys));
+ }
+
/**
* Set headers columns
*
@@ -722,6 +739,18 @@ public function _getHeaderColumns()
*/
protected function setHeaderColumns($customOptionsData, $stockItemRows)
{
+ $exportAttributes = (
+ array_key_exists("skip_attr", $this->_parameters) && count($this->_parameters["skip_attr"])
+ ) ?
+ array_intersect(
+ $this->_getExportMainAttrCodes(),
+ array_merge(
+ $this->_customHeadersMapping($this->_getExportAttrCodes()),
+ $this->getNonSystemAttributes()
+ )
+ ) :
+ $this->_getExportMainAttrCodes();
+
if (!$this->_headerColumns) {
$this->_headerColumns = array_merge(
[
@@ -732,7 +761,7 @@ protected function setHeaderColumns($customOptionsData, $stockItemRows)
self::COL_CATEGORY,
self::COL_PRODUCT_WEBSITES,
],
- $this->_getExportMainAttrCodes(),
+ $exportAttributes,
[self::COL_ADDITIONAL_ATTRIBUTES],
reset($stockItemRows) ? array_keys(end($stockItemRows)) : [],
[
@@ -923,6 +952,7 @@ protected function getExportData()
foreach ($rawData as $productId => $productData) {
foreach ($productData as $storeId => $dataRow) {
if ($storeId == Store::DEFAULT_STORE_ID && isset($stockItemRows[$productId])) {
+ // phpcs:ignore Magento2.Performance.ForeachArrayMerge
$dataRow = array_merge($dataRow, $stockItemRows[$productId]);
}
$this->appendMultirowData($dataRow, $multirawData);
@@ -1330,7 +1360,7 @@ private function appendMultirowData(&$dataRow, $multiRawData)
$dataRow[self::COL_SKU] = $sku;
$dataRow[self::COL_ATTR_SET] = $attributeSet;
$dataRow[self::COL_TYPE] = $type;
-
+
return $dataRow;
}
diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php
index 4ff995c2a872c..8f70ea88f4ba7 100644
--- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php
+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php
@@ -141,7 +141,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity
const COL_PRODUCT_WEBSITES = '_product_websites';
/**
- * Media gallery attribute code.
+ * Attribute code for media gallery.
*/
const MEDIA_GALLERY_ATTRIBUTE_CODE = 'media_gallery';
@@ -151,12 +151,12 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity
const COL_MEDIA_IMAGE = '_media_image';
/**
- * Inventory use config.
+ * Inventory use config label.
*/
const INVENTORY_USE_CONFIG = 'Use Config';
/**
- * Inventory use config prefix.
+ * Prefix for inventory use config.
*/
const INVENTORY_USE_CONFIG_PREFIX = 'use_config_';
@@ -302,6 +302,9 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity
ValidatorInterface::ERROR_DUPLICATE_URL_KEY => 'Url key: \'%s\' was already generated for an item with the SKU: \'%s\'. You need to specify the unique URL key manually',
ValidatorInterface::ERROR_DUPLICATE_MULTISELECT_VALUES => 'Value for multiselect attribute %s contains duplicated values',
'invalidNewToDateValue' => 'Make sure new_to_date is later than or the same as new_from_date',
+ // Can't add new translated strings in patch release
+ 'invalidLayoutUpdate' => 'Invalid format.',
+ 'insufficientPermissions' => 'Invalid format.',
];
//@codingStandardsIgnoreEnd
@@ -1195,7 +1198,7 @@ protected function _initTypeModels()
// phpcs:disable Magento2.Performance.ForeachArrayMerge.ForeachArrayMerge
$this->_fieldsMap = array_merge($this->_fieldsMap, $model->getCustomFieldsMapping());
$this->_specialAttributes = array_merge($this->_specialAttributes, $model->getParticularAttributes());
- // phpcs:enable
+ // phpcs:enable
}
$this->_initErrorTemplates();
// remove doubles
@@ -1511,7 +1514,7 @@ public function getImagesFromRow(array $rowData)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
* @SuppressWarnings(PHPMD.UnusedLocalVariable)
* @throws LocalizedException
- * phpcs:disable Generic.Metrics.NestingLevel
+ * phpcs:disable Generic.Metrics.NestingLevel.TooHigh
*/
protected function _saveProducts()
{
@@ -1886,6 +1889,7 @@ protected function _saveProducts()
return $this;
}
+ //phpcs:enable Generic.Metrics.NestingLevel
/**
* Prepare array with image states (visible or hidden from product page)
@@ -2031,9 +2035,9 @@ protected function _saveProductTierPrices(array $tierPriceData)
protected function _getUploader()
{
if ($this->_fileUploader === null) {
- $this->_fileUploader = $this->_uploaderFactory->create();
+ $fileUploader = $this->_uploaderFactory->create();
- $this->_fileUploader->init();
+ $fileUploader->init();
$dirConfig = DirectoryList::getDefaultConfig();
$dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH];
@@ -2044,7 +2048,7 @@ protected function _getUploader()
$tmpPath = $dirAddon . '/' . $this->_mediaDirectory->getRelativePath('import');
}
- if (!$this->_fileUploader->setTmpDir($tmpPath)) {
+ if (!$fileUploader->setTmpDir($tmpPath)) {
throw new LocalizedException(
__('File directory \'%1\' is not readable.', $tmpPath)
);
@@ -2053,11 +2057,13 @@ protected function _getUploader()
$destinationPath = $dirAddon . '/' . $this->_mediaDirectory->getRelativePath($destinationDir);
$this->_mediaDirectory->create($destinationPath);
- if (!$this->_fileUploader->setDestDir($destinationPath)) {
+ if (!$fileUploader->setDestDir($destinationPath)) {
throw new LocalizedException(
__('File directory \'%1\' is not writable.', $destinationPath)
);
}
+
+ $this->_fileUploader = $fileUploader;
}
return $this->_fileUploader;
}
@@ -2736,8 +2742,6 @@ protected function _saveValidatedBunches()
try {
$rowData = $source->current();
} catch (\InvalidArgumentException $e) {
- $this->addRowError($e->getMessage(), $this->_processedRowsCount);
- $this->_processedRowsCount++;
$source->next();
continue;
}
@@ -3058,6 +3062,8 @@ private function getValidationErrorLevel($sku): string
* @param int $nextLinkId
* @param array $positionAttrId
* @return void
+ * @throws LocalizedException
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
private function processLinkBunches(
array $bunch,
@@ -3068,6 +3074,7 @@ private function processLinkBunches(
$productIds = [];
$linkRows = [];
$positionRows = [];
+ $linksToDelete = [];
$bunch = array_filter($bunch, [$this, 'isRowAllowedToImport'], ARRAY_FILTER_USE_BOTH);
foreach ($bunch as $rowData) {
@@ -3084,10 +3091,15 @@ function ($linkName) use ($rowData) {
);
foreach ($linkNameToId as $linkName => $linkId) {
$linkSkus = explode($this->getMultipleValueSeparator(), $rowData[$linkName . 'sku']);
+ //process empty value
+ if (!empty($linkSkus[0]) && $linkSkus[0] === $this->getEmptyAttributeValueConstant()) {
+ $linksToDelete[$linkId][] = $productId;
+ continue;
+ }
+
$linkPositions = !empty($rowData[$linkName . 'position'])
? explode($this->getMultipleValueSeparator(), $rowData[$linkName . 'position'])
: [];
-
$linkSkus = array_filter(
$linkSkus,
function ($linkedSku) use ($sku) {
@@ -3096,6 +3108,7 @@ function ($linkedSku) use ($sku) {
&& strcasecmp($linkedSku, $sku) !== 0;
}
);
+
foreach ($linkSkus as $linkedKey => $linkedSku) {
$linkedId = $this->getProductLinkedId($linkedSku);
if ($linkedId == null) {
@@ -3127,9 +3140,34 @@ function ($linkedSku) use ($sku) {
}
}
}
+ $this->deleteProductsLinks($resource, $linksToDelete);
$this->saveLinksData($resource, $productIds, $linkRows, $positionRows);
}
+ /**
+ * Delete links
+ *
+ * @param Link $resource
+ * @param array $linksToDelete
+ * @return void
+ * @throws LocalizedException
+ */
+ private function deleteProductsLinks(Link $resource, array $linksToDelete)
+ {
+ if (!empty($linksToDelete) && Import::BEHAVIOR_APPEND === $this->getBehavior()) {
+ foreach ($linksToDelete as $linkTypeId => $productIds) {
+ if (!empty($productIds)) {
+ $whereLinkId = $this->_connection->quoteInto('link_type_id', $linkTypeId);
+ $whereProductId = $this->_connection->quoteInto('product_id IN (?)', array_unique($productIds));
+ $this->_connection->delete(
+ $resource->getMainTable(),
+ $whereLinkId . ' AND ' . $whereProductId
+ );
+ }
+ }
+ }
+ }
+
/**
* Fetches Product Links
*
diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php
index 4d8088a235402..4c716421b7ae6 100644
--- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php
+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php
@@ -1695,7 +1695,7 @@ protected function _parseRequiredData(array $rowData)
$this->_rowIsMain = false;
}
- return [$this->_rowProductId, $this->_rowStoreId, $this->_rowType, $this->_rowIsMain];
+ return true;
}
/**
@@ -2086,7 +2086,9 @@ protected function _parseCustomOptions($rowData)
}
}
}
- $options[$name][$k]['_custom_option_store'] = $rowData[Product::COL_STORE_VIEW_CODE];
+ if (isset($rowData[Product::COL_STORE_VIEW_CODE])) {
+ $options[$name][$k][self::COLUMN_STORE] = $rowData[Product::COL_STORE_VIEW_CODE];
+ }
$k++;
}
$rowData['custom_options'] = $options;
diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php
index 3b6caef66ce6c..d87c3d8477556 100644
--- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php
+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php
@@ -13,6 +13,7 @@
/**
* Import entity abstract product type model
*
+ * phpcs:disable Magento2.Classes.AbstractApi
* @api
*
* @SuppressWarnings(PHPMD.TooManyFields)
@@ -543,7 +544,7 @@ public function prepareAttributesWithDefaultValueForSave(array $rowData, $withDe
} else {
$resultAttrs[$attrCode] = $rowData[$attrCode];
}
- } elseif (array_key_exists($attrCode, $rowData) && empty($rowData['_store'])) {
+ } elseif (array_key_exists($attrCode, $rowData)) {
$resultAttrs[$attrCode] = $rowData[$attrCode];
} elseif ($withDefaultValue && null !== $attrParams['default_value'] && empty($rowData['_store'])) {
$resultAttrs[$attrCode] = $attrParams['default_value'];
diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdate.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdate.php
new file mode 100644
index 0000000000000..99919628518c6
--- /dev/null
+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdate.php
@@ -0,0 +1,85 @@
+layoutValidatorFactory = $layoutValidatorFactory;
+ $this->validationState = $validationState;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function isValid($value): bool
+ {
+ if (!empty($value['custom_layout_update']) && !$this->validateXml($value['custom_layout_update'])) {
+ $this->_addMessages(
+ [
+ $this->context->retrieveMessageTemplate(self::ERROR_INVALID_LAYOUT_UPDATE)
+ ]
+ );
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Validate XML layout update
+ *
+ * @param string $xml
+ * @return bool
+ */
+ private function validateXml(string $xml): bool
+ {
+ /** @var $layoutXmlValidator \Magento\Framework\View\Model\Layout\Update\Validator */
+ $layoutXmlValidator = $this->layoutValidatorFactory->create(
+ [
+ 'validationState' => $this->validationState,
+ ]
+ );
+
+ try {
+ if (!$layoutXmlValidator->isValid($xml)) {
+ return false;
+ }
+ } catch (\Exception $e) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdatePermissions.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdatePermissions.php
new file mode 100644
index 0000000000000..50d38cedfb754
--- /dev/null
+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Validator/LayoutUpdatePermissions.php
@@ -0,0 +1,78 @@
+userContext = $userContext;
+ $this->authorization = $authorization;
+ }
+
+ /**
+ * Validate that the current user is allowed to make design updates
+ *
+ * @param array $data
+ * @return boolean
+ */
+ public function isValid($data): bool
+ {
+ if (empty($data['custom_layout_update'])) {
+ return true;
+ }
+
+ $userType = $this->userContext->getUserType();
+ $isValid = in_array($userType, $this->allowedUserTypes)
+ && $this->authorization->isAllowed('Magento_Catalog::edit_product_design');
+
+ if (!$isValid) {
+ $this->_addMessages(
+ [
+ $this->context->retrieveMessageTemplate(self::ERROR_INSUFFICIENT_PERMISSIONS),
+ ]
+ );
+ }
+
+ return $isValid;
+ }
+}
diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php
index 4ce1c0e39d6de..487ffaffa95e9 100644
--- a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php
+++ b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php
@@ -7,6 +7,8 @@
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Exception\ValidatorException;
+use Magento\Framework\Filesystem;
use Magento\Framework\Filesystem\DriverPool;
/**
@@ -34,13 +36,6 @@ class Uploader extends \Magento\MediaStorage\Model\File\Uploader
*/
protected $_tmpDir = '';
- /**
- * Download directory for url-based resources.
- *
- * @var string
- */
- private $downloadDir;
-
/**
* Destination directory.
*
@@ -111,13 +106,18 @@ class Uploader extends \Magento\MediaStorage\Model\File\Uploader
*/
private $random;
+ /**
+ * @var Filesystem
+ */
+ private $fileSystem;
+
/**
* @param \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDb
* @param \Magento\MediaStorage\Helper\File\Storage $coreFileStorage
* @param \Magento\Framework\Image\AdapterFactory $imageFactory
* @param \Magento\MediaStorage\Model\File\Validator\NotProtectedExtension $validator
- * @param \Magento\Framework\Filesystem $filesystem
- * @param \Magento\Framework\Filesystem\File\ReadFactory $readFactory
+ * @param Filesystem $filesystem
+ * @param Filesystem\File\ReadFactory $readFactory
* @param string|null $filePath
* @param \Magento\Framework\Math\Random|null $random
* @throws \Magento\Framework\Exception\FileSystemException
@@ -128,8 +128,8 @@ public function __construct(
\Magento\MediaStorage\Helper\File\Storage $coreFileStorage,
\Magento\Framework\Image\AdapterFactory $imageFactory,
\Magento\MediaStorage\Model\File\Validator\NotProtectedExtension $validator,
- \Magento\Framework\Filesystem $filesystem,
- \Magento\Framework\Filesystem\File\ReadFactory $readFactory,
+ Filesystem $filesystem,
+ Filesystem\File\ReadFactory $readFactory,
$filePath = null,
\Magento\Framework\Math\Random $random = null
) {
@@ -137,13 +137,13 @@ public function __construct(
$this->_coreFileStorageDb = $coreFileStorageDb;
$this->_coreFileStorage = $coreFileStorage;
$this->_validator = $validator;
+ $this->fileSystem = $filesystem;
$this->_directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT);
$this->_readFactory = $readFactory;
if ($filePath !== null) {
$this->_setUploadFile($filePath);
}
$this->random = $random ?: ObjectManager::getInstance()->get(\Magento\Framework\Math\Random::class);
- $this->downloadDir = DirectoryList::getDefaultConfig()[DirectoryList::TMP][DirectoryList::PATH];
}
/**
@@ -179,8 +179,7 @@ public function move($fileName, $renameFileOff = false)
$driver = ($matches[0] === $this->httpScheme) ? DriverPool::HTTP : DriverPool::HTTPS;
$tmpFilePath = $this->downloadFileFromUrl($url, $driver);
} else {
- $tmpDir = $this->getTmpDir() ? ($this->getTmpDir() . '/') : '';
- $tmpFilePath = $this->_directory->getRelativePath($tmpDir . $fileName);
+ $tmpFilePath = $this->_directory->getRelativePath($this->getTempFilePath($fileName));
}
$this->_setUploadFile($tmpFilePath);
@@ -217,8 +216,13 @@ private function downloadFileFromUrl($url, $driver)
$tmpFileName = str_replace(".$fileExtension", '', $fileName);
$tmpFileName .= '_' . $this->random->getRandomString(16);
$tmpFileName .= $fileExtension ? ".$fileExtension" : '';
- $tmpFilePath = $this->_directory->getRelativePath($this->downloadDir . '/' . $tmpFileName);
+ $tmpFilePath = $this->_directory->getRelativePath($this->getTempFilePath($tmpFileName));
+ if (!$this->_directory->isWritable($this->getTmpDir())) {
+ throw new \Magento\Framework\Exception\LocalizedException(
+ __('Import images directory must be writable in order to process remote images.')
+ );
+ }
$this->_directory->writeFile(
$tmpFilePath,
$this->_readFactory->create($url, $driver)->readAll()
@@ -236,7 +240,20 @@ private function downloadFileFromUrl($url, $driver)
*/
protected function _setUploadFile($filePath)
{
- if (!$this->_directory->isReadable($filePath)) {
+ try {
+ $fullPath = $this->_directory->getAbsolutePath($filePath);
+ if ($this->getTmpDir()) {
+ $tmpDir = $this->fileSystem->getDirectoryReadByPath(
+ $this->_directory->getAbsolutePath($this->getTmpDir())
+ );
+ } else {
+ $tmpDir = $this->_directory;
+ }
+ $readable = $tmpDir->isReadable($fullPath);
+ } catch (ValidatorException $exception) {
+ $readable = false;
+ }
+ if (!$readable) {
throw new \Magento\Framework\Exception\LocalizedException(
__('File \'%1\' was not found or has read restriction.', $filePath)
);
@@ -381,6 +398,19 @@ protected function _moveFile($tmpPath, $destPath)
}
}
+ /**
+ * Append temp path to filename
+ *
+ * @param string $filename
+ * @return string
+ */
+ private function getTempFilePath(string $filename): string
+ {
+ return $this->getTmpDir()
+ ? rtrim($this->getTmpDir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename
+ : $filename;
+ }
+
/**
* @inheritdoc
*/
diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml
index f792b0be2eb6b..c56bc667e2494 100644
--- a/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml
+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/ActionGroup/AdminExportActionGroup.xml
@@ -26,7 +26,7 @@
-
+
@@ -41,7 +41,7 @@
-
+
diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml
new file mode 100644
index 0000000000000..f0ec7dbd0706b
--- /dev/null
+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml
@@ -0,0 +1,222 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php
index bd2fe896b8c0a..371d75bc922f3 100644
--- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php
+++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php
@@ -72,7 +72,9 @@ protected function setUp()
'setAttributeSetFilter'
]
);
- $attribute = $this->createPartialMock(\Magento\Eav\Model\Entity\Attribute::class, [
+ $attribute = $this->createPartialMock(
+ \Magento\Eav\Model\Entity\Attribute::class,
+ [
'getAttributeCode',
'getId',
'getIsVisible',
@@ -85,7 +87,8 @@ protected function setUp()
'getDefaultValue',
'usesSource',
'getFrontendInput',
- ]);
+ ]
+ );
$attribute->expects($this->any())->method('getIsVisible')->willReturn(true);
$attribute->expects($this->any())->method('getIsGlobal')->willReturn(true);
$attribute->expects($this->any())->method('getIsRequired')->willReturn(true);
@@ -107,6 +110,7 @@ protected function setUp()
];
$attribute1 = clone $attribute;
$attribute2 = clone $attribute;
+ $attribute3 = clone $attribute;
$attribute1->expects($this->any())->method('getId')->willReturn('1');
$attribute1->expects($this->any())->method('getAttributeCode')->willReturn('attr_code');
@@ -118,6 +122,11 @@ protected function setUp()
$attribute2->expects($this->any())->method('getFrontendInput')->willReturn('boolean');
$attribute2->expects($this->any())->method('isStatic')->willReturn(false);
+ $attribute3->expects($this->any())->method('getId')->willReturn('3');
+ $attribute3->expects($this->any())->method('getAttributeCode')->willReturn('text_attribute');
+ $attribute3->expects($this->any())->method('getFrontendInput')->willReturn('text');
+ $attribute3->expects($this->any())->method('isStatic')->willReturn(false);
+
$this->entityModel->expects($this->any())->method('getEntityTypeId')->willReturn(3);
$this->entityModel->expects($this->any())->method('getAttributeOptions')->willReturnOnConsecutiveCalls(
['option1', 'option2'],
@@ -126,7 +135,9 @@ protected function setUp()
$attrSetColFactory->expects($this->any())->method('create')->willReturn($attrSetCollection);
$attrSetCollection->expects($this->any())->method('setEntityTypeFilter')->willReturn([$attributeSet]);
$attrColFactory->expects($this->any())->method('create')->willReturn($attrCollection);
- $attrCollection->expects($this->any())->method('setAttributeSetFilter')->willReturn([$attribute1, $attribute2]);
+ $attrCollection->expects($this->any())
+ ->method('setAttributeSetFilter')
+ ->willReturn([$attribute1, $attribute2, $attribute3]);
$attributeSet->expects($this->any())->method('getId')->willReturn(1);
$attributeSet->expects($this->any())->method('getAttributeSetName')->willReturn('attribute_set_name');
@@ -157,9 +168,11 @@ protected function setUp()
],
]
)
- ->willReturn([$attribute1, $attribute2]);
+ ->willReturn([$attribute1, $attribute2, $attribute3]);
- $this->connection = $this->createPartialMock(\Magento\Framework\DB\Adapter\Pdo\Mysql::class, [
+ $this->connection = $this->createPartialMock(
+ \Magento\Framework\DB\Adapter\Pdo\Mysql::class,
+ [
'select',
'fetchAll',
'fetchPairs',
@@ -167,13 +180,17 @@ protected function setUp()
'insertOnDuplicate',
'delete',
'quoteInto'
- ]);
- $this->select = $this->createPartialMock(\Magento\Framework\DB\Select::class, [
+ ]
+ );
+ $this->select = $this->createPartialMock(
+ \Magento\Framework\DB\Select::class,
+ [
'from',
'where',
'joinLeft',
'getConnection',
- ]);
+ ]
+ );
$this->select->expects($this->any())->method('from')->will($this->returnSelf());
$this->select->expects($this->any())->method('where')->will($this->returnSelf());
$this->select->expects($this->any())->method('joinLeft')->will($this->returnSelf());
@@ -189,10 +206,13 @@ protected function setUp()
->method('fetchAll')
->will($this->returnValue($entityAttributes));
- $this->resource = $this->createPartialMock(\Magento\Framework\App\ResourceConnection::class, [
+ $this->resource = $this->createPartialMock(
+ \Magento\Framework\App\ResourceConnection::class,
+ [
'getConnection',
'getTableName',
- ]);
+ ]
+ );
$this->resource->expects($this->any())->method('getConnection')->will(
$this->returnValue($this->connection)
);
@@ -257,9 +277,13 @@ public function testIsRowValidSuccess()
$rowNum = 1;
$this->entityModel->expects($this->any())->method('getRowScope')->willReturn(null);
$this->entityModel->expects($this->never())->method('addRowError');
- $this->setPropertyValue($this->simpleType, '_attributes', [
- $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => [],
- ]);
+ $this->setPropertyValue(
+ $this->simpleType,
+ '_attributes',
+ [
+ $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => [],
+ ]
+ );
$this->assertTrue($this->simpleType->isRowValid($rowData, $rowNum));
}
@@ -278,13 +302,17 @@ public function testIsRowValidError()
'attr_code'
)
->willReturnSelf();
- $this->setPropertyValue($this->simpleType, '_attributes', [
- $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => [
- 'attr_code' => [
- 'is_required' => true,
+ $this->setPropertyValue(
+ $this->simpleType,
+ '_attributes',
+ [
+ $rowData[\Magento\CatalogImportExport\Model\Import\Product::COL_ATTR_SET] => [
+ 'attr_code' => [
+ 'is_required' => true,
+ ],
],
- ],
- ]);
+ ]
+ );
$this->assertFalse($this->simpleType->isRowValid($rowData, $rowNum));
}
@@ -364,9 +392,14 @@ public function testPrepareAttributesWithDefaultValueForSave()
{
$rowData = [
'_attribute_set' => 'attributeSetName',
- 'boolean_attribute' => 'Yes'
+ 'boolean_attribute' => 'Yes',
+ ];
+
+ $expected = [
+ 'boolean_attribute' => 1,
+ 'text_attribute' => 'default_value'
];
$result = $this->simpleType->prepareAttributesWithDefaultValueForSave($rowData);
- $this->assertEquals(['boolean_attribute' => 1], $result);
+ $this->assertEquals($expected, $result);
}
}
diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php
index 9f63decac5ff7..f8b14a471fd9c 100644
--- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php
+++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/OptionTest.php
@@ -776,6 +776,77 @@ public function testValidateAmbiguousData(
$this->assertEquals($errors, $resultErrors);
}
+ /**
+ * Test for row without store view code field
+ * @param array $rowData
+ * @param array $responseData
+ *
+ * @covers \Magento\CatalogImportExport\Model\Import\Product\Option::_parseCustomOptions
+ * @dataProvider validateRowStoreViewCodeFieldDataProvider
+ */
+ public function testValidateRowDataForStoreViewCodeField($rowData, $responseData)
+ {
+ $reflection = new \ReflectionClass(\Magento\CatalogImportExport\Model\Import\Product\Option::class);
+ $reflectionMethod = $reflection->getMethod('_parseCustomOptions');
+ $reflectionMethod->setAccessible(true);
+ $result = $reflectionMethod->invoke($this->model, $rowData);
+ $this->assertEquals($responseData, $result);
+ }
+
+ /**
+ * Data provider for test of method _parseCustomOptions
+ *
+ * @return array
+ */
+ public function validateRowStoreViewCodeFieldDataProvider()
+ {
+ return [
+ 'with_store_view_code' => [
+ '$rowData' => [
+ 'store_view_code' => '',
+ 'custom_options' =>
+ 'name=Test Field Title,type=field,required=1;sku=1-text,price=0,price_type=fixed'
+ ],
+ '$responseData' => [
+ 'store_view_code' => '',
+ 'custom_options' => [
+ 'Test Field Title' => [
+ [
+ 'name' => 'Test Field Title',
+ 'type' => 'field',
+ 'required' => '1',
+ 'sku' => '1-text',
+ 'price' => '0',
+ 'price_type' => 'fixed',
+ '_custom_option_store' => ''
+ ]
+ ]
+ ]
+ ],
+ ],
+ 'without_store_view_code' => [
+ '$rowData' => [
+ 'custom_options' =>
+ 'name=Test Field Title,type=field,required=1;sku=1-text,price=0,price_type=fixed'
+ ],
+ '$responseData' => [
+ 'custom_options' => [
+ 'Test Field Title' => [
+ [
+ 'name' => 'Test Field Title',
+ 'type' => 'field',
+ 'required' => '1',
+ 'sku' => '1-text',
+ 'price' => '0',
+ 'price_type' => 'fixed'
+ ]
+ ]
+ ]
+ ],
+ ]
+ ];
+ }
+
/**
* Data provider of row data and errors
*
diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdatePermissionsTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdatePermissionsTest.php
new file mode 100644
index 0000000000000..e018fc0cf5ccf
--- /dev/null
+++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdatePermissionsTest.php
@@ -0,0 +1,101 @@
+userContext = $this->createMock(UserContextInterface::class);
+ $this->authorization = $this->createMock(AuthorizationInterface::class);
+ $this->context = $this->createMock(Product::class);
+ $this->context
+ ->method('retrieveMessageTemplate')
+ ->with('insufficientPermissions')
+ ->willReturn('oh no');
+ $this->validator = new LayoutUpdatePermissions(
+ $this->userContext,
+ $this->authorization
+ );
+ $this->validator->init($this->context);
+ }
+
+ /**
+ * @param $value
+ * @param $userContext
+ * @param $isAllowed
+ * @param $isValid
+ * @dataProvider configurationsProvider
+ */
+ public function testValidationConfiguration($value, $userContext, $isAllowed, $isValid)
+ {
+ $this->userContext
+ ->method('getUserType')
+ ->willReturn($userContext);
+
+ $this->authorization
+ ->method('isAllowed')
+ ->with('Magento_Catalog::edit_product_design')
+ ->willReturn($isAllowed);
+
+ $result = $this->validator->isValid(['custom_layout_update' => $value]);
+ $messages = $this->validator->getMessages();
+
+ self::assertSame($isValid, $result);
+
+ if ($isValid) {
+ self::assertSame([], $messages);
+ } else {
+ self::assertSame(['oh no'], $messages);
+ }
+ }
+
+ public function configurationsProvider()
+ {
+ return [
+ ['', null, null, true],
+ [null, null, null, true],
+ ['foo', UserContextInterface::USER_TYPE_ADMIN, true, true],
+ ['foo', UserContextInterface::USER_TYPE_INTEGRATION, true, true],
+ ['foo', UserContextInterface::USER_TYPE_ADMIN, false, false],
+ ['foo', UserContextInterface::USER_TYPE_INTEGRATION, false, false],
+ ['foo', 'something', null, false],
+ ];
+ }
+}
diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdateTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdateTest.php
new file mode 100644
index 0000000000000..d1e8b879f6a08
--- /dev/null
+++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Validator/LayoutUpdateTest.php
@@ -0,0 +1,97 @@
+createMock(ValidatorFactory::class);
+ $validationState = $this->createMock(ValidationStateInterface::class);
+ $this->layoutValidator = $this->createMock(Validator::class);
+ $validatorFactory->method('create')
+ ->with(['validationState' => $validationState])
+ ->willReturn($this->layoutValidator);
+
+ $this->validator = new LayoutUpdate(
+ $validatorFactory,
+ $validationState
+ );
+ }
+
+ public function testValidationIsSkippedWithDataNotPresent()
+ {
+ $this->layoutValidator
+ ->expects($this->never())
+ ->method('isValid');
+
+ $result = $this->validator->isValid([]);
+ self::assertTrue($result);
+ }
+
+ public function testValidationFailsProperly()
+ {
+ $this->layoutValidator
+ ->method('isValid')
+ ->with('foo')
+ ->willReturn(false);
+
+ $contextMock = $this->createMock(Product::class);
+ $contextMock
+ ->method('retrieveMessageTemplate')
+ ->with('invalidLayoutUpdate')
+ ->willReturn('oh no');
+ $this->validator->init($contextMock);
+
+ $result = $this->validator->isValid(['custom_layout_update' => 'foo']);
+ $messages = $this->validator->getMessages();
+ self::assertFalse($result);
+ self::assertSame(['oh no'], $messages);
+ }
+
+ public function testInvalidDataException()
+ {
+ $this->layoutValidator
+ ->method('isValid')
+ ->willThrowException(new \Exception('foo'));
+
+ $contextMock = $this->createMock(Product::class);
+ $contextMock
+ ->method('retrieveMessageTemplate')
+ ->with('invalidLayoutUpdate')
+ ->willReturn('oh no');
+ $this->validator->init($contextMock);
+
+ $result = $this->validator->isValid(['custom_layout_update' => 'foo']);
+ $messages = $this->validator->getMessages();
+ self::assertFalse($result);
+ self::assertSame(['oh no'], $messages);
+ }
+}
diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php
index f85d33edb5d8c..40041fe90db96 100644
--- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php
+++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php
@@ -284,9 +284,11 @@ protected function setUp()
->getMock();
$this->storeResolver =
$this->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Product\StoreResolver::class)
- ->setMethods([
- 'getStoreCodeToId',
- ])
+ ->setMethods(
+ [
+ 'getStoreCodeToId',
+ ]
+ )
->disableOriginalConstructor()
->getMock();
$this->skuProcessor =
@@ -410,7 +412,7 @@ protected function _objectConstructor()
$this->_filesystem->expects($this->once())
->method('getDirectoryWrite')
->with(DirectoryList::ROOT)
- ->will($this->returnValue(self::MEDIA_DIRECTORY));
+ ->willReturn($this->_mediaDirectory);
$this->validator->expects($this->any())->method('init');
return $this;
@@ -596,9 +598,13 @@ public function testGetMultipleValueSeparatorDefault()
public function testGetMultipleValueSeparatorFromParameters()
{
$expectedSeparator = 'value';
- $this->setPropertyValue($this->importProduct, '_parameters', [
- \Magento\ImportExport\Model\Import::FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR => $expectedSeparator,
- ]);
+ $this->setPropertyValue(
+ $this->importProduct,
+ '_parameters',
+ [
+ \Magento\ImportExport\Model\Import::FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR => $expectedSeparator,
+ ]
+ );
$this->assertEquals(
$expectedSeparator,
@@ -618,9 +624,13 @@ public function testGetEmptyAttributeValueConstantDefault()
public function testGetEmptyAttributeValueConstantFromParameters()
{
$expectedSeparator = '__EMPTY__VALUE__TEST__';
- $this->setPropertyValue($this->importProduct, '_parameters', [
- \Magento\ImportExport\Model\Import::FIELD_EMPTY_ATTRIBUTE_VALUE_CONSTANT => $expectedSeparator,
- ]);
+ $this->setPropertyValue(
+ $this->importProduct,
+ '_parameters',
+ [
+ \Magento\ImportExport\Model\Import::FIELD_EMPTY_ATTRIBUTE_VALUE_CONSTANT => $expectedSeparator,
+ ]
+ );
$this->assertEquals(
$expectedSeparator,
@@ -632,9 +642,12 @@ public function testDeleteProductsForReplacement()
{
$importProduct = $this->getMockBuilder(Product::class)
->disableOriginalConstructor()
- ->setMethods([
- 'setParameters', '_deleteProducts'
- ])
+ ->setMethods(
+ [
+ 'setParameters',
+ '_deleteProducts'
+ ]
+ )
->getMock();
$importProduct->expects($this->once())->method('setParameters')->with(
@@ -764,9 +777,13 @@ public function testGetProductWebsites()
'key 3' => 'val',
];
$expectedResult = array_keys($productValue);
- $this->setPropertyValue($this->importProduct, 'websitesCache', [
- $productSku => $productValue
- ]);
+ $this->setPropertyValue(
+ $this->importProduct,
+ 'websitesCache',
+ [
+ $productSku => $productValue
+ ]
+ );
$actualResult = $this->importProduct->getProductWebsites($productSku);
@@ -785,9 +802,13 @@ public function testGetProductCategories()
'key 3' => 'val',
];
$expectedResult = array_keys($productValue);
- $this->setPropertyValue($this->importProduct, 'categoriesCache', [
- $productSku => $productValue
- ]);
+ $this->setPropertyValue(
+ $this->importProduct,
+ 'categoriesCache',
+ [
+ $productSku => $productValue
+ ]
+ );
$actualResult = $this->importProduct->getProductCategories($productSku);
@@ -1112,9 +1133,13 @@ public function testValidateRowSetAttributeSetCodeIntoRowData()
->disableOriginalConstructor()
->getMock();
$productType->expects($this->once())->method('isRowValid')->with($expectedRowData);
- $this->setPropertyValue($importProduct, '_productTypeModels', [
- $newSku['type_id'] => $productType
- ]);
+ $this->setPropertyValue(
+ $importProduct,
+ '_productTypeModels',
+ [
+ $newSku['type_id'] => $productType
+ ]
+ );
//suppress option validation
$this->_rewriteGetOptionEntityInImportProduct($importProduct);
@@ -1229,6 +1254,56 @@ public function testParseAttributesWithWrappedValuesWillReturnsLowercasedAttribu
$this->assertArrayNotHasKey('PARAM2', $attributes);
}
+ /**
+ * @param bool $isRead
+ * @param bool $isWrite
+ * @param string $message
+ * @dataProvider fillUploaderObjectDataProvider
+ */
+ public function testFillUploaderObject($isRead, $isWrite, $message)
+ {
+ $fileUploaderMock = $this
+ ->getMockBuilder(\Magento\CatalogImportExport\Model\Import\Uploader::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $fileUploaderMock
+ ->method('setTmpDir')
+ ->with('pub/media/import')
+ ->willReturn($isRead);
+
+ $fileUploaderMock
+ ->method('setDestDir')
+ ->with('pub/media/catalog/product')
+ ->willReturn($isWrite);
+
+ $this->_mediaDirectory
+ ->method('getRelativePath')
+ ->willReturnMap(
+ [
+ ['import', 'import'],
+ ['catalog/product', 'catalog/product'],
+ ]
+ );
+
+ $this->_mediaDirectory
+ ->method('create')
+ ->with('pub/media/catalog/product');
+
+ $this->_uploaderFactory
+ ->expects($this->once())
+ ->method('create')
+ ->willReturn($fileUploaderMock);
+
+ try {
+ $this->importProduct->getUploader();
+ $this->assertNotNull($this->getPropertyValue($this->importProduct, '_fileUploader'));
+ } catch (\Magento\Framework\Exception\LocalizedException $e) {
+ $this->assertNull($this->getPropertyValue($this->importProduct, '_fileUploader'));
+ $this->assertEquals($message, $e->getMessage());
+ }
+ }
+
/**
* Test that errors occurred during importing images are logged.
*
@@ -1275,6 +1350,20 @@ function ($name) use ($throwException, $exception) {
);
}
+ /**
+ * Data provider for testFillUploaderObject.
+ *
+ * @return array
+ */
+ public function fillUploaderObjectDataProvider()
+ {
+ return [
+ [false, true, 'File directory \'pub/media/import\' is not readable.'],
+ [true, false, 'File directory \'pub/media/catalog/product\' is not writable.'],
+ [true, true, ''],
+ ];
+ }
+
/**
* Data provider for testUploadMediaFiles.
*
diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php
index 2c6aa6535c10e..f10cf0364c545 100644
--- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php
+++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php
@@ -128,6 +128,7 @@ public function testMoveFileUrl($fileUrl, $expectedHost, $expectedFileName, $che
{
$tmpDir = 'var/tmp';
$destDir = 'var/dest/dir';
+ $this->uploader->method('getTmpDir')->willReturn($tmpDir);
// Expected invocation to validate file extension
$this->uploader->expects($this->exactly($checkAllowedExtension))->method('checkAllowedExtension')
@@ -159,9 +160,11 @@ public function testMoveFileUrl($fileUrl, $expectedHost, $expectedFileName, $che
$this->directoryMock->expects($this->any())->method('writeFile')
->will($this->returnValue($expectedFileName));
- // Expected invocations to move the temp file to the destination directory
- $this->directoryMock->expects($this->once())->method('isWritable')
- ->with($destDir)
+ // Expected invocations save the downloaded file to temp file
+ // and move the temp file to the destination directory
+ $this->directoryMock->expects($this->exactly(2))
+ ->method('isWritable')
+ ->withConsecutive([$destDir], [$tmpDir])
->willReturn(true);
$this->directoryMock->expects($this->once())->method('getAbsolutePath')
->with($destDir)
@@ -172,9 +175,6 @@ public function testMoveFileUrl($fileUrl, $expectedHost, $expectedFileName, $che
->with($destDir . '/' . $expectedFileName)
->willReturn(['name' => $expectedFileName, 'path' => 'absPath']);
- // Do not use configured temp directory
- $this->uploader->expects($this->never())->method('getTmpDir');
-
$this->uploader->setDestDir($destDir);
$result = $this->uploader->move($fileUrl);
diff --git a/app/code/Magento/CatalogImportExport/composer.json b/app/code/Magento/CatalogImportExport/composer.json
index 6af2bbaf45e3c..25d9c1bde0d68 100644
--- a/app/code/Magento/CatalogImportExport/composer.json
+++ b/app/code/Magento/CatalogImportExport/composer.json
@@ -16,7 +16,8 @@
"magento/module-import-export": "*",
"magento/module-media-storage": "*",
"magento/module-store": "*",
- "magento/module-tax": "*"
+ "magento/module-tax": "*",
+ "magento/module-authorization": "*"
},
"type": "magento2-module",
"license": [
diff --git a/app/code/Magento/CatalogImportExport/etc/di.xml b/app/code/Magento/CatalogImportExport/etc/di.xml
index 6906272b11d68..4e2fe390e0b17 100644
--- a/app/code/Magento/CatalogImportExport/etc/di.xml
+++ b/app/code/Magento/CatalogImportExport/etc/di.xml
@@ -25,7 +25,14 @@
- Magento\CatalogImportExport\Model\Import\Product\Validator\Website
- Magento\CatalogImportExport\Model\Import\Product\Validator\Weight
- Magento\CatalogImportExport\Model\Import\Product\Validator\Quantity
+
- Magento\CatalogImportExport\Model\Import\Product\Validator\LayoutUpdate
+
- Magento\CatalogImportExport\Model\Import\Product\Validator\LayoutUpdatePermissions
+
+
+ Magento\Framework\Config\ValidationState\Required
+
+
diff --git a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php
index 6c4f6a0f46a59..ffcb758dcbd66 100644
--- a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php
+++ b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php
@@ -37,7 +37,7 @@ protected function _getGroupRenderer()
'',
['data' => ['is_render_to_js_template' => true]]
);
- $this->_groupRenderer->setClass('customer_group_select');
+ $this->_groupRenderer->setClass('customer_group_select admin__control-select');
}
return $this->_groupRenderer;
}
@@ -57,7 +57,7 @@ protected function _prepareToRender()
'min_sale_qty',
[
'label' => __('Minimum Qty'),
- 'class' => 'required-entry validate-number validate-greater-than-zero'
+ 'class' => 'required-entry validate-number validate-greater-than-zero admin__control-text'
]
);
$this->_addAfter = false;
diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php b/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php
index f9a49d4f8d121..35231b8460b19 100644
--- a/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php
+++ b/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php
@@ -104,6 +104,10 @@ public function modifyPrice(IndexTableStructure $priceTable, array $entityIds =
$select->where('stock_item.use_config_manage_stock = 0 AND stock_item.manage_stock = 1');
}
+ if (!empty($entityIds)) {
+ $select->where('stock_item.product_id in (?)', $entityIds);
+ }
+
$select->group('stock_item.product_id');
$select->having('max_is_in_stock = 0');
diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php
index 3670b93b8cb48..c5644060c689f 100644
--- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php
+++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php
@@ -230,6 +230,8 @@ protected function _getStockStatusSelect($entityIds = null, $usePrimaryTable = f
{
$connection = $this->getConnection();
$qtyExpr = $connection->getCheckSql('cisi.qty > 0', 'cisi.qty', 0);
+ $metadata = $this->getMetadataPool()->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class);
+ $linkField = $metadata->getLinkField();
$select = $connection->select()->from(
['e' => $this->getTable('catalog_product_entity')],
@@ -243,6 +245,12 @@ protected function _getStockStatusSelect($entityIds = null, $usePrimaryTable = f
['cisi' => $this->getTable('cataloginventory_stock_item')],
'cisi.stock_id = cis.stock_id AND cisi.product_id = e.entity_id',
[]
+ )->joinInner(
+ ['mcpei' => $this->getTable('catalog_product_entity_int')],
+ 'e.' . $linkField . ' = mcpei.' . $linkField
+ . ' AND mcpei.attribute_id = ' . $this->_getAttribute('status')->getId()
+ . ' AND mcpei.value = ' . ProductStatus::STATUS_ENABLED,
+ []
)->columns(
['qty' => $qtyExpr]
)->where(
diff --git a/app/code/Magento/CatalogInventory/Setup/Patch/Schema/ChangeTmpTablesEngine.php b/app/code/Magento/CatalogInventory/Setup/Patch/Schema/ChangeTmpTablesEngine.php
deleted file mode 100644
index 7f43cd279d4e3..0000000000000
--- a/app/code/Magento/CatalogInventory/Setup/Patch/Schema/ChangeTmpTablesEngine.php
+++ /dev/null
@@ -1,61 +0,0 @@
-schemaSetup = $schemaSetup;
- }
-
- /**
- * @inheritdoc
- */
- public function apply()
- {
- $this->schemaSetup->startSetup();
-
- $tableName = $this->schemaSetup->getTable('cataloginventory_stock_status_tmp');
- if ($this->schemaSetup->getConnection()->isTableExists($tableName)) {
- $this->schemaSetup->getConnection()->changeTableEngine($tableName, 'InnoDB');
- }
-
- $this->schemaSetup->endSetup();
- }
-
- /**
- * @inheritdoc
- */
- public static function getDependencies()
- {
- return [];
- }
-
- /**
- * @inheritdoc
- */
- public function getAliases()
- {
- return [];
- }
-}
diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminCatalogInventoryConfigurationActionGroup.xml b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminCatalogInventoryConfigurationActionGroup.xml
new file mode 100644
index 0000000000000..49956473132ec
--- /dev/null
+++ b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminCatalogInventoryConfigurationActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminProductActionGroup.xml b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminProductActionGroup.xml
new file mode 100644
index 0000000000000..84dc6b93c885f
--- /dev/null
+++ b/app/code/Magento/CatalogInventory/Test/Mftf/ActionGroup/AdminProductActionGroup.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryConfigData.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryConfigData.xml
index e14c36446fc2b..cd5a8cf5bbac9 100644
--- a/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryConfigData.xml
+++ b/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryConfigData.xml
@@ -20,4 +20,17 @@
No
0
+
+
+ cataloginventory/options/can_subtract
+ 0
+ Yes
+ 1
+
+
+ cataloginventory/options/can_subtract
+ 0
+ No
+ 0
+
diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryItemOptionsData.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryItemOptionsData.xml
new file mode 100644
index 0000000000000..767d65f9facca
--- /dev/null
+++ b/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventoryItemOptionsData.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+ MaxSaleQtyDefaultValue
+
+
+ 10000
+
+
diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Metadata/cataloginventory_item_options-meta.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Metadata/cataloginventory_item_options-meta.xml
new file mode 100644
index 0000000000000..7672cb7478f1a
--- /dev/null
+++ b/app/code/Magento/CatalogInventory/Test/Mftf/Metadata/cataloginventory_item_options-meta.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+ integer
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Page/AdminInventoryProductStockOptionsConfigPage.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Page/AdminInventoryProductStockOptionsConfigPage.xml
new file mode 100644
index 0000000000000..3d8c3ef3cf9f8
--- /dev/null
+++ b/app/code/Magento/CatalogInventory/Test/Mftf/Page/AdminInventoryProductStockOptionsConfigPage.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Page/AdminProductCreatePage.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Page/AdminProductCreatePage.xml
new file mode 100644
index 0000000000000..5835e7564c172
--- /dev/null
+++ b/app/code/Magento/CatalogInventory/Test/Mftf/Page/AdminProductCreatePage.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminInventoryProductStockOptionsConfigSection.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminInventoryProductStockOptionsConfigSection.xml
new file mode 100644
index 0000000000000..ef7fe30f4970b
--- /dev/null
+++ b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminInventoryProductStockOptionsConfigSection.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml
similarity index 92%
rename from app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml
rename to app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml
index 4196a86fe25db..7ff9c2d70755f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml
+++ b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml
@@ -30,5 +30,7 @@
+
+
diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormSection.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormSection.xml
new file mode 100644
index 0000000000000..945613ee753d6
--- /dev/null
+++ b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormSection.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest.xml
new file mode 100644
index 0000000000000..f7cf0a4deba4b
--- /dev/null
+++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/ProductPriceIndexFilterTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/ProductPriceIndexFilterTest.php
new file mode 100644
index 0000000000000..46f4e0f26f378
--- /dev/null
+++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/ProductPriceIndexFilterTest.php
@@ -0,0 +1,121 @@
+stockConfiguration = $this->createMock(StockConfigurationInterface::class);
+ $this->item = $this->createMock(Item::class);
+ $this->resourceCnnection = $this->createMock(ResourceConnection::class);
+ $this->generator = $this->createMock(Generator::class);
+
+ $this->productPriceIndexFilter = new ProductPriceIndexFilter(
+ $this->stockConfiguration,
+ $this->item,
+ $this->resourceCnnection,
+ 'indexer',
+ $this->generator,
+ 100
+ );
+ }
+
+ /**
+ * Test to ensure that Modify Price method uses entityIds,
+ */
+ public function testModifyPrice()
+ {
+ $entityIds = [1, 2, 3];
+ $indexTableStructure = $this->createMock(IndexTableStructure::class);
+ $connectionMock = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class);
+ $this->resourceCnnection->expects($this->once())->method('getConnection')->willReturn($connectionMock);
+ $selectMock = $this->createMock(\Magento\Framework\DB\Select::class);
+ $connectionMock->expects($this->once())->method('select')->willReturn($selectMock);
+ $selectMock->expects($this->at(2))
+ ->method('where')
+ ->with('stock_item.product_id in (?)', $entityIds)
+ ->willReturn($selectMock);
+ $this->generator->expects($this->once())
+ ->method('generate')
+ ->will(
+ $this->returnCallback(
+ $this->getBatchIteratorCallback($selectMock, 5)
+ )
+ );
+
+ $fetchStmtMock = $this->createPartialMock(\Zend_Db_Statement_Pdo::class, ['fetchAll']);
+ $fetchStmtMock->expects($this->any())
+ ->method('fetchAll')
+ ->will($this->returnValue([['product_id' => 1]]));
+ $connectionMock->expects($this->any())->method('query')->will($this->returnValue($fetchStmtMock));
+ $this->productPriceIndexFilter->modifyPrice($indexTableStructure, $entityIds);
+ }
+
+ /**
+ * Returns batches.
+ *
+ * @param MockObject $selectMock
+ * @param int $batchCount
+ * @return \Closure
+ */
+ private function getBatchIteratorCallback(MockObject $selectMock, int $batchCount): \Closure
+ {
+ $iteratorCallback = function () use ($batchCount, $selectMock): array {
+ $result = [];
+ $count = $batchCount;
+ while ($count) {
+ $count--;
+ $result[$count] = $selectMock;
+ }
+
+ return $result;
+ };
+
+ return $iteratorCallback;
+ }
+}
diff --git a/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php
index da465f5bdd3dc..aafde14a28584 100644
--- a/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php
+++ b/app/code/Magento/CatalogInventory/Ui/DataProvider/Product/Form/Modifier/AdvancedInventory.php
@@ -1,7 +1,4 @@
arrayManager->findPath($fieldCode, $this->meta, null, 'children');
if ($pathField) {
- $labelField = $this->arrayManager->get(
- $this->arrayManager->slicePath($pathField, 0, -2) . '/arguments/data/config/label',
- $this->meta
- );
$fieldsetPath = $this->arrayManager->slicePath($pathField, 0, -4);
$this->meta = $this->arrayManager->merge(
@@ -219,10 +212,9 @@ private function prepareMeta()
'formElement' => 'container',
'componentType' => 'container',
'component' => "Magento_Ui/js/form/components/group",
- 'label' => $labelField,
+ 'label' => false,
'breakLine' => false,
'dataScope' => $fieldCode,
- 'scopeLabel' => '[GLOBAL]',
'source' => 'product_details',
'sortOrder' => (int) $this->arrayManager->get(
$this->arrayManager->slicePath($pathField, 0, -2) . '/arguments/data/config/sortOrder',
@@ -230,86 +222,58 @@ private function prepareMeta()
) - 1,
'disabled' => $this->locator->getProduct()->isLockedAttribute($fieldCode),
];
+ $qty['arguments']['data']['config'] = [
+ 'component' => 'Magento_CatalogInventory/js/components/qty-validator-changer',
+ 'group' => 'quantity_and_stock_status_qty',
+ 'dataType' => 'number',
+ 'formElement' => 'input',
+ 'componentType' => 'field',
+ 'visible' => '1',
+ 'require' => '0',
+ 'additionalClasses' => 'admin__field-small',
+ 'label' => __('Quantity'),
+ 'scopeLabel' => '[GLOBAL]',
+ 'dataScope' => 'qty',
+ 'validation' => [
+ 'validate-number' => true,
+ 'less-than-equals-to' => StockDataFilter::MAX_QTY_VALUE,
+ ],
+ 'imports' => [
+ 'handleChanges' => '${$.provider}:data.product.stock_data.is_qty_decimal',
+ ],
+ 'sortOrder' => 10,
+ ];
+ $advancedInventoryButton['arguments']['data']['config'] = [
+ 'displayAsLink' => true,
+ 'formElement' => 'container',
+ 'componentType' => 'container',
+ 'component' => 'Magento_Ui/js/form/components/button',
+ 'template' => 'ui/form/components/button/container',
+ 'actions' => [
+ [
+ 'targetName' => 'product_form.product_form.advanced_inventory_modal',
+ 'actionName' => 'toggleModal',
+ ],
+ ],
+ 'imports' => [
+ 'childError' => 'product_form.product_form.advanced_inventory_modal.stock_data:error',
+ ],
+ 'title' => __('Advanced Inventory'),
+ 'provider' => false,
+ 'additionalForGroup' => true,
+ 'source' => 'product_details',
+ 'sortOrder' => 20,
+ ];
$container['children'] = [
- 'qty' => $this->getQtyMetaStructure(),
- 'advanced_inventory_button' => $this->getAdvancedInventoryButtonMetaStructure(),
+ 'qty' => $qty,
+ 'advanced_inventory_button' => $advancedInventoryButton,
];
$this->meta = $this->arrayManager->merge(
$fieldsetPath . '/children',
$this->meta,
- ['container_quantity_and_stock_status_qty' => $container]
+ ['quantity_and_stock_status_qty' => $container]
);
}
}
-
- /**
- * Get Qty meta structure
- *
- * @return array
- */
- private function getQtyMetaStructure()
- {
- return [
- 'arguments' => [
- 'data' => [
- 'config' => [
- 'component' => 'Magento_CatalogInventory/js/components/qty-validator-changer',
- 'group' => 'quantity_and_stock_status_qty',
- 'dataType' => 'number',
- 'formElement' => 'input',
- 'componentType' => 'field',
- 'visible' => '1',
- 'require' => '0',
- 'additionalClasses' => 'admin__field-small',
- 'label' => __('Quantity'),
- 'scopeLabel' => '[GLOBAL]',
- 'dataScope' => 'qty',
- 'validation' => [
- 'validate-number' => true,
- 'less-than-equals-to' => StockDataFilter::MAX_QTY_VALUE,
- ],
- 'imports' => [
- 'handleChanges' => '${$.provider}:data.product.stock_data.is_qty_decimal',
- ],
- 'sortOrder' => 10,
- 'disabled' => $this->locator->getProduct()->isLockedAttribute('quantity_and_stock_status'),
- ]
- ]
- ]
- ];
- }
-
- /**
- * Get advances inventory button meta structure
- *
- * @return array
- */
- private function getAdvancedInventoryButtonMetaStructure()
- {
- return [
- 'arguments' => [
- 'data' => [
- 'config' => [
- 'displayAsLink' => true,
- 'formElement' => 'container',
- 'componentType' => 'container',
- 'component' => 'Magento_Ui/js/form/components/button',
- 'template' => 'ui/form/components/button/container',
- 'actions' => [
- [
- 'targetName' => 'product_form.product_form.advanced_inventory_modal',
- 'actionName' => 'toggleModal',
- ],
- ],
- 'title' => __('Advanced Inventory'),
- 'provider' => false,
- 'additionalForGroup' => true,
- 'source' => 'product_details',
- 'sortOrder' => 20,
- ]
- ]
- ]
- ];
- }
}
diff --git a/app/code/Magento/CatalogInventory/etc/adminhtml/system.xml b/app/code/Magento/CatalogInventory/etc/adminhtml/system.xml
index 08ed0a8f49470..546f838b9b428 100644
--- a/app/code/Magento/CatalogInventory/etc/adminhtml/system.xml
+++ b/app/code/Magento/CatalogInventory/etc/adminhtml/system.xml
@@ -55,7 +55,7 @@
Maximum Qty Allowed in Shopping Cart
- validate-number
+ validate-number validate-greater-than-zero
Out-of-Stock Threshold
diff --git a/app/code/Magento/CatalogInventory/etc/db_schema.xml b/app/code/Magento/CatalogInventory/etc/db_schema.xml
index 5ac7fedc5aa18..b5c4a96f24a94 100644
--- a/app/code/Magento/CatalogInventory/etc/db_schema.xml
+++ b/app/code/Magento/CatalogInventory/etc/db_schema.xml
@@ -9,9 +9,9 @@
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
+ comment="Stock ID"/>
+ comment="Website ID"/>
@@ -22,11 +22,11 @@
+ comment="Item ID"/>
+ default="0" comment="Product ID"/>
+ default="0" comment="Stock ID"/>
@@ -94,11 +94,11 @@
+ comment="Product ID"/>
+ comment="Website ID"/>
+ comment="Stock ID"/>
+ comment="Product ID"/>
+ comment="Website ID"/>
+ comment="Stock ID"/>
-
+ comment="Product ID"/>
+ comment="Website ID"/>
+ comment="Stock ID"/>
-
+
-
+
+ comment="Product ID"/>
+ comment="Website ID"/>
+ comment="Stock ID"/>
-
- container
- - Manage Stock
- stock_data
- - [GLOBAL]
@@ -74,12 +72,8 @@
${$.provider}:data.product.stock_data.manage_stock
- ${$.parentName}.manage_stock:disabled
${$.parentName}.manage_stock:disabled
-
- ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled
-
@@ -105,7 +99,6 @@
quantity_and_stock_status.qty
ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:value
- ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled
${$.provider}:data.product.stock_data.is_qty_decimal
@@ -117,9 +110,7 @@
-
- container
- - Out-of-Stock Threshold
- stock_data
- - [GLOBAL]
-
- ${$.provider}:data.product.stock_data.manage_stock
@@ -154,12 +145,8 @@
${$.provider}:data.product.stock_data.min_qty
- ${$.parentName}.min_qty:disabled
${$.parentName}.min_qty:disabled
-
- ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled
-
@@ -220,13 +207,6 @@
true
use_config_min_sale_qty
-
- ${$.parentName}.min_sale_qty:disabled
- ${$.parentName}.min_sale_qty:disabled
-
-
- ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled
-
@@ -290,9 +270,7 @@
-
- container
- - Maximum Qty Allowed in Shopping Cart
- stock_data
- - [GLOBAL]
@@ -304,6 +282,7 @@
[GLOBAL]
+ true
true
Maximum Qty Allowed in Shopping Cart
@@ -324,12 +303,8 @@
${$.provider}:data.product.stock_data.max_sale_qty
- ${$.parentName}.max_sale_qty:disabled
${$.parentName}.max_sale_qty:disabled
-
- ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled
-
@@ -357,7 +332,6 @@
stock_data.is_qty_decimal
${$.provider}:data.product.stock_data.manage_stock
- ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled
@@ -380,7 +354,6 @@
stock_data.is_decimal_divided
${$.provider}:data.product.stock_data.manage_stock
- ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled
@@ -395,9 +368,7 @@
-
- container
- - Backorders
- stock_data
- - [GLOBAL]
-
- ${$.provider}:data.product.stock_data.manage_stock
@@ -440,12 +411,8 @@
${$.provider}:data.product.stock_data.backorders
- ${$.parentName}.backorders:disabled
${$.parentName}.backorders:disabled
-
- ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled
-
@@ -464,9 +431,7 @@
-
- container
- - Notify for Quantity Below
- stock_data
- - [GLOBAL]
-
- ${$.provider}:data.product.stock_data.manage_stock
@@ -501,12 +466,8 @@
${$.provider}:data.product.stock_data.notify_stock_qty
- ${$.parentName}.notify_stock_qty:disabled
${$.parentName}.notify_stock_qty:disabled
-
- ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled
-
@@ -525,9 +486,7 @@
-
- container
- - Enable Qty Increments
- stock_data
- - [GLOBAL]
@@ -564,12 +523,8 @@
${$.provider}:data.product.stock_data.enable_qty_increments
- ${$.parentName}.enable_qty_increments:disabled
${$.parentName}.enable_qty_increments:disabled
-
- ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled
-
@@ -588,9 +543,7 @@
-
- container
- - Qty Increments
- stock_data
- - [GLOBAL]
-
- ${$.provider}:data.product.stock_data.enable_qty_increments
@@ -629,12 +582,8 @@
${$.provider}:data.product.stock_data.qty_increments
- ${$.parentName}.qty_increments:disabled
${$.parentName}.qty_increments:disabled
-
- ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled
-
@@ -653,9 +602,7 @@
-
- container
- - Stock Status
- quantity_and_stock_status
- - [GLOBAL]
-
- ${$.provider}:data.product.stock_data.manage_stock
@@ -671,9 +618,6 @@
[GLOBAL]
Stock Status
is_in_stock
-
- ns = ${ $.ns }, index = qty, group = quantity_and_stock_status_qty:disabled
-
diff --git a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php
index 4f58293d53359..6d499b93e411f 100644
--- a/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php
+++ b/app/code/Magento/CatalogRule/Controller/Adminhtml/Promo/Catalog/Save.php
@@ -12,6 +12,7 @@
use Magento\Framework\Registry;
use Magento\Framework\Stdlib\DateTime\Filter\Date;
use Magento\Framework\App\Request\DataPersistorInterface;
+use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
/**
* Save action for catalog rule
@@ -25,19 +26,27 @@ class Save extends \Magento\CatalogRule\Controller\Adminhtml\Promo\Catalog imple
*/
protected $dataPersistor;
+ /**
+ * @var TimezoneInterface
+ */
+ private $localeDate;
+
/**
* @param Context $context
* @param Registry $coreRegistry
* @param Date $dateFilter
* @param DataPersistorInterface $dataPersistor
+ * @param TimezoneInterface $localeDate
*/
public function __construct(
Context $context,
Registry $coreRegistry,
Date $dateFilter,
- DataPersistorInterface $dataPersistor
+ DataPersistorInterface $dataPersistor,
+ TimezoneInterface $localeDate
) {
$this->dataPersistor = $dataPersistor;
+ $this->localeDate = $localeDate;
parent::__construct($context, $coreRegistry, $dateFilter);
}
@@ -46,16 +55,15 @@ public function __construct(
*
* @return \Magento\Framework\App\ResponseInterface|\Magento\Framework\Controller\ResultInterface|void
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.NPathComplexity)
*/
public function execute()
{
if ($this->getRequest()->getPostValue()) {
-
/** @var \Magento\CatalogRule\Api\CatalogRuleRepositoryInterface $ruleRepository */
$ruleRepository = $this->_objectManager->get(
\Magento\CatalogRule\Api\CatalogRuleRepositoryInterface::class
);
-
/** @var \Magento\CatalogRule\Model\Rule $model */
$model = $this->_objectManager->create(\Magento\CatalogRule\Model\Rule::class);
@@ -65,7 +73,9 @@ public function execute()
['request' => $this->getRequest()]
);
$data = $this->getRequest()->getPostValue();
-
+ if (!$this->getRequest()->getParam('from_date')) {
+ $data['from_date'] = $this->localeDate->formatDate();
+ }
$filterValues = ['from_date' => $this->_dateFilter];
if ($this->getRequest()->getParam('to_date')) {
$filterValues['to_date'] = $this->_dateFilter;
diff --git a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php
index e12eabba76401..1fc53c78985fb 100644
--- a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php
+++ b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php
@@ -242,7 +242,22 @@ public function __construct(
*/
public function reindexById($id)
{
- $this->reindexByIds([$id]);
+ try {
+ $this->cleanProductIndex([$id]);
+
+ $products = $this->productLoader->getProducts([$id]);
+ $activeRules = $this->getActiveRules();
+ foreach ($products as $product) {
+ $this->applyRules($activeRules, $product);
+ }
+
+ $this->reindexRuleGroupWebsite->execute();
+ } catch (\Exception $e) {
+ $this->critical($e);
+ throw new \Magento\Framework\Exception\LocalizedException(
+ __('Catalog rule indexing failed. See details in exception log.')
+ );
+ }
}
/**
@@ -275,11 +290,18 @@ protected function doReindexByIds($ids)
{
$this->cleanProductIndex($ids);
- $products = $this->productLoader->getProducts($ids);
- $activeRules = $this->getActiveRules();
- foreach ($products as $product) {
- $this->applyRules($activeRules, $product);
+ /** @var Rule[] $activeRules */
+ $activeRules = $this->getActiveRules()->getItems();
+ foreach ($activeRules as $rule) {
+ $rule->setProductsFilter($ids);
+ $this->reindexRuleProduct->execute($rule, $this->batchCount);
}
+
+ foreach ($ids as $productId) {
+ $this->cleanProductPriceIndex([$productId]);
+ $this->reindexRuleProductPrice->execute($this->batchCount, $productId);
+ }
+
$this->reindexRuleGroupWebsite->execute();
}
@@ -365,17 +387,13 @@ protected function cleanByIds($productIds)
* Assign product to rule
*
* @param Rule $rule
- * @param Product $product
+ * @param int $productEntityId
+ * @param array $websiteIds
* @return void
*/
- private function assignProductToRule(Rule $rule, Product $product): void
+ private function assignProductToRule(Rule $rule, int $productEntityId, array $websiteIds): void
{
- if (!$rule->validate($product)) {
- return;
- }
-
$ruleId = (int) $rule->getId();
- $productEntityId = (int) $product->getId();
$ruleProductTable = $this->getTable('catalogrule_product');
$this->connection->delete(
$ruleProductTable,
@@ -385,7 +403,6 @@ private function assignProductToRule(Rule $rule, Product $product): void
]
);
- $websiteIds = array_intersect($product->getWebsiteIds(), $rule->getWebsiteIds());
$customerGroupIds = $rule->getCustomerGroupIds();
$fromTime = strtotime($rule->getFromDate());
$toTime = strtotime($rule->getToDate());
@@ -429,12 +446,17 @@ private function assignProductToRule(Rule $rule, Product $product): void
* @param Product $product
* @return $this
* @throws \Exception
+ * @deprecated
+ * @see ReindexRuleProduct::execute
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
protected function applyRule(Rule $rule, $product)
{
- $this->assignProductToRule($rule, $product);
- $this->reindexRuleProductPrice->execute($this->batchCount, $product);
+ if ($rule->validate($product)) {
+ $websiteIds = array_intersect($product->getWebsiteIds(), $rule->getWebsiteIds());
+ $this->assignProductToRule($rule, $product->getId(), $websiteIds);
+ }
+ $this->reindexRuleProductPrice->execute($this->batchCount, $product->getId());
$this->reindexRuleGroupWebsite->execute();
return $this;
@@ -450,11 +472,16 @@ protected function applyRule(Rule $rule, $product)
private function applyRules(RuleCollection $ruleCollection, Product $product): void
{
foreach ($ruleCollection as $rule) {
- $this->assignProductToRule($rule, $product);
+ if (!$rule->validate($product)) {
+ continue;
+ }
+
+ $websiteIds = array_intersect($product->getWebsiteIds(), $rule->getWebsiteIds());
+ $this->assignProductToRule($rule, $product->getId(), $websiteIds);
}
$this->cleanProductPriceIndex([$product->getId()]);
- $this->reindexRuleProductPrice->execute($this->batchCount, $product);
+ $this->reindexRuleProductPrice->execute($this->batchCount, $product->getId());
}
/**
@@ -507,7 +534,7 @@ protected function updateRuleProductData(Rule $rule)
*/
protected function applyAllRules(Product $product = null)
{
- $this->reindexRuleProductPrice->execute($this->batchCount, $product);
+ $this->reindexRuleProductPrice->execute($this->batchCount, $product->getId());
$this->reindexRuleGroupWebsite->execute();
return $this;
}
@@ -562,7 +589,7 @@ protected function calcRuleProductPrice($ruleData, $productData = null)
*/
protected function getRuleProductsStmt($websiteId, Product $product = null)
{
- return $this->ruleProductsSelectBuilder->build($websiteId, $product);
+ return $this->ruleProductsSelectBuilder->build((int) $websiteId, (int) $product->getId());
}
/**
diff --git a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php
index e589c8595ce2c..944710773123f 100644
--- a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php
+++ b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProduct.php
@@ -101,7 +101,9 @@ public function execute(Rule $rule, $batchCount, $useAdditionalTable = false)
$scopeTz = new \DateTimeZone(
$this->localeDate->getConfigTimezone(ScopeInterface::SCOPE_WEBSITE, $websiteId)
);
- $fromTime = (new \DateTime($rule->getFromDate(), $scopeTz))->getTimestamp();
+ $fromTime = $rule->getFromDate()
+ ? (new \DateTime($rule->getFromDate(), $scopeTz))->getTimestamp()
+ : 0;
$toTime = $rule->getToDate()
? (new \DateTime($rule->getToDate(), $scopeTz))->getTimestamp() + IndexBuilder::SECONDS_IN_DAY - 1
: 0;
diff --git a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php
index 11ba87730bec1..51869f1accbb3 100644
--- a/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php
+++ b/app/code/Magento/CatalogRule/Model/Indexer/ReindexRuleProductPrice.php
@@ -6,7 +6,6 @@
namespace Magento\CatalogRule\Model\Indexer;
-use Magento\Catalog\Model\Product;
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
use Magento\Store\Model\StoreManagerInterface;
@@ -65,19 +64,19 @@ public function __construct(
* Reindex product prices.
*
* @param int $batchCount
- * @param Product|null $product
+ * @param int|null $productId
* @param bool $useAdditionalTable
* @return bool
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
- public function execute($batchCount, Product $product = null, $useAdditionalTable = false)
+ public function execute(int $batchCount, ?int $productId = null, bool $useAdditionalTable = false)
{
/**
* Update products rules prices per each website separately
* because for each website date in website's timezone should be used
*/
foreach ($this->storeManager->getWebsites() as $website) {
- $productsStmt = $this->ruleProductsSelectBuilder->build($website->getId(), $product, $useAdditionalTable);
+ $productsStmt = $this->ruleProductsSelectBuilder->build($website->getId(), $productId, $useAdditionalTable);
$dayPrices = [];
$stopFlags = [];
$prevKey = null;
diff --git a/app/code/Magento/CatalogRule/Model/Indexer/RuleProductsSelectBuilder.php b/app/code/Magento/CatalogRule/Model/Indexer/RuleProductsSelectBuilder.php
index 6989a33535ad8..e15bf6b3b1faa 100644
--- a/app/code/Magento/CatalogRule/Model/Indexer/RuleProductsSelectBuilder.php
+++ b/app/code/Magento/CatalogRule/Model/Indexer/RuleProductsSelectBuilder.php
@@ -74,15 +74,12 @@ public function __construct(
* Build select for indexer according passed parameters.
*
* @param int $websiteId
- * @param \Magento\Catalog\Model\Product|null $product
+ * @param int|null $productId
* @param bool $useAdditionalTable
* @return \Zend_Db_Statement_Interface
*/
- public function build(
- $websiteId,
- \Magento\Catalog\Model\Product $product = null,
- $useAdditionalTable = false
- ) {
+ public function build(int $websiteId, ?int $productId = null, bool $useAdditionalTable = false)
+ {
$connection = $this->resource->getConnection();
$indexTable = $this->resource->getTableName('catalogrule_product');
if ($useAdditionalTable) {
@@ -107,8 +104,8 @@ public function build(
['rp.website_id', 'rp.customer_group_id', 'rp.product_id', 'rp.sort_order', 'rp.rule_id']
);
- if ($product && $product->getEntityId()) {
- $select->where('rp.product_id=?', $product->getEntityId());
+ if ($productId) {
+ $select->where('rp.product_id=?', $productId);
}
/**
@@ -159,9 +156,11 @@ public function build(
sprintf($joinCondition, $tableAlias, $storeId),
[]
);
- $select->columns([
- 'default_price' => $connection->getIfNullSql($tableAlias . '.value', 'pp_default.value'),
- ]);
+ $select->columns(
+ [
+ 'default_price' => $connection->getIfNullSql($tableAlias . '.value', 'pp_default.value'),
+ ]
+ );
return $connection->query($select);
}
diff --git a/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php b/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php
index 7cbbc547571ab..ec63d70d55abf 100644
--- a/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php
+++ b/app/code/Magento/CatalogRule/Pricing/Price/CatalogRulePrice.php
@@ -85,7 +85,8 @@ public function getValue()
{
if (null === $this->value) {
if ($this->product->hasData(self::PRICE_CODE)) {
- $this->value = (float)$this->product->getData(self::PRICE_CODE) ?: false;
+ $value = $this->product->getData(self::PRICE_CODE);
+ $this->value = $value ? (float)$value : false;
} else {
$this->value = $this->ruleResource->getRulePrice(
$this->dateTime->scopeDate($this->storeManager->getStore()->getId()),
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCreateNewCatalogPriceRuleActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCreateNewCatalogPriceRuleActionGroup.xml
index 00dcb68089b73..209095e0b0195 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCreateNewCatalogPriceRuleActionGroup.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCreateNewCatalogPriceRuleActionGroup.xml
@@ -21,7 +21,6 @@
-
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCatalogPriceRuleFormActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCatalogPriceRuleFormActionGroup.xml
index 77fe0f50653c7..0a4b6366d11a8 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCatalogPriceRuleFormActionGroup.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AssertCatalogPriceRuleFormActionGroup.xml
@@ -13,7 +13,7 @@
Validates that the provided Catalog Rule, Status, Websites and Customer Group details are present and correct on a Admin Catalog Price Rule creation/edit page.
-
+
@@ -21,7 +21,6 @@
-
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml
index a7500858fc94e..50b4165a3f34b 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/CatalogPriceRuleActionGroup.xml
@@ -25,6 +25,7 @@
+
@@ -47,17 +48,39 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -185,4 +208,48 @@
+
+
+
+ Clicks on the Conditions tab. Fills in the provided condition for Catalog Price Rule.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Goes to the Catalog Price Rule grid. Clicks on Add. Fills in the provided Catalog Rule details with invalid data.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml
index 75a7484324576..2920a895f607d 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleData.xml
@@ -173,4 +173,19 @@
Free Shipping in conditions
Free Shipping in conditions
+
+
+ CatalogPriceRule
+ Catalog Price Rule Description
+ 1
+
+ - 0
+
+
+ - 1
+
+ by_percent
+ 10
+ ten
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleProductConditionData.xml b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleProductConditionData.xml
new file mode 100644
index 0000000000000..510ea25c3f566
--- /dev/null
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Data/CatalogRuleProductConditionData.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Magento\CatalogRule\Model\Rule\Condition\Product|category_ids
+
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Metadata/catalog-rule-meta.xml b/app/code/Magento/CatalogRule/Test/Mftf/Metadata/catalog-rule-meta.xml
index 0d89c7970b852..5b6b610508fef 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Metadata/catalog-rule-meta.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Metadata/catalog-rule-meta.xml
@@ -9,8 +9,9 @@
-
+
application/x-www-form-urlencoded
string
string
@@ -24,4 +25,7 @@
string
string
+
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminNewCatalogPriceRulePage.xml b/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminNewCatalogPriceRulePage.xml
index fded4f5e5f322..71372481490e8 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminNewCatalogPriceRulePage.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Page/AdminNewCatalogPriceRulePage.xml
@@ -10,5 +10,6 @@
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd">
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml
index ba0493d8e995b..7d375da6dfb65 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Section/AdminNewCatalogPriceRuleSection.xml
@@ -20,8 +20,12 @@
+
+
+
+
@@ -33,6 +37,8 @@
+
+
-
-
+
+
+
@@ -51,6 +58,10 @@
+
+
+
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml
index 741da96179b8c..ca534ec7f5375 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml
@@ -52,6 +52,7 @@
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.xml
index befe0b0ce7f98..ee61af180d350 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest.xml
@@ -25,6 +25,10 @@
+
+
+
+
@@ -150,6 +154,7 @@
+
@@ -172,6 +177,10 @@
+
+
+
+
@@ -187,4 +196,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml
index 5223b18df4e4a..83dff1ecdcab5 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml
@@ -58,6 +58,7 @@
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest.xml
index 06392764290ac..d80759531ecae 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest.xml
@@ -18,7 +18,7 @@
-
+
@@ -59,7 +59,7 @@
-
+
@@ -154,6 +154,10 @@
+
+
+
+
@@ -188,7 +192,7 @@
-
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml
index d3546d06492be..16fbca2697702 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml
@@ -19,14 +19,14 @@
-
+
-
-
+
+
@@ -34,9 +34,16 @@
-
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml
index b7a231df5045d..b9d601238ac73 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml
@@ -16,6 +16,9 @@
+
+
+
@@ -148,7 +151,7 @@
-
+
@@ -195,6 +198,7 @@
Websites: Main Website
Customer Groups: NOT LOGGED IN -->
+
@@ -228,6 +232,7 @@
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml
index 5b7e722c92a02..a251ee1e235d0 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml
@@ -77,63 +77,27 @@
-
+
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
+
-
-
-
-
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml
index b486654fe9acf..08e59c6316411 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml
@@ -26,9 +26,14 @@
-
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php
deleted file mode 100644
index 78668366bccdc..0000000000000
--- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/IndexBuilderTest.php
+++ /dev/null
@@ -1,289 +0,0 @@
-resource = $this->createPartialMock(
- \Magento\Framework\App\ResourceConnection::class,
- ['getConnection', 'getTableName']
- );
- $this->ruleCollectionFactory = $this->createPartialMock(
- \Magento\CatalogRule\Model\ResourceModel\Rule\CollectionFactory::class,
- ['create']
- );
- $this->backend = $this->createMock(\Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend::class);
- $this->select = $this->createMock(\Magento\Framework\DB\Select::class);
- $this->metadataPool = $this->createMock(\Magento\Framework\EntityManager\MetadataPool::class);
- $metadata = $this->createMock(\Magento\Framework\EntityManager\EntityMetadata::class);
- $this->metadataPool->expects($this->any())->method('getMetadata')->willReturn($metadata);
- $this->connection = $this->createMock(\Magento\Framework\DB\Adapter\AdapterInterface::class);
- $this->db = $this->createMock(\Zend_Db_Statement_Interface::class);
- $this->website = $this->createMock(\Magento\Store\Model\Website::class);
- $this->storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class);
- $this->combine = $this->createMock(\Magento\Rule\Model\Condition\Combine::class);
- $this->rules = $this->createMock(\Magento\CatalogRule\Model\Rule::class);
- $this->logger = $this->createMock(\Psr\Log\LoggerInterface::class);
- $this->attribute = $this->createMock(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class);
- $this->priceCurrency = $this->createMock(\Magento\Framework\Pricing\PriceCurrencyInterface::class);
- $this->dateFormat = $this->createMock(\Magento\Framework\Stdlib\DateTime::class);
- $this->dateTime = $this->createMock(\Magento\Framework\Stdlib\DateTime\DateTime::class);
- $this->eavConfig = $this->createPartialMock(\Magento\Eav\Model\Config::class, ['getAttribute']);
- $this->product = $this->createMock(\Magento\Catalog\Model\Product::class);
- $this->productFactory = $this->createPartialMock(\Magento\Catalog\Model\ProductFactory::class, ['create']);
- $this->connection->expects($this->any())->method('select')->will($this->returnValue($this->select));
- $this->connection->expects($this->any())->method('query')->will($this->returnValue($this->db));
- $this->select->expects($this->any())->method('distinct')->will($this->returnSelf());
- $this->select->expects($this->any())->method('where')->will($this->returnSelf());
- $this->select->expects($this->any())->method('from')->will($this->returnSelf());
- $this->select->expects($this->any())->method('order')->will($this->returnSelf());
- $this->resource->expects($this->any())->method('getConnection')->will($this->returnValue($this->connection));
- $this->resource->expects($this->any())->method('getTableName')->will($this->returnArgument(0));
- $this->storeManager->expects($this->any())->method('getWebsites')->will($this->returnValue([$this->website]));
- $this->storeManager->expects($this->any())->method('getWebsite')->will($this->returnValue($this->website));
- $this->rules->expects($this->any())->method('getId')->will($this->returnValue(1));
- $this->rules->expects($this->any())->method('getWebsiteIds')->will($this->returnValue([1]));
- $this->rules->expects($this->any())->method('getCustomerGroupIds')->will($this->returnValue([1]));
-
- $ruleCollection = $this->createMock(\Magento\CatalogRule\Model\ResourceModel\Rule\Collection::class);
- $this->ruleCollectionFactory->expects($this->once())
- ->method('create')
- ->willReturn($ruleCollection);
- $ruleCollection->expects($this->once())
- ->method('addFieldToFilter')
- ->willReturnSelf();
- $ruleIterator = new \ArrayIterator([$this->rules]);
- $ruleCollection->method('getIterator')
- ->willReturn($ruleIterator);
-
- $this->product->expects($this->any())->method('load')->will($this->returnSelf());
- $this->product->expects($this->any())->method('getId')->will($this->returnValue(1));
- $this->product->expects($this->any())->method('getWebsiteIds')->will($this->returnValue([1]));
-
- $this->rules->expects($this->any())->method('validate')->with($this->product)->willReturn(true);
- $this->attribute->expects($this->any())->method('getBackend')->will($this->returnValue($this->backend));
- $this->productFactory->expects($this->any())->method('create')->will($this->returnValue($this->product));
- $this->productLoader = $this->getMockBuilder(ProductLoader::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->indexBuilder = (new ObjectManager($this))->getObject(
- \Magento\CatalogRule\Model\Indexer\IndexBuilder::class,
- [
- 'ruleCollectionFactory' => $this->ruleCollectionFactory,
- 'priceCurrency' => $this->priceCurrency,
- 'resource' => $this->resource,
- 'storeManager' => $this->storeManager,
- 'logger' => $this->logger,
- 'eavConfig' => $this->eavConfig,
- 'dateFormat' => $this->dateFormat,
- 'dateTime' => $this->dateTime,
- 'productFactory' => $this->productFactory,
- 'productLoader' => $this->productLoader,
- ]
- );
-
- $this->reindexRuleProductPrice = $this->createMock(
- \Magento\CatalogRule\Model\Indexer\ReindexRuleProductPrice::class
- );
- $this->reindexRuleGroupWebsite = $this->createMock(
- \Magento\CatalogRule\Model\Indexer\ReindexRuleGroupWebsite::class
- );
- $this->setProperties(
- $this->indexBuilder,
- [
- 'metadataPool' => $this->metadataPool,
- 'reindexRuleProductPrice' => $this->reindexRuleProductPrice,
- 'reindexRuleGroupWebsite' => $this->reindexRuleGroupWebsite,
- ]
- );
- }
-
- /**
- * Test UpdateCatalogRuleGroupWebsiteData
- *
- * @covers \Magento\CatalogRule\Model\Indexer\IndexBuilder::updateCatalogRuleGroupWebsiteData
- * @return void
- */
- public function testUpdateCatalogRuleGroupWebsiteData()
- {
- $priceAttrMock = $this->createPartialMock(\Magento\Catalog\Model\Entity\Attribute::class, ['getBackend']);
- $backendModelMock = $this->createPartialMock(
- \Magento\Catalog\Model\Product\Attribute\Backend\Tierprice::class,
- ['getResource']
- );
- $resourceMock = $this->createPartialMock(
- \Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Tierprice::class,
- ['getMainTable']
- );
- $resourceMock->expects($this->any())
- ->method('getMainTable')
- ->will($this->returnValue('catalog_product_entity_tier_price'));
- $backendModelMock->expects($this->any())
- ->method('getResource')
- ->will($this->returnValue($resourceMock));
- $priceAttrMock->expects($this->any())
- ->method('getBackend')
- ->will($this->returnValue($backendModelMock));
-
- $iterator = [$this->product];
- $this->productLoader->expects($this->once())
- ->method('getProducts')
- ->willReturn($iterator);
-
- $this->reindexRuleProductPrice->expects($this->once())->method('execute')->willReturn(true);
- $this->reindexRuleGroupWebsite->expects($this->once())->method('execute')->willReturn(true);
-
- $this->indexBuilder->reindexByIds([1]);
- }
-
- /**
- * @param $object
- * @param array $properties
- */
- private function setProperties($object, $properties = [])
- {
- $reflectionClass = new \ReflectionClass(get_class($object));
- foreach ($properties as $key => $value) {
- if ($reflectionClass->hasProperty($key)) {
- $reflectionProperty = $reflectionClass->getProperty($key);
- $reflectionProperty->setAccessible(true);
- $reflectionProperty->setValue($object, $value);
- }
- }
- }
-}
diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php
index 5f63283df6760..d0f266d574945 100644
--- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php
+++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/ReindexRuleProductPriceTest.php
@@ -71,6 +71,7 @@ public function testExecute()
$websiteId = 234;
$defaultGroupId = 11;
$defaultStoreId = 22;
+ $productId = 55;
$websiteMock = $this->createMock(WebsiteInterface::class);
$websiteMock->expects($this->once())
@@ -93,11 +94,10 @@ public function testExecute()
->with($defaultGroupId)
->willReturn($groupMock);
- $productMock = $this->createMock(Product::class);
$statementMock = $this->createMock(\Zend_Db_Statement_Interface::class);
$this->ruleProductsSelectBuilderMock->expects($this->once())
->method('build')
- ->with($websiteId, $productMock, true)
+ ->with($websiteId, $productId, true)
->willReturn($statementMock);
$ruleData = [
@@ -126,6 +126,6 @@ public function testExecute()
$this->pricesPersistorMock->expects($this->once())
->method('execute');
- $this->assertTrue($this->model->execute(1, $productMock, true));
+ $this->assertTrue($this->model->execute(1, $productId, true));
}
}
diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/RuleProductsSelectBuilderTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/RuleProductsSelectBuilderTest.php
deleted file mode 100644
index e43fe41dc2127..0000000000000
--- a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/RuleProductsSelectBuilderTest.php
+++ /dev/null
@@ -1,200 +0,0 @@
-storeManagerMock = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class)
- ->getMockForAbstractClass();
- $this->resourceMock = $this->getMockBuilder(\Magento\Framework\App\ResourceConnection::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->activeTableSwitcherMock = $this->getMockBuilder(ActiveTableSwitcher::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->eavConfigMock = $this->getMockBuilder(\Magento\Eav\Model\Config::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->metadataPoolMock = $this->getMockBuilder(\Magento\Framework\EntityManager\MetadataPool::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->tableSwapperMock = $this->getMockForAbstractClass(
- IndexerTableSwapperInterface::class
- );
-
- $this->model = new \Magento\CatalogRule\Model\Indexer\RuleProductsSelectBuilder(
- $this->resourceMock,
- $this->eavConfigMock,
- $this->storeManagerMock,
- $this->metadataPoolMock,
- $this->activeTableSwitcherMock,
- $this->tableSwapperMock
- );
- }
-
- /**
- * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
- */
- public function testBuild()
- {
- $websiteId = 55;
- $ruleTable = 'catalogrule_product';
- $rplTable = 'catalogrule_product_replica';
- $prTable = 'catalog_product_entity';
- $wsTable = 'catalog_product_website';
- $productMock = $this->getMockBuilder(Product::class)->disableOriginalConstructor()->getMock();
- $productMock->expects($this->exactly(2))->method('getEntityId')->willReturn(500);
-
- $connectionMock = $this->getMockBuilder(AdapterInterface::class)->disableOriginalConstructor()->getMock();
- $this->resourceMock->expects($this->at(0))->method('getConnection')->willReturn($connectionMock);
-
- $this->tableSwapperMock->expects($this->once())
- ->method('getWorkingTableName')
- ->with($ruleTable)
- ->willReturn($rplTable);
-
- $this->resourceMock->expects($this->at(1))->method('getTableName')->with($ruleTable)->willReturn($ruleTable);
- $this->resourceMock->expects($this->at(2))->method('getTableName')->with($rplTable)->willReturn($rplTable);
- $this->resourceMock->expects($this->at(3))->method('getTableName')->with($prTable)->willReturn($prTable);
- $this->resourceMock->expects($this->at(4))->method('getTableName')->with($wsTable)->willReturn($wsTable);
-
- $selectMock = $this->getMockBuilder(Select::class)->disableOriginalConstructor()->getMock();
- $connectionMock->expects($this->once())->method('select')->willReturn($selectMock);
- $selectMock->expects($this->at(0))->method('from')->with(['rp' => $rplTable])->willReturnSelf();
- $selectMock->expects($this->at(1))
- ->method('order')
- ->with(['rp.website_id', 'rp.customer_group_id', 'rp.product_id', 'rp.sort_order', 'rp.rule_id'])
- ->willReturnSelf();
- $selectMock->expects($this->at(2))->method('where')->with('rp.product_id=?', 500)->willReturnSelf();
-
- $attributeMock = $this->getMockBuilder(AbstractAttribute::class)->disableOriginalConstructor()->getMock();
- $this->eavConfigMock->expects($this->once())
- ->method('getAttribute')
- ->with(Product::ENTITY, 'price')
- ->willReturn($attributeMock);
- $backendMock = $this->getMockBuilder(AbstractBackend::class)->disableOriginalConstructor()->getMock();
- $backendMock->expects($this->once())->method('getTable')->willReturn('price_table');
- $attributeMock->expects($this->once())->method('getBackend')->willReturn($backendMock);
- $attributeMock->expects($this->once())->method('getId')->willReturn(200);
-
- $metadataMock = $this->getMockBuilder(EntityMetadataInterface::class)->disableOriginalConstructor()->getMock();
- $this->metadataPoolMock->expects($this->once())
- ->method('getMetadata')
- ->with(\Magento\Catalog\Api\Data\ProductInterface::class)
- ->willReturn($metadataMock);
- $metadataMock->expects($this->once())->method('getLinkField')->willReturn('link_field');
-
- $selectMock->expects($this->at(3))
- ->method('join')
- ->with(['e' => $prTable], 'e.entity_id = rp.product_id', [])
- ->willReturnSelf();
- $selectMock->expects($this->at(4))
- ->method('join')
- ->with(
- ['pp_default' => 'price_table'],
- 'pp_default.link_field=e.link_field AND (pp_default.attribute_id=200) and pp_default.store_id=0',
- []
- )->willReturnSelf();
- $websiteMock = $this->getMockBuilder(WebsiteInterface::class)
- ->setMethods(['getDefaultGroup'])
- ->getMockForAbstractClass();
- $this->storeManagerMock->expects($this->once())
- ->method('getWebsite')
- ->with($websiteId)
- ->willReturn($websiteMock);
-
- $groupMock = $this->getMockBuilder(\Magento\Store\Model\Group::class)
- ->setMethods(['getDefaultStoreId'])
- ->disableOriginalConstructor()
- ->getMock();
- $websiteMock->expects($this->once())->method('getDefaultGroup')->willReturn($groupMock);
- $groupMock->expects($this->once())->method('getDefaultStoreId')->willReturn(700);
-
- $selectMock->expects($this->at(5))
- ->method('joinInner')
- ->with(
- ['product_website' => $wsTable],
- 'product_website.product_id=rp.product_id '
- . 'AND product_website.website_id = rp.website_id '
- . 'AND product_website.website_id='
- . $websiteId,
- []
- )->willReturnSelf();
- $selectMock->expects($this->at(6))
- ->method('joinLeft')
- ->with(
- ['pp' . $websiteId => 'price_table'],
- 'pp55.link_field=e.link_field AND (pp55.attribute_id=200) and pp55.store_id=700',
- []
- )->willReturnSelf();
-
- $connectionMock->expects($this->once())
- ->method('getIfNullSql')
- ->with('pp55.value', 'pp_default.value')
- ->willReturn('IF NULL SQL');
- $selectMock->expects($this->at(7))
- ->method('columns')
- ->with(['default_price' => 'IF NULL SQL'])
- ->willReturnSelf();
- $statementMock = $this->getMockBuilder(\Zend_Db_Statement_Interface::class)
- ->disableOriginalConstructor()
- ->getMock();
- $connectionMock->expects($this->once())->method('query')->with($selectMock)->willReturn($statementMock);
-
- $this->assertEquals($statementMock, $this->model->build($websiteId, $productMock, true));
- }
-}
diff --git a/app/code/Magento/CatalogRule/Test/Unit/Pricing/Price/CatalogRulePriceTest.php b/app/code/Magento/CatalogRule/Test/Unit/Pricing/Price/CatalogRulePriceTest.php
index 7514d2bc4b5c5..54cd9e411df5c 100644
--- a/app/code/Magento/CatalogRule/Test/Unit/Pricing/Price/CatalogRulePriceTest.php
+++ b/app/code/Magento/CatalogRule/Test/Unit/Pricing/Price/CatalogRulePriceTest.php
@@ -176,4 +176,17 @@ public function testGetAmountNoBaseAmount()
$result = $this->object->getValue();
$this->assertFalse($result);
}
+
+ public function testGetValueWithNullAmount()
+ {
+ $catalogRulePrice = null;
+ $convertedPrice = 0.0;
+
+ $this->saleableItemMock->expects($this->once())->method('hasData')
+ ->with('catalog_rule_price')->willReturn(true);
+ $this->saleableItemMock->expects($this->once())->method('getData')
+ ->with('catalog_rule_price')->willReturn($catalogRulePrice);
+
+ $this->assertEquals($convertedPrice, $this->object->getValue());
+ }
}
diff --git a/app/code/Magento/CatalogRule/etc/db_schema.xml b/app/code/Magento/CatalogRule/etc/db_schema.xml
index 59082e93b04c2..b3692f280fec5 100644
--- a/app/code/Magento/CatalogRule/etc/db_schema.xml
+++ b/app/code/Magento/CatalogRule/etc/db_schema.xml
@@ -37,7 +37,7 @@
+ comment="Rule Product ID"/>
+ default="0" comment="Product ID"/>
+ comment="Website ID"/>
@@ -91,11 +91,11 @@
+ default="0" comment="Product ID"/>
+ comment="Website ID"/>
@@ -121,9 +121,9 @@
+ default="0" comment="Customer Group ID"/>
+ default="0" comment="Website ID"/>
@@ -140,7 +140,7 @@
+ comment="Website ID"/>
@@ -160,7 +160,7 @@
+ comment="Customer Group ID"/>
@@ -177,7 +177,7 @@
+ comment="Rule Product ID"/>
+ default="0" comment="Product ID"/>
+ comment="Website ID"/>
@@ -232,11 +232,11 @@
+ default="0" comment="Product ID"/>
+ comment="Website ID"/>
@@ -263,9 +263,9 @@
+ default="0" comment="Customer Group ID"/>
+ default="0" comment="Website ID"/>
diff --git a/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml b/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml
index c114f6b1d77cd..59e3c4668e8a4 100644
--- a/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml
+++ b/app/code/Magento/CatalogRule/view/adminhtml/ui_component/catalog_rule_form.xml
@@ -95,33 +95,30 @@
description
-
+
-
- catalog_rule
+ - 0
- number
- Status
- true
- is_active
+
+ true
+
+ boolean
+ Active
-
+
-
-
- - 1
- - Active
-
-
- - 0
- - Inactive
-
-
+
+ 0
+ 1
+
+ toggle
-
+
@@ -211,6 +208,9 @@
+
+ true
+
text
Priority
sort_order
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 c758e773f43c1..a97d362c5de7f 100644
--- a/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php
+++ b/app/code/Magento/CatalogSearch/Model/Adapter/Mysql/Filter/Preprocessor.php
@@ -165,7 +165,10 @@ private function processQueryWithField(FilterInterface $filter, $isNegation, $qu
$this->customerSession->getCustomerGroupId()
);
} elseif ($filter->getField() === 'category_ids') {
- return 'category_ids_index.category_id = ' . (int) $filter->getValue();
+ return $this->connection->quoteInto(
+ 'category_ids_index.category_id in (?)',
+ $filter->getValue()
+ );
} elseif ($attribute->isStatic()) {
$alias = $this->aliasResolver->getAlias($filter);
$resultQuery = str_replace(
@@ -198,8 +201,9 @@ private function processQueryWithField(FilterInterface $filter, $isNegation, $qu
)
->joinLeft(
['current_store' => $table],
- 'current_store.attribute_id = main_table.attribute_id AND current_store.store_id = '
- . $currentStoreId,
+ "current_store.{$linkIdField} = main_table.{$linkIdField} AND "
+ . "current_store.attribute_id = main_table.attribute_id AND current_store.store_id = "
+ . $currentStoreId,
null
)
->columns([$filter->getField() => $ifNullCondition])
diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php
index 21d8b7297da7d..912dec8666191 100644
--- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php
+++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php
@@ -3,11 +3,14 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
namespace Magento\CatalogSearch\Model\Indexer;
use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\FullFactory;
+use Magento\CatalogSearch\Model\Indexer\Scope\State;
use Magento\CatalogSearch\Model\Indexer\Scope\StateFactory;
use Magento\CatalogSearch\Model\ResourceModel\Fulltext as FulltextResource;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Indexer\DimensionProviderInterface;
use Magento\Store\Model\StoreDimensionProvider;
use Magento\Indexer\Model\ProcessManager;
@@ -79,6 +82,7 @@ class Fulltext implements
* @param DimensionProviderInterface $dimensionProvider
* @param array $data
* @param ProcessManager $processManager
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __construct(
FullFactory $fullActionFactory,
@@ -95,11 +99,9 @@ public function __construct(
$this->fulltextResource = $fulltextResource;
$this->data = $data;
$this->indexSwitcher = $indexSwitcher;
- $this->indexScopeState = $indexScopeStateFactory->create();
+ $this->indexScopeState = ObjectManager::getInstance()->get(State::class);
$this->dimensionProvider = $dimensionProvider;
- $this->processManager = $processManager ?: \Magento\Framework\App\ObjectManager::getInstance()->get(
- ProcessManager::class
- );
+ $this->processManager = $processManager ?: ObjectManager::getInstance()->get(ProcessManager::class);
}
/**
@@ -127,9 +129,11 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds =
throw new \InvalidArgumentException('Indexer "' . self::INDEXER_ID . '" support only Store dimension');
}
$storeId = $dimensions[StoreDimensionProvider::DIMENSION_NAME]->getValue();
- $saveHandler = $this->indexerHandlerFactory->create([
- 'data' => $this->data
- ]);
+ $saveHandler = $this->indexerHandlerFactory->create(
+ [
+ 'data' => $this->data,
+ ]
+ );
if (null === $entityIds) {
$this->indexScopeState->useTemporaryIndex();
diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php
index 09d4f0068459a..cd2529a8fd725 100644
--- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php
+++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php
@@ -7,14 +7,9 @@
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\Product\Attribute\Source\Status;
-use Magento\CatalogInventory\Api\Data\StockStatusInterface;
-use Magento\CatalogInventory\Api\StockConfigurationInterface;
-use Magento\CatalogInventory\Api\StockStatusCriteriaInterface;
-use Magento\CatalogInventory\Api\StockStatusRepositoryInterface;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\DB\Select;
use Magento\Store\Model\Store;
-use Magento\Framework\App\ObjectManager;
/**
* Catalog search full test search data provider.
@@ -129,16 +124,6 @@ class DataProvider
*/
private $antiGapMultiplier;
- /**
- * @var StockConfigurationInterface
- */
- private $stockConfiguration;
-
- /**
- * @var StockStatusRepositoryInterface
- */
- private $stockStatusRepository;
-
/**
* @param ResourceConnection $resource
* @param \Magento\Catalog\Model\Product\Type $catalogProductType
@@ -563,8 +548,6 @@ public function prepareProductIndex($indexData, $productData, $storeId)
{
$index = [];
- $indexData = $this->filterOutOfStockProducts($indexData, $storeId);
-
foreach ($this->getSearchableAttributes('static') as $attribute) {
$attributeCode = $attribute->getAttributeCode();
@@ -689,68 +672,4 @@ private function filterAttributeValue($value)
{
return preg_replace('/\s+/iu', ' ', trim(strip_tags($value)));
}
-
- /**
- * Filter out of stock products for products.
- *
- * @param array $indexData
- * @param int $storeId
- * @return array
- */
- private function filterOutOfStockProducts($indexData, $storeId): array
- {
- if (!$this->getStockConfiguration()->isShowOutOfStock($storeId)) {
- $productIds = array_keys($indexData);
- $stockStatusCriteria = $this->createStockStatusCriteria();
- $stockStatusCriteria->setProductsFilter($productIds);
- $stockStatusCollection = $this->getStockStatusRepository()->getList($stockStatusCriteria);
- $stockStatuses = $stockStatusCollection->getItems();
- $stockStatuses = array_filter(
- $stockStatuses,
- function (StockStatusInterface $stockStatus) {
- return StockStatusInterface::STATUS_IN_STOCK == $stockStatus->getStockStatus();
- }
- );
- $indexData = array_intersect_key($indexData, $stockStatuses);
- }
- return $indexData;
- }
-
- /**
- * Get stock configuration.
- *
- * @return StockConfigurationInterface
- */
- private function getStockConfiguration()
- {
- if (null === $this->stockConfiguration) {
- $this->stockConfiguration = ObjectManager::getInstance()->get(StockConfigurationInterface::class);
- }
- return $this->stockConfiguration;
- }
-
- /**
- * Create stock status criteria.
- *
- * Substitution of autogenerated factory in backward compatibility reasons.
- *
- * @return StockStatusCriteriaInterface
- */
- private function createStockStatusCriteria()
- {
- return ObjectManager::getInstance()->create(StockStatusCriteriaInterface::class);
- }
-
- /**
- * Get stock status repository.
- *
- * @return StockStatusRepositoryInterface
- */
- private function getStockStatusRepository()
- {
- if (null === $this->stockStatusRepository) {
- $this->stockStatusRepository = ObjectManager::getInstance()->get(StockStatusRepositoryInterface::class);
- }
- return $this->stockStatusRepository;
- }
}
diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Product/Category/Action/Rows.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Product/Category/Action/Rows.php
new file mode 100644
index 0000000000000..2b1844deb114c
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Product/Category/Action/Rows.php
@@ -0,0 +1,52 @@
+indexerRegistry = $indexerRegistry;
+ }
+
+ /**
+ * Reindex after catalog category product reindex
+ *
+ * @param ActionRows $subject
+ * @param ActionRows $result
+ * @param array $entityIds
+ * @return ActionRows
+ *
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterExecute(ActionRows $subject, ActionRows $result, array $entityIds): ActionRows
+ {
+ if (!empty($entityIds)) {
+ $indexer = $this->indexerRegistry->get(FulltextIndexer::INDEXER_ID);
+ if ($indexer->isScheduled()) {
+ $indexer->reindexList($entityIds);
+ }
+ }
+ return $result;
+ }
+}
diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Plugin/StockedProductsFilterPlugin.php b/app/code/Magento/CatalogSearch/Model/Indexer/Plugin/StockedProductsFilterPlugin.php
new file mode 100644
index 0000000000000..02e48c5d8a1c0
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Model/Indexer/Plugin/StockedProductsFilterPlugin.php
@@ -0,0 +1,91 @@
+stockConfiguration = $stockConfiguration;
+ $this->stockStatusRepository = $stockStatusRepository;
+ $this->stockStatusCriteriaFactory = $stockStatusCriteriaFactory;
+ }
+
+ /**
+ * Filter out of stock options for configurable product.
+ *
+ * @param DataProvider $dataProvider
+ * @param array $indexData
+ * @param array $productData
+ * @param int $storeId
+ * @return array
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function beforePrepareProductIndex(
+ DataProvider $dataProvider,
+ array $indexData,
+ array $productData,
+ int $storeId
+ ): array {
+ if (!$this->stockConfiguration->isShowOutOfStock($storeId)) {
+ $productIds = array_keys($indexData);
+ $stockStatusCriteria = $this->stockStatusCriteriaFactory->create();
+ $stockStatusCriteria->setProductsFilter($productIds);
+ $stockStatusCollection = $this->stockStatusRepository->getList($stockStatusCriteria);
+ $stockStatuses = $stockStatusCollection->getItems();
+ $stockStatuses = array_filter(
+ $stockStatuses,
+ function (StockStatusInterface $stockStatus) {
+ return StockStatusInterface::STATUS_IN_STOCK == $stockStatus->getStockStatus();
+ }
+ );
+ $indexData = array_intersect_key($indexData, $stockStatuses);
+ }
+
+ return [
+ $indexData,
+ $productData,
+ $storeId,
+ ];
+ }
+}
diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php
index 85a32dc60b119..d8947ac4224a8 100644
--- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php
+++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php
@@ -58,15 +58,18 @@ public function apply(\Magento\Framework\App\RequestInterface $request)
if (empty($attributeValue) && !is_numeric($attributeValue)) {
return $this;
}
+
$attribute = $this->getAttributeModel();
/** @var \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection $productCollection */
$productCollection = $this->getLayer()
->getProductCollection();
$productCollection->addFieldToFilter($attribute->getAttributeCode(), $attributeValue);
- $label = $this->getOptionText($attributeValue);
- if (is_array($label)) {
- $label = implode(',', $label);
+
+ $labels = [];
+ foreach ((array) $attributeValue as $value) {
+ $labels[] = (array) $this->getOptionText($value);
}
+ $label = implode(',', array_unique(array_merge(...$labels)));
$this->getLayer()
->getState()
->addFilter($this->_createItem($label, $attributeValue));
diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php
index e9fb1070fedd5..3b0c4dfb6df2f 100644
--- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php
+++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Decimal.php
@@ -3,6 +3,8 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\CatalogSearch\Model\Layer\Filter;
use Magento\Catalog\Model\Layer\Filter\AbstractFilter;
@@ -12,6 +14,9 @@
*/
class Decimal extends AbstractFilter
{
+ /** Decimal delta for filter */
+ private const DECIMAL_DELTA = 0.001;
+
/**
* @var \Magento\Framework\Pricing\PriceCurrencyInterface
*/
@@ -70,11 +75,17 @@ public function apply(\Magento\Framework\App\RequestInterface $request)
list($from, $to) = explode('-', $filter);
+ // When the range is 10-20 we only need to get products that are in the 10-19.99 range.
+ $toValue = $to;
+ if (!empty($toValue) && $from !== $toValue) {
+ $toValue -= self::DECIMAL_DELTA;
+ }
+
$this->getLayer()
->getProductCollection()
->addFieldToFilter(
$this->getAttributeModel()->getAttributeCode(),
- ['from' => $from, 'to' => $to]
+ ['from' => $from, 'to' => $toValue]
);
$this->getLayer()->getState()->addFilter(
@@ -111,7 +122,7 @@ protected function _getItemsData()
$from = '';
}
if ($to == '*') {
- $to = null;
+ $to = '';
}
$label = $this->renderRangeLabel(empty($from) ? 0 : $from, $to);
$value = $from . '-' . $to;
@@ -138,7 +149,7 @@ protected function _getItemsData()
protected function renderRangeLabel($fromPrice, $toPrice)
{
$formattedFromPrice = $this->priceCurrency->format($fromPrice);
- if ($toPrice === null) {
+ if ($toPrice === '') {
return __('%1 and above', $formattedFromPrice);
} else {
if ($fromPrice != $toPrice) {
diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php
index a19f53469ae01..66d9281ed38e2 100644
--- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php
+++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php
@@ -3,6 +3,8 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\CatalogSearch\Model\Layer\Filter;
use Magento\Catalog\Model\Layer\Filter\AbstractFilter;
@@ -11,6 +13,7 @@
* Layer price filter based on Search API
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
*/
class Price extends AbstractFilter
{
@@ -138,7 +141,7 @@ public function apply(\Magento\Framework\App\RequestInterface $request)
list($from, $to) = $filter;
$this->getLayer()->getProductCollection()->addFieldToFilter(
- 'price',
+ $this->getAttributeModel()->getAttributeCode(),
['from' => $from, 'to' => empty($to) || $from == $to ? $to : $to - self::PRICE_DELTA]
);
diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php
index 1946dd35b8d37..bbebbc99103a2 100644
--- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php
+++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php
@@ -27,6 +27,7 @@
use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\Api\Search\SearchResultInterface;
+use Magento\Search\Model\EngineResolver;
/**
* Advanced search collection
@@ -40,6 +41,11 @@
*/
class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection
{
+ /**
+ * Config search engine path.
+ */
+ private const SEARCH_ENGINE_VALUE_PATH = 'catalog/search/engine';
+
/**
* List Of filters
* @var array
@@ -125,7 +131,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection
* @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper
* @param \Magento\Framework\Validator\UniversalFactory $universalFactory
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
- * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager
+ * @param \Magento\Framework\Module\Manager $moduleManager
* @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState
* @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
* @param Product\OptionFactory $productOptionFactory
@@ -160,7 +166,7 @@ public function __construct(
\Magento\Catalog\Model\ResourceModel\Helper $resourceHelper,
\Magento\Framework\Validator\UniversalFactory $universalFactory,
\Magento\Store\Model\StoreManagerInterface $storeManager,
- \Magento\Framework\Module\ModuleManagerInterface $moduleManager,
+ \Magento\Framework\Module\Manager $moduleManager,
\Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState,
\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
\Magento\Catalog\Model\Product\OptionFactory $productOptionFactory,
@@ -344,6 +350,63 @@ protected function _renderFiltersBefore()
parent::_renderFiltersBefore();
}
+ /**
+ * @inheritDoc
+ */
+ public function clear()
+ {
+ $this->searchResult = null;
+ return parent::clear();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function _reset()
+ {
+ $this->searchResult = null;
+ return parent::_reset();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function _loadEntities($printQuery = false, $logQuery = false)
+ {
+ $this->getEntity();
+
+ $currentSearchEngine = $this->_scopeConfig->getValue(self::SEARCH_ENGINE_VALUE_PATH);
+ if ($this->_pageSize && $currentSearchEngine === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE) {
+ $this->getSelect()->limitPage($this->getCurPage(), $this->_pageSize);
+ }
+
+ $this->printLogQuery($printQuery, $logQuery);
+
+ try {
+ /**
+ * Prepare select query
+ * @var string $query
+ */
+ $query = $this->getSelect();
+ $rows = $this->_fetchAll($query);
+ } catch (\Exception $e) {
+ $this->printLogQuery(false, true, $query);
+ throw $e;
+ }
+
+ foreach ($rows as $value) {
+ $object = $this->getNewEmptyItem()->setData($value);
+ $this->addItem($object);
+ if (isset($this->_itemsById[$object->getId()])) {
+ $this->_itemsById[$object->getId()][] = $object;
+ } else {
+ $this->_itemsById[$object->getId()] = [$object];
+ }
+ }
+
+ return $this;
+ }
+
/**
* Get total records resolver.
*
@@ -391,7 +454,9 @@ private function getSearchResultApplier(SearchResultInterface $searchResult): Se
'collection' => $this,
'searchResult' => $searchResult,
/** This variable sets by serOrder method, but doesn't have a getter method. */
- 'orders' => $this->_orders
+ 'orders' => $this->_orders,
+ 'size' => $this->getPageSize(),
+ 'currentPage' => (int)$this->_curPage,
]
);
}
diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php
index 4f84f3868c6a3..3506437ea038d 100644
--- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php
+++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php
@@ -27,6 +27,7 @@
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\App\ObjectManager;
use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory;
+use Magento\Search\Model\EngineResolver;
/**
* Fulltext Collection
@@ -41,6 +42,11 @@
*/
class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection
{
+ /**
+ * Config search engine path.
+ */
+ private const SEARCH_ENGINE_VALUE_PATH = 'catalog/search/engine';
+
/**
* @var QueryResponse
* @deprecated 100.1.0
@@ -146,7 +152,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection
* @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper
* @param \Magento\Framework\Validator\UniversalFactory $universalFactory
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
- * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager
+ * @param \Magento\Framework\Module\Manager $moduleManager
* @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState
* @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
* @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory
@@ -185,7 +191,7 @@ public function __construct(
\Magento\Catalog\Model\ResourceModel\Helper $resourceHelper,
\Magento\Framework\Validator\UniversalFactory $universalFactory,
\Magento\Store\Model\StoreManagerInterface $storeManager,
- \Magento\Framework\Module\ModuleManagerInterface $moduleManager,
+ \Magento\Framework\Module\Manager $moduleManager,
\Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState,
\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
\Magento\Catalog\Model\Product\OptionFactory $productOptionFactory,
@@ -212,10 +218,8 @@ public function __construct(
DefaultFilterStrategyApplyCheckerInterface $defaultFilterStrategyApplyChecker = null
) {
$this->queryFactory = $catalogSearchData;
- if ($searchResultFactory === null) {
- $this->searchResultFactory = \Magento\Framework\App\ObjectManager::getInstance()
+ $this->searchResultFactory = $searchResultFactory ?? \Magento\Framework\App\ObjectManager::getInstance()
->get(\Magento\Framework\Api\Search\SearchResultFactory::class);
- }
parent::__construct(
$entityFactory,
$logger,
@@ -375,6 +379,63 @@ public function addFieldToFilter($field, $condition = null)
return $this;
}
+ /**
+ * @inheritDoc
+ */
+ public function clear()
+ {
+ $this->searchResult = null;
+ return parent::clear();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function _reset()
+ {
+ $this->searchResult = null;
+ return parent::_reset();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function _loadEntities($printQuery = false, $logQuery = false)
+ {
+ $this->getEntity();
+
+ $currentSearchEngine = $this->_scopeConfig->getValue(self::SEARCH_ENGINE_VALUE_PATH);
+ if ($this->_pageSize && $currentSearchEngine === EngineResolver::CATALOG_SEARCH_MYSQL_ENGINE) {
+ $this->getSelect()->limitPage($this->getCurPage(), $this->_pageSize);
+ }
+
+ $this->printLogQuery($printQuery, $logQuery);
+
+ try {
+ /**
+ * Prepare select query
+ * @var string $query
+ */
+ $query = $this->getSelect();
+ $rows = $this->_fetchAll($query);
+ } catch (\Exception $e) {
+ $this->printLogQuery(false, true, $query);
+ throw $e;
+ }
+
+ foreach ($rows as $value) {
+ $object = $this->getNewEmptyItem()->setData($value);
+ $this->addItem($object);
+ if (isset($this->_itemsById[$object->getId()])) {
+ $this->_itemsById[$object->getId()][] = $object;
+ } else {
+ $this->_itemsById[$object->getId()] = [$object];
+ }
+ }
+
+ return $this;
+ }
+
/**
* Add search query filter
*
@@ -427,25 +488,40 @@ protected function _renderFiltersBefore()
return;
}
- $this->prepareSearchTermFilter();
- $this->preparePriceAggregation();
-
- $searchCriteria = $this->getSearchCriteriaResolver()->resolve();
- try {
- $this->searchResult = $this->getSearch()->search($searchCriteria);
- $this->_totalRecords = $this->getTotalRecordsResolver($this->searchResult)->resolve();
- } catch (EmptyRequestDataException $e) {
- /** @var \Magento\Framework\Api\Search\SearchResultInterface $searchResult */
- $this->searchResult = $this->searchResultFactory->create()->setItems([]);
- } catch (NonExistingRequestNameException $e) {
- $this->_logger->error($e->getMessage());
- throw new LocalizedException(__('An error occurred. For details, see the error log.'));
+ if ($this->searchRequestName !== 'quick_search_container'
+ || strlen(trim($this->queryText))
+ ) {
+ $this->prepareSearchTermFilter();
+ $this->preparePriceAggregation();
+
+ $searchCriteria = $this->getSearchCriteriaResolver()->resolve();
+ try {
+ $this->searchResult = $this->getSearch()->search($searchCriteria);
+ $this->_totalRecords = $this->getTotalRecordsResolver($this->searchResult)->resolve();
+ } catch (EmptyRequestDataException $e) {
+ $this->searchResult = $this->createEmptyResult();
+ } catch (NonExistingRequestNameException $e) {
+ $this->_logger->error($e->getMessage());
+ throw new LocalizedException(__('An error occurred. For details, see the error log.'));
+ }
+ } else {
+ $this->searchResult = $this->createEmptyResult();
}
$this->getSearchResultApplier($this->searchResult)->apply();
parent::_renderFiltersBefore();
}
+ /**
+ * Create empty search result
+ *
+ * @return SearchResultInterface
+ */
+ private function createEmptyResult()
+ {
+ return $this->searchResultFactory->create()->setItems([]);
+ }
+
/**
* Set sort order for search query.
*
@@ -485,12 +561,12 @@ private function getSearchCriteriaResolver(): SearchCriteriaResolverInterface
{
return $this->searchCriteriaResolverFactory->create(
[
- 'builder' => $this->getSearchCriteriaBuilder(),
- 'collection' => $this,
- 'searchRequestName' => $this->searchRequestName,
- 'currentPage' => $this->_curPage,
- 'size' => $this->getPageSize(),
- 'orders' => $this->searchOrders,
+ 'builder' => $this->getSearchCriteriaBuilder(),
+ 'collection' => $this,
+ 'searchRequestName' => $this->searchRequestName,
+ 'currentPage' => (int)$this->_curPage,
+ 'size' => $this->getPageSize(),
+ 'orders' => $this->searchOrders,
]
);
}
@@ -505,10 +581,12 @@ private function getSearchResultApplier(SearchResultInterface $searchResult): Se
{
return $this->searchResultApplierFactory->create(
[
- 'collection' => $this,
- 'searchResult' => $searchResult,
- /** This variable sets by serOrder method, but doesn't have a getter method. */
- 'orders' => $this->_orders,
+ 'collection' => $this,
+ 'searchResult' => $searchResult,
+ /** This variable sets by serOrder method, but doesn't have a getter method. */
+ 'orders' => $this->_orders,
+ 'size' => $this->getPageSize(),
+ 'currentPage' => (int)$this->_curPage,
]
);
}
diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php
index 6cdcc7c55a26f..e625ccbe51fe3 100644
--- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php
+++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php
@@ -50,7 +50,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection
* @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper
* @param \Magento\Framework\Validator\UniversalFactory $universalFactory
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
- * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager
+ * @param \Magento\Framework\Module\Manager $moduleManager
* @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState
* @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
* @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory
@@ -74,7 +74,7 @@ public function __construct(
\Magento\Catalog\Model\ResourceModel\Helper $resourceHelper,
\Magento\Framework\Validator\UniversalFactory $universalFactory,
\Magento\Store\Model\StoreManagerInterface $storeManager,
- \Magento\Framework\Module\ModuleManagerInterface $moduleManager,
+ \Magento\Framework\Module\Manager $moduleManager,
\Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState,
\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
\Magento\Catalog\Model\Product\OptionFactory $productOptionFactory,
diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php
index 8f8ba39ebd329..5ac252677ff79 100644
--- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php
+++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator.php
@@ -3,6 +3,8 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\CatalogSearch\Model\Search;
use Magento\Catalog\Api\Data\EavAttributeInterface;
@@ -78,6 +80,7 @@ private function generateRequest($attributeType, $container, $useFulltext)
{
$request = [];
foreach ($this->getSearchableAttributes() as $attribute) {
+ /** @var $attribute Attribute */
if ($attribute->getData($attributeType)) {
if (!in_array($attribute->getAttributeCode(), ['price', 'category_ids'], true)) {
$queryName = $attribute->getAttributeCode() . '_query';
@@ -97,12 +100,14 @@ private function generateRequest($attributeType, $container, $useFulltext)
],
];
$bucketName = $attribute->getAttributeCode() . self::BUCKET_SUFFIX;
- $generator = $this->generatorResolver->getGeneratorForType($attribute->getBackendType());
+ $generatorType = $attribute->getFrontendInput() === 'price'
+ ? $attribute->getFrontendInput()
+ : $attribute->getBackendType();
+ $generator = $this->generatorResolver->getGeneratorForType($generatorType);
$request['filters'][$filterName] = $generator->getFilterData($attribute, $filterName);
$request['aggregations'][$bucketName] = $generator->getAggregationData($attribute, $bucketName);
}
}
- /** @var $attribute Attribute */
if (!$attribute->getIsSearchable() || in_array($attribute->getAttributeCode(), ['price'], true)) {
// Some fields have their own specific handlers
continue;
diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php
index b3d39a48fe9fc..73d011cc532db 100644
--- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php
+++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Decimal.php
@@ -3,6 +3,7 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
namespace Magento\CatalogSearch\Model\Search\RequestGenerator;
diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Price.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Price.php
new file mode 100644
index 0000000000000..949806d14f45a
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/Price.php
@@ -0,0 +1,46 @@
+ FilterInterface::TYPE_RANGE,
+ 'name' => $filterName,
+ 'field' => $attribute->getAttributeCode(),
+ 'from' => '$' . $attribute->getAttributeCode() . '.from$',
+ 'to' => '$' . $attribute->getAttributeCode() . '.to$',
+ ];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getAggregationData(Attribute $attribute, $bucketName): array
+ {
+ return [
+ 'type' => BucketInterface::TYPE_DYNAMIC,
+ 'name' => $bucketName,
+ 'field' => $attribute->getAttributeCode(),
+ 'method' => '$price_dynamic_algorithm$',
+ 'metric' => [['type' => 'count']],
+ ];
+ }
+}
diff --git a/app/code/Magento/CatalogSearch/Setup/Patch/Data/SetInitialSearchWeightForAttributes.php b/app/code/Magento/CatalogSearch/Setup/Patch/Data/SetInitialSearchWeightForAttributes.php
index 7f6dbe033e3a5..21d5e82d494b5 100644
--- a/app/code/Magento/CatalogSearch/Setup/Patch/Data/SetInitialSearchWeightForAttributes.php
+++ b/app/code/Magento/CatalogSearch/Setup/Patch/Data/SetInitialSearchWeightForAttributes.php
@@ -6,18 +6,21 @@
namespace Magento\CatalogSearch\Setup\Patch\Data;
+use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
use Magento\Framework\App\State;
+use Magento\Framework\Indexer\IndexerInterfaceFactory;
use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\Framework\Setup\Patch\PatchVersionInterface;
-use Magento\Framework\Indexer\IndexerInterfaceFactory;
-use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
/**
+ * This patch sets up search weight for the product's system attributes, reindex required after patch applying.
+ *
* @deprecated
* @see \Magento\ElasticSearch
*/
class SetInitialSearchWeightForAttributes implements DataPatchInterface, PatchVersionInterface
{
+
/**
* @var IndexerInterfaceFactory
*/
@@ -50,7 +53,7 @@ public function __construct(
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function apply()
{
@@ -60,13 +63,15 @@ public function apply()
$this->state->emulateAreaCode(
\Magento\Framework\App\Area::AREA_CRONTAB,
function () use ($indexer) {
- $indexer->reindexAll();
+ $indexer->getState()
+ ->setStatus(\Magento\Framework\Indexer\StateInterface::STATUS_INVALID)
+ ->save();
}
);
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public static function getDependencies()
{
@@ -74,7 +79,7 @@ public static function getDependencies()
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public static function getVersion()
{
@@ -82,7 +87,7 @@ public static function getVersion()
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function getAliases()
{
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml
index a72762ff796e0..2022f809139ec 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontCatalogSearchActionGroup.xml
@@ -41,6 +41,27 @@
+
+
+ Fill the Storefront Search field. Submits the Form. Validates that 'Minimum Search query length' warning appears.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -51,6 +72,7 @@
+
@@ -66,7 +88,8 @@
-
+
+
@@ -101,7 +124,7 @@
-
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontFillFormAdvancedSearchActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontFillFormAdvancedSearchActionGroup.xml
new file mode 100644
index 0000000000000..1afdb6e5e46fa
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/StorefrontFillFormAdvancedSearchActionGroup.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchData.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchData.xml
index 6868456079110..9dab06ffb14f0 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchData.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Data/CatalogSearchData.xml
@@ -23,5 +23,10 @@
1
-
+
+ DefaultCatalogSearchEngine
+
+
+ true
+
\ No newline at end of file
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Data/ConfigData.xml
new file mode 100644
index 0000000000000..dd8c426592619
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Data/ConfigData.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+ catalog/search/min_query_length
+ 3
+
+
+ catalog/search/min_query_length
+ 4
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Metadata/catalog_search-meta.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Metadata/catalog_search-meta.xml
index 7405377249aa4..ce869f81a23df 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Metadata/catalog_search-meta.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Metadata/catalog_search-meta.xml
@@ -29,4 +29,15 @@
+
+
+
+
+
+ boolean
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchAdvancedResultMainSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchAdvancedResultMainSection.xml
index 6b28b4f36c6a7..eb3bc8e79d7b5 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchAdvancedResultMainSection.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchAdvancedResultMainSection.xml
@@ -17,5 +17,6 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchMainSection.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchMainSection.xml
index 667f08fea6579..b005e100b30bb 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchMainSection.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Section/StorefrontCatalogSearchMainSection.xml
@@ -16,5 +16,7 @@
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml
index 13665100f79af..0e92d9fb0c7ad 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest.xml
@@ -13,6 +13,11 @@
+
+
+
+
+
@@ -26,6 +31,11 @@
+
+
+
+
+
@@ -39,6 +49,10 @@
+
+
+
+
@@ -52,6 +66,11 @@
+
+
+
+
+
@@ -65,6 +84,11 @@
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml
index 99f3fc00a7401..aa7cf933f6328 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml
@@ -9,6 +9,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/LayerNavigationOfCatalogSearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/LayerNavigationOfCatalogSearchTest.xml
new file mode 100644
index 0000000000000..c8f84c732d6ba
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/LayerNavigationOfCatalogSearchTest.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml
index b6417e12a6db7..89269a1ad0d9e 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest.xml
index 19db201e91f40..3b60e4b09de28 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest.xml
@@ -23,6 +23,10 @@
+
+
+
+
@@ -74,6 +78,7 @@
+
@@ -87,19 +92,26 @@
+
+
+
+
-
-
+
+
+
+
+
@@ -107,15 +119,30 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -124,6 +151,7 @@
+
@@ -160,6 +188,7 @@
+
@@ -242,6 +271,7 @@
+
@@ -306,6 +336,10 @@
+
+
+
+
@@ -336,12 +370,18 @@
+
+
+
+
+
+
@@ -368,6 +408,7 @@
+
@@ -375,8 +416,13 @@
+
+
+
+
+
@@ -399,19 +445,27 @@
+
+
+
+
+
+
-
+
+
+
-
+
@@ -450,11 +504,16 @@
+
+
+
+
+
@@ -512,11 +571,18 @@
+
+
+
+
+
+
+
@@ -601,6 +667,10 @@
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByAllParametersTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByAllParametersTest.xml
new file mode 100644
index 0000000000000..9ad868ff6db7e
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByAllParametersTest.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByDescriptionTest.xml
new file mode 100644
index 0000000000000..5693721e6ed65
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByDescriptionTest.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByNameSkuDescriptionPriceTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByNameSkuDescriptionPriceTest.xml
new file mode 100644
index 0000000000000..4d3ba22f79356
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByNameSkuDescriptionPriceTest.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByNameTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByNameTest.xml
new file mode 100644
index 0000000000000..f0b81e08252fc
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByNameTest.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialNameTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialNameTest.xml
new file mode 100644
index 0000000000000..f875021bd9669
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialNameTest.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialShortDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialShortDescriptionTest.xml
new file mode 100644
index 0000000000000..0edc3f31216bb
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialShortDescriptionTest.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuAndDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuAndDescriptionTest.xml
new file mode 100644
index 0000000000000..b2b4ef9cc4782
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuAndDescriptionTest.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuTest.xml
new file mode 100644
index 0000000000000..45cec0a899361
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuTest.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceFromAndPriceToTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceFromAndPriceToTest.xml
new file mode 100644
index 0000000000000..6b85cdf61c84c
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceFromAndPriceToTest.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceToTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceToTest.xml
new file mode 100644
index 0000000000000..33dff8aefa334
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceToTest.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByShortDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByShortDescriptionTest.xml
new file mode 100644
index 0000000000000..c4622d02a5152
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByShortDescriptionTest.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchBySkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchBySkuTest.xml
new file mode 100644
index 0000000000000..ca5e237099681
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchBySkuTest.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml
new file mode 100644
index 0000000000000..14df2133017d9
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchNegativeProductSearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchNegativeProductSearchTest.xml
new file mode 100644
index 0000000000000..b4f2314295a00
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchNegativeProductSearchTest.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchWithoutEnteringDataTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchWithoutEnteringDataTest.xml
new file mode 100644
index 0000000000000..8a29ab718bd25
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchWithoutEnteringDataTest.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 7e3de7534e8c4..a79ffcc33cabe 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
@@ -129,7 +129,7 @@ protected function setUp()
->getMock();
$this->connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class)
->disableOriginalConstructor()
- ->setMethods(['select', 'getIfNullSql', 'quote'])
+ ->setMethods(['select', 'getIfNullSql', 'quote', 'quoteInto'])
->getMockForAbstractClass();
$this->select = $this->getMockBuilder(\Magento\Framework\DB\Select::class)
->disableOriginalConstructor()
@@ -222,9 +222,10 @@ public function testProcessPrice()
public function processCategoryIdsDataProvider()
{
return [
- ['5', 'category_ids_index.category_id = 5'],
- [3, 'category_ids_index.category_id = 3'],
- ["' and 1 = 0", 'category_ids_index.category_id = 0'],
+ ['5', "category_ids_index.category_id in ('5')"],
+ [3, "category_ids_index.category_id in (3)"],
+ ["' and 1 = 0", "category_ids_index.category_id in ('\' and 1 = 0')"],
+ [['5', '10'], "category_ids_index.category_id in ('5', '10')"]
];
}
@@ -251,6 +252,12 @@ public function testProcessCategoryIds($categoryId, $expectedResult)
->with(\Magento\Catalog\Model\Product::ENTITY, 'category_ids')
->will($this->returnValue($this->attribute));
+ $this->connection
+ ->expects($this->once())
+ ->method('quoteInto')
+ ->with('category_ids_index.category_id in (?)', $categoryId)
+ ->willReturn($expectedResult);
+
$actualResult = $this->target->process($this->filter, $isNegation, $query);
$this->assertSame($expectedResult, $this->removeWhitespaces($actualResult));
}
diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Plugin/StockedProductsFilterPluginTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Plugin/StockedProductsFilterPluginTest.php
new file mode 100644
index 0000000000000..b9909ec2c74b2
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Plugin/StockedProductsFilterPluginTest.php
@@ -0,0 +1,122 @@
+stockConfigurationMock = $this->getMockBuilder(StockConfigurationInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->stockStatusRepositoryMock = $this->getMockBuilder(StockStatusRepositoryInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->stockStatusCriteriaFactoryMock = $this->getMockBuilder(StockStatusCriteriaInterfaceFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->plugin = new StockedProductsFilterPlugin(
+ $this->stockConfigurationMock,
+ $this->stockStatusRepositoryMock,
+ $this->stockStatusCriteriaFactoryMock
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testBeforePrepareProductIndex(): void
+ {
+ /** @var DataProvider|\PHPUnit_Framework_MockObject_MockObject $dataProviderMock */
+ $dataProviderMock = $this->getMockBuilder(DataProvider::class)->disableOriginalConstructor()->getMock();
+ $indexData = [
+ 1 => [],
+ 2 => [],
+ ];
+ $productData = [];
+ $storeId = 1;
+
+ $this->stockConfigurationMock
+ ->expects($this->once())
+ ->method('isShowOutOfStock')
+ ->willReturn(false);
+
+ $stockStatusCriteriaMock = $this->getMockBuilder(StockStatusCriteriaInterface::class)->getMock();
+ $stockStatusCriteriaMock
+ ->expects($this->once())
+ ->method('setProductsFilter')
+ ->willReturn(true);
+ $this->stockStatusCriteriaFactoryMock
+ ->expects($this->once())
+ ->method('create')
+ ->willReturn($stockStatusCriteriaMock);
+
+ $stockStatusMock = $this->getMockBuilder(StockStatusInterface::class)->getMock();
+ $stockStatusMock->expects($this->atLeastOnce())
+ ->method('getStockStatus')
+ ->willReturnOnConsecutiveCalls(Stock::STOCK_IN_STOCK, Stock::STOCK_OUT_OF_STOCK);
+ $stockStatusCollectionMock = $this->getMockBuilder(StockStatusCollectionInterface::class)->getMock();
+ $stockStatusCollectionMock
+ ->expects($this->once())
+ ->method('getItems')
+ ->willReturn([1 => $stockStatusMock, 2 => $stockStatusMock]);
+ $this->stockStatusRepositoryMock
+ ->expects($this->once())
+ ->method('getList')
+ ->willReturn($stockStatusCollectionMock);
+
+ list ($indexData, $productData, $storeId) = $this->plugin->beforePrepareProductIndex(
+ $dataProviderMock,
+ $indexData,
+ $productData,
+ $storeId
+ );
+
+ $this->assertEquals([1], array_keys($indexData));
+ }
+}
diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/PriceTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/PriceTest.php
index abad58a6876d3..f783f75a170e3 100644
--- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/PriceTest.php
+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Layer/Filter/PriceTest.php
@@ -3,6 +3,7 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
namespace Magento\CatalogSearch\Test\Unit\Model\Layer\Filter;
@@ -208,6 +209,12 @@ public function testApply()
$priceId = '15-50';
$requestVar = 'test_request_var';
+ $this->target->setAttributeModel($this->attribute);
+ $attributeCode = 'price';
+ $this->attribute->expects($this->any())
+ ->method('getAttributeCode')
+ ->will($this->returnValue($attributeCode));
+
$this->target->setRequestVar($requestVar);
$this->request->expects($this->exactly(1))
->method('getParam')
diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php
index 683070c286239..10010188c26c9 100644
--- a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php
+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Advanced/CollectionTest.php
@@ -14,6 +14,7 @@
use Magento\CatalogSearch\Test\Unit\Model\ResourceModel\BaseCollection;
use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory;
use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory;
+use PHPUnit\Framework\MockObject\MockObject;
/**
* Tests Magento\CatalogSearch\Model\ResourceModel\Advanced\Collection
@@ -35,32 +36,37 @@ class CollectionTest extends BaseCollection
private $advancedCollection;
/**
- * @var \Magento\Framework\Api\FilterBuilder|\PHPUnit_Framework_MockObject_MockObject
+ * @var \Magento\Framework\Api\FilterBuilder|MockObject
*/
private $filterBuilder;
/**
- * @var \Magento\Framework\Api\Search\SearchCriteriaBuilder|\PHPUnit_Framework_MockObject_MockObject
+ * @var \Magento\Framework\Api\Search\SearchCriteriaBuilder|MockObject
*/
private $criteriaBuilder;
/**
- * @var \Magento\Framework\Search\Adapter\Mysql\TemporaryStorageFactory|\PHPUnit_Framework_MockObject_MockObject
+ * @var \Magento\Framework\Search\Adapter\Mysql\TemporaryStorageFactory|MockObject
*/
private $temporaryStorageFactory;
/**
- * @var \Magento\Search\Api\SearchInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var \Magento\Search\Api\SearchInterface|MockObject
*/
private $search;
/**
- * @var \Magento\Eav\Model\Config|\PHPUnit_Framework_MockObject_MockObject
+ * @var \Magento\Eav\Model\Config|MockObject
*/
private $eavConfig;
/**
- * setUp method for CollectionTest
+ * @var SearchResultApplierFactory|MockObject
+ */
+ private $searchResultApplierFactory;
+
+ /**
+ * @inheritdoc
*/
protected function setUp()
{
@@ -97,17 +103,10 @@ protected function setUp()
->method('create')
->willReturn($searchCriteriaResolver);
- $searchResultApplier = $this->getMockBuilder(SearchResultApplierInterface::class)
- ->disableOriginalConstructor()
- ->setMethods(['apply'])
- ->getMockForAbstractClass();
- $searchResultApplierFactory = $this->getMockBuilder(SearchResultApplierFactory::class)
+ $this->searchResultApplierFactory = $this->getMockBuilder(SearchResultApplierFactory::class)
->disableOriginalConstructor()
->setMethods(['create'])
->getMock();
- $searchResultApplierFactory->expects($this->any())
- ->method('create')
- ->willReturn($searchResultApplier);
$totalRecordsResolver = $this->getMockBuilder(TotalRecordsResolverInterface::class)
->disableOriginalConstructor()
@@ -134,12 +133,15 @@ protected function setUp()
'productLimitationFactory' => $productLimitationFactoryMock,
'collectionProvider' => null,
'searchCriteriaResolverFactory' => $searchCriteriaResolverFactory,
- 'searchResultApplierFactory' => $searchResultApplierFactory,
+ 'searchResultApplierFactory' => $this->searchResultApplierFactory,
'totalRecordsResolverFactory' => $totalRecordsResolverFactory
]
);
}
+ /**
+ * Test to Load data with filter in place
+ */
public function testLoadWithFilterNoFilters()
{
$this->advancedCollection->loadWithFilter();
@@ -150,6 +152,7 @@ public function testLoadWithFilterNoFilters()
*/
public function testLike()
{
+ $pageSize = 10;
$attributeCode = 'description';
$attributeCodeId = 42;
$attribute = $this->createMock(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class);
@@ -168,6 +171,23 @@ public function testLike()
$searchResult = $this->createMock(\Magento\Framework\Api\Search\SearchResultInterface::class);
$this->search->expects($this->once())->method('search')->willReturn($searchResult);
+ $this->advancedCollection->setPageSize($pageSize);
+ $this->advancedCollection->setCurPage(0);
+
+ $searchResultApplier = $this->createMock(SearchResultApplierInterface::class);
+ $this->searchResultApplierFactory->expects($this->once())
+ ->method('create')
+ ->with(
+ [
+ 'collection' => $this->advancedCollection,
+ 'searchResult' => $searchResult,
+ 'orders' => [],
+ 'size' => $pageSize,
+ 'currentPage' => 0,
+ ]
+ )
+ ->willReturn($searchResultApplier);
+
// addFieldsToFilter will load filters,
// then loadWithFilter will trigger _renderFiltersBefore code in Advanced/Collection
$this->assertSame(
@@ -177,7 +197,7 @@ public function testLike()
}
/**
- * @return \PHPUnit_Framework_MockObject_MockObject
+ * @return MockObject
*/
protected function getCriteriaBuilder()
{
@@ -185,6 +205,7 @@ protected function getCriteriaBuilder()
->setMethods(['addFilter', 'create', 'setRequestName'])
->disableOriginalConstructor()
->getMock();
+
return $criteriaBuilder;
}
}
diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php
index 9170b81dc3182..9b4010cfae453 100644
--- a/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php
+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/ResourceModel/Fulltext/CollectionTest.php
@@ -5,6 +5,7 @@
*/
namespace Magento\CatalogSearch\Test\Unit\Model\ResourceModel\Fulltext;
+use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory;
use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory;
use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface;
use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory;
@@ -12,11 +13,12 @@
use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface;
use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface;
use Magento\CatalogSearch\Test\Unit\Model\ResourceModel\BaseCollection;
+use PHPUnit\Framework\MockObject\MockObject;
use Magento\Framework\Search\Adapter\Mysql\TemporaryStorageFactory;
-use PHPUnit_Framework_MockObject_MockObject as MockObject;
-use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory;
/**
+ * Test class for Fulltext Collection
+ *
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class CollectionTest extends BaseCollection
@@ -27,12 +29,12 @@ class CollectionTest extends BaseCollection
private $objectManager;
/**
- * @var \Magento\Framework\Search\Adapter\Mysql\TemporaryStorage|\PHPUnit_Framework_MockObject_MockObject
+ * @var \Magento\Framework\Search\Adapter\Mysql\TemporaryStorage|MockObject
*/
private $temporaryStorage;
/**
- * @var \Magento\Search\Api\SearchInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var \Magento\Search\Api\SearchInterface|MockObject
*/
private $search;
@@ -61,6 +63,11 @@ class CollectionTest extends BaseCollection
*/
private $filterBuilder;
+ /**
+ * @var SearchResultApplierFactory|MockObject
+ */
+ private $searchResultApplierFactory;
+
/**
* @var \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection
*/
@@ -72,7 +79,7 @@ class CollectionTest extends BaseCollection
private $filter;
/**
- * setUp method for CollectionTest
+ * @inheritdoc
*/
protected function setUp()
{
@@ -115,17 +122,10 @@ protected function setUp()
->method('create')
->willReturn($searchCriteriaResolver);
- $searchResultApplier = $this->getMockBuilder(SearchResultApplierInterface::class)
- ->disableOriginalConstructor()
- ->setMethods(['apply'])
- ->getMockForAbstractClass();
- $searchResultApplierFactory = $this->getMockBuilder(SearchResultApplierFactory::class)
+ $this->searchResultApplierFactory = $this->getMockBuilder(SearchResultApplierFactory::class)
->disableOriginalConstructor()
->setMethods(['create'])
->getMock();
- $searchResultApplierFactory->expects($this->any())
- ->method('create')
- ->willReturn($searchResultApplier);
$totalRecordsResolver = $this->getMockBuilder(TotalRecordsResolverInterface::class)
->disableOriginalConstructor()
@@ -148,7 +148,7 @@ protected function setUp()
'temporaryStorageFactory' => $temporaryStorageFactory,
'productLimitationFactory' => $productLimitationFactoryMock,
'searchCriteriaResolverFactory' => $searchCriteriaResolverFactory,
- 'searchResultApplierFactory' => $searchResultApplierFactory,
+ 'searchResultApplierFactory' => $this->searchResultApplierFactory,
'totalRecordsResolverFactory' => $totalRecordsResolverFactory,
]
);
@@ -161,6 +161,9 @@ protected function setUp()
$this->model->setFilterBuilder($this->filterBuilder);
}
+ /**
+ * @inheritdoc
+ */
protected function tearDown()
{
$reflectionProperty = new \ReflectionProperty(\Magento\Framework\App\ObjectManager::class, '_instance');
@@ -168,16 +171,49 @@ protected function tearDown()
$reflectionProperty->setValue(null);
}
+ /**
+ * Test to Return field faceted data from faceted search result
+ */
public function testGetFacetedDataWithEmptyAggregations()
{
+ $pageSize = 10;
+
$searchResult = $this->getMockBuilder(\Magento\Framework\Api\Search\SearchResultInterface::class)
->getMockForAbstractClass();
$this->search->expects($this->once())
->method('search')
->willReturn($searchResult);
+
+ $searchResultApplier = $this->getMockBuilder(SearchResultApplierInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['apply'])
+ ->getMockForAbstractClass();
+ $this->searchResultApplierFactory->expects($this->any())
+ ->method('create')
+ ->willReturn($searchResultApplier);
+
+ $this->model->setPageSize($pageSize);
+ $this->model->setCurPage(0);
+
+ $this->searchResultApplierFactory->expects($this->once())
+ ->method('create')
+ ->with(
+ [
+ 'collection' => $this->model,
+ 'searchResult' => $searchResult,
+ 'orders' => [],
+ 'size' => $pageSize,
+ 'currentPage' => 0,
+ ]
+ )
+ ->willReturn($searchResultApplier);
+
$this->model->getFacetedData('field');
}
+ /**
+ * Test to Apply attribute filter to facet collection
+ */
public function testAddFieldToFilter()
{
$this->filter = $this->createFilter();
@@ -220,6 +256,7 @@ protected function getCriteriaBuilder()
protected function getFilterBuilder()
{
$filterBuilder = $this->createMock(\Magento\Framework\Api\FilterBuilder::class);
+
return $filterBuilder;
}
@@ -241,6 +278,7 @@ protected function addFiltersToFilterBuilder(MockObject $filterBuilder, array $f
->with($value)
->willReturnSelf();
}
+
return $filterBuilder;
}
@@ -252,6 +290,7 @@ protected function createFilter()
$filter = $this->getMockBuilder(\Magento\Framework\Api\Filter::class)
->disableOriginalConstructor()
->getMock();
+
return $filter;
}
}
diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/DecimalTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/DecimalTest.php
index 8157c1fa8fa82..350344372612a 100644
--- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/DecimalTest.php
+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/DecimalTest.php
@@ -3,6 +3,7 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
namespace Magento\CatalogSearch\Test\Unit\Model\Search\RequestGenerator;
@@ -11,6 +12,9 @@
use Magento\Framework\Search\Request\BucketInterface;
use Magento\Framework\Search\Request\FilterInterface;
+/**
+ * Test catalog search range request generator.
+ */
class DecimalTest extends \PHPUnit\Framework\TestCase
{
/** @var Decimal */
diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/PriceTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/PriceTest.php
new file mode 100644
index 0000000000000..3635430197591
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Search/RequestGenerator/PriceTest.php
@@ -0,0 +1,82 @@
+attribute = $this->getMockBuilder(Attribute::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getAttributeCode'])
+ ->getMockForAbstractClass();
+ $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->setMethods(['getValue'])
+ ->getMockForAbstractClass();
+ $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+ $this->price = $objectManager->getObject(
+ Price::class,
+ ['scopeConfig' => $this->scopeConfigMock]
+ );
+ }
+
+ public function testGetFilterData()
+ {
+ $filterName = 'test_filter_name';
+ $attributeCode = 'test_attribute_code';
+ $expected = [
+ 'type' => FilterInterface::TYPE_RANGE,
+ 'name' => $filterName,
+ 'field' => $attributeCode,
+ 'from' => '$' . $attributeCode . '.from$',
+ 'to' => '$' . $attributeCode . '.to$',
+ ];
+ $this->attribute->expects($this->atLeastOnce())
+ ->method('getAttributeCode')
+ ->willReturn($attributeCode);
+ $actual = $this->price->getFilterData($this->attribute, $filterName);
+ $this->assertEquals($expected, $actual);
+ }
+
+ public function testGetAggregationData()
+ {
+ $bucketName = 'test_bucket_name';
+ $attributeCode = 'test_attribute_code';
+ $method = 'price_dynamic_algorithm';
+ $expected = [
+ 'type' => BucketInterface::TYPE_DYNAMIC,
+ 'name' => $bucketName,
+ 'field' => $attributeCode,
+ 'method' => '$'. $method . '$',
+ 'metric' => [['type' => 'count']],
+ ];
+ $this->attribute->expects($this->atLeastOnce())
+ ->method('getAttributeCode')
+ ->willReturn($attributeCode);
+ $actual = $this->price->getAggregationData($this->attribute, $bucketName);
+ $this->assertEquals($expected, $actual);
+ }
+}
diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Ui/DataProvider/Product/AddFulltextFilterToCollectionTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Ui/DataProvider/Product/AddFulltextFilterToCollectionTest.php
new file mode 100644
index 0000000000000..881c843ecf92b
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Unit/Ui/DataProvider/Product/AddFulltextFilterToCollectionTest.php
@@ -0,0 +1,74 @@
+objectManager = new ObjectManagerHelper($this);
+
+ $this->searchCollection = $this->getMockBuilder(SearchCollection::class)
+ ->setMethods(['addBackendSearchFilter', 'load', 'getAllIds'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->searchCollection->expects($this->any())
+ ->method('load')
+ ->willReturnSelf();
+ $this->collection = $this->getMockBuilder(Collection::class)
+ ->setMethods(['addIdFilter'])
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->model = $this->objectManager->getObject(
+ AddFulltextFilterToCollection::class,
+ [
+ 'searchCollection' => $this->searchCollection
+ ]
+ );
+ }
+
+ public function testAddFilter()
+ {
+ $this->searchCollection->expects($this->once())
+ ->method('addBackendSearchFilter')
+ ->with('test');
+ $this->searchCollection->expects($this->once())
+ ->method('getAllIds')
+ ->willReturn([]);
+ $this->collection->expects($this->once())
+ ->method('addIdFilter')
+ ->with(-1);
+ $this->model->addFilter($this->collection, 'test', ['fulltext' => 'test']);
+ }
+}
diff --git a/app/code/Magento/CatalogSearch/Ui/DataProvider/Product/AddFulltextFilterToCollection.php b/app/code/Magento/CatalogSearch/Ui/DataProvider/Product/AddFulltextFilterToCollection.php
index f312178e0bf0b..af5020a2f8c94 100644
--- a/app/code/Magento/CatalogSearch/Ui/DataProvider/Product/AddFulltextFilterToCollection.php
+++ b/app/code/Magento/CatalogSearch/Ui/DataProvider/Product/AddFulltextFilterToCollection.php
@@ -40,6 +40,10 @@ public function addFilter(Collection $collection, $field, $condition = null)
if (isset($condition['fulltext']) && (string)$condition['fulltext'] !== '') {
$this->searchCollection->addBackendSearchFilter($condition['fulltext']);
$productIds = $this->searchCollection->load()->getAllIds();
+ if (empty($productIds)) {
+ //add dummy id to prevent returning full unfiltered collection
+ $productIds = -1;
+ }
$collection->addIdFilter($productIds);
}
}
diff --git a/app/code/Magento/CatalogSearch/etc/di.xml b/app/code/Magento/CatalogSearch/etc/di.xml
index 28d5035308dee..4e5b38878ee52 100644
--- a/app/code/Magento/CatalogSearch/etc/di.xml
+++ b/app/code/Magento/CatalogSearch/etc/di.xml
@@ -75,6 +75,9 @@
+
+
+
@@ -281,6 +284,7 @@
\Magento\CatalogSearch\Model\Search\RequestGenerator\General
- Magento\CatalogSearch\Model\Search\RequestGenerator\Decimal
+ - Magento\CatalogSearch\Model\Search\RequestGenerator\Price
@@ -373,4 +377,7 @@
+
+
+
diff --git a/app/code/Magento/CatalogSearch/view/frontend/templates/result.phtml b/app/code/Magento/CatalogSearch/view/frontend/templates/result.phtml
index c63e6ff4abe0f..32b26eec9dbe6 100644
--- a/app/code/Magento/CatalogSearch/view/frontend/templates/result.phtml
+++ b/app/code/Magento/CatalogSearch/view/frontend/templates/result.phtml
@@ -3,6 +3,9 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
+/** This changes need to valid applying filters and configuration before search process is started. */
+$productList = $block->getProductListHtml();
?>
getResultCount()) : ?>
= /* @noEscape */ $block->getChildHtml('tagged_product_list_rss_link') ?>
@@ -16,7 +19,7 @@
- = $block->getProductListHtml() ?>
+ = /* @noEscape */ $productList ?>
diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Product/AnchorUrlRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/Product/AnchorUrlRewriteGenerator.php
index 4a191b54dea68..5d08ea33ff8a1 100644
--- a/app/code/Magento/CatalogUrlRewrite/Model/Product/AnchorUrlRewriteGenerator.php
+++ b/app/code/Magento/CatalogUrlRewrite/Model/Product/AnchorUrlRewriteGenerator.php
@@ -6,6 +6,7 @@
namespace Magento\CatalogUrlRewrite\Model\Product;
use Magento\Catalog\Api\CategoryRepositoryInterface;
+use Magento\Catalog\Model\Category;
use Magento\Catalog\Model\Product;
use Magento\CatalogUrlRewrite\Model\ObjectRegistry;
use Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator;
@@ -67,6 +68,9 @@ public function generate($storeId, Product $product, ObjectRegistry $productCate
if ($anchorCategoryIds) {
foreach ($anchorCategoryIds as $anchorCategoryId) {
$anchorCategory = $this->categoryRepository->get($anchorCategoryId);
+ if ((int)$anchorCategory->getParentId() === Category::TREE_ROOT_ID) {
+ continue;
+ }
$urls[] = $this->urlRewriteFactory->create()
->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE)
->setEntityId($product->getId())
diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Storage/DynamicStorage.php b/app/code/Magento/CatalogUrlRewrite/Model/Storage/DynamicStorage.php
index edca633fb14cc..d9e9705ac039d 100644
--- a/app/code/Magento/CatalogUrlRewrite/Model/Storage/DynamicStorage.php
+++ b/app/code/Magento/CatalogUrlRewrite/Model/Storage/DynamicStorage.php
@@ -148,7 +148,7 @@ private function getCategoryUrlSuffix($storeId = null): string
CategoryUrlPathGenerator::XML_PATH_CATEGORY_URL_SUFFIX,
ScopeInterface::SCOPE_STORE,
$storeId
- );
+ ) ?? '';
}
/**
diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php
index 704b60a8aaf2a..b1dfa79373a05 100644
--- a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php
+++ b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php
@@ -23,7 +23,6 @@
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\Exception\LocalizedException;
-use Magento\Framework\Exception\NoSuchEntityException;
use Magento\ImportExport\Model\Import as ImportExport;
use Magento\Store\Model\Store;
use Magento\Store\Model\StoreManagerInterface;
@@ -252,7 +251,7 @@ public function execute(Observer $observer)
* @throws LocalizedException
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
- protected function _populateForUrlGeneration($rowData)
+ private function _populateForUrlGeneration($rowData)
{
$newSku = $this->import->getNewSku($rowData[ImportProduct::COL_SKU]);
$oldSku = $this->import->getOldSku();
@@ -321,7 +320,7 @@ private function isNeedToPopulateForUrlGeneration($rowData, $newSku, $oldSku): b
* @param array $rowData
* @return void
*/
- protected function setStoreToProduct(Product $product, array $rowData)
+ private function setStoreToProduct(Product $product, array $rowData)
{
if (!empty($rowData[ImportProduct::COL_STORE])
&& ($storeId = $this->import->getStoreIdByCode($rowData[ImportProduct::COL_STORE]))
@@ -339,7 +338,7 @@ protected function setStoreToProduct(Product $product, array $rowData)
* @param string $storeId
* @return $this
*/
- protected function addProductToImport($product, $storeId)
+ private function addProductToImport($product, $storeId)
{
if ($product->getVisibility() == (string)Visibility::getOptionArray()[Visibility::VISIBILITY_NOT_VISIBLE]) {
return $this;
@@ -357,7 +356,7 @@ protected function addProductToImport($product, $storeId)
* @param Product $product
* @return $this
*/
- protected function populateGlobalProduct($product)
+ private function populateGlobalProduct($product)
{
foreach ($this->import->getProductWebsites($product->getSku()) as $websiteId) {
foreach ($this->websitesToStoreIds[$websiteId] as $storeId) {
@@ -376,7 +375,7 @@ protected function populateGlobalProduct($product)
* @return UrlRewrite[]
* @throws LocalizedException
*/
- protected function generateUrls()
+ private function generateUrls()
{
$mergeDataProvider = clone $this->mergeDataProviderPrototype;
$mergeDataProvider->merge($this->canonicalUrlRewriteGenerate());
@@ -398,7 +397,7 @@ protected function generateUrls()
* @param int|null $storeId
* @return bool
*/
- protected function isGlobalScope($storeId)
+ private function isGlobalScope($storeId)
{
return null === $storeId || $storeId == Store::DEFAULT_STORE_ID;
}
@@ -408,7 +407,7 @@ protected function isGlobalScope($storeId)
*
* @return UrlRewrite[]
*/
- protected function canonicalUrlRewriteGenerate()
+ private function canonicalUrlRewriteGenerate()
{
$urls = [];
foreach ($this->products as $productId => $productsByStores) {
@@ -433,7 +432,7 @@ protected function canonicalUrlRewriteGenerate()
* @return UrlRewrite[]
* @throws LocalizedException
*/
- protected function categoriesUrlRewriteGenerate()
+ private function categoriesUrlRewriteGenerate(): array
{
$urls = [];
foreach ($this->products as $productId => $productsByStores) {
@@ -444,17 +443,24 @@ protected function categoriesUrlRewriteGenerate()
continue;
}
$requestPath = $this->productUrlPathGenerator->getUrlPathWithSuffix($product, $storeId, $category);
- $urls[] = $this->urlRewriteFactory->create()
- ->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE)
- ->setEntityId($productId)
- ->setRequestPath($requestPath)
- ->setTargetPath($this->productUrlPathGenerator->getCanonicalUrlPath($product, $category))
- ->setStoreId($storeId)
- ->setMetadata(['category_id' => $category->getId()]);
+ $urls[] = [
+ $this->urlRewriteFactory->create()
+ ->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE)
+ ->setEntityId($productId)
+ ->setRequestPath($requestPath)
+ ->setTargetPath($this->productUrlPathGenerator->getCanonicalUrlPath($product, $category))
+ ->setStoreId($storeId)
+ ->setMetadata(['category_id' => $category->getId()])
+ ];
+ $parentCategoryIds = $category->getAnchorsAbove();
+ if ($parentCategoryIds) {
+ $urls[] = $this->getParentCategoriesUrlRewrites($parentCategoryIds, $storeId, $product);
+ }
}
}
}
- return $urls;
+ $result = !empty($urls) ? array_merge(...$urls) : [];
+ return $result;
}
/**
@@ -462,7 +468,7 @@ protected function categoriesUrlRewriteGenerate()
*
* @return UrlRewrite[]
*/
- protected function currentUrlRewritesRegenerate()
+ private function currentUrlRewritesRegenerate()
{
$currentUrlRewrites = $this->urlFinder->findAllByData(
[
@@ -496,7 +502,7 @@ protected function currentUrlRewritesRegenerate()
* @param Category $category
* @return array
*/
- protected function generateForAutogenerated($url, $category)
+ private function generateForAutogenerated($url, $category)
{
$storeId = $url->getStoreId();
$productId = $url->getEntityId();
@@ -532,7 +538,7 @@ protected function generateForAutogenerated($url, $category)
* @param Category $category
* @return array
*/
- protected function generateForCustom($url, $category)
+ private function generateForCustom($url, $category)
{
$storeId = $url->getStoreId();
$productId = $url->getEntityId();
@@ -566,7 +572,7 @@ protected function generateForCustom($url, $category)
* @param UrlRewrite $url
* @return Category|null|bool
*/
- protected function retrieveCategoryFromMetadata($url)
+ private function retrieveCategoryFromMetadata($url)
{
$metadata = $url->getMetadata();
if (isset($metadata['category_id'])) {
@@ -576,32 +582,6 @@ protected function retrieveCategoryFromMetadata($url)
return null;
}
- /**
- * Check, category suited for url-rewrite generation.
- *
- * @param Category $category
- * @param int $storeId
- * @return bool
- * @throws NoSuchEntityException
- */
- protected function isCategoryProperForGenerating($category, $storeId)
- {
- if (isset($this->acceptableCategories[$storeId]) &&
- isset($this->acceptableCategories[$storeId][$category->getId()])) {
- return $this->acceptableCategories[$storeId][$category->getId()];
- }
- $acceptable = false;
- if ($category->getParentId() != Category::TREE_ROOT_ID) {
- list(, $rootCategoryId) = $category->getParentIds();
- $acceptable = ($rootCategoryId == $this->storeManager->getStore($storeId)->getRootCategoryId());
- }
- if (!isset($this->acceptableCategories[$storeId])) {
- $this->acceptableCategories[$storeId] = [];
- }
- $this->acceptableCategories[$storeId][$category->getId()] = $acceptable;
- return $acceptable;
- }
-
/**
* Get category by id considering store scope.
*
@@ -635,4 +615,36 @@ private function isCategoryRewritesEnabled()
{
return (bool)$this->scopeConfig->getValue('catalog/seo/generate_category_product_rewrites');
}
+
+ /**
+ * Generate url-rewrite for anchor parent-categories.
+ *
+ * @param array $categoryIds
+ * @param int $storeId
+ * @param Product $product
+ * @return array
+ * @throws LocalizedException
+ */
+ private function getParentCategoriesUrlRewrites(array $categoryIds, int $storeId, Product $product): array
+ {
+ $urls = [];
+ foreach ($categoryIds as $categoryId) {
+ $category = $this->getCategoryById($categoryId, $storeId);
+ if ($category->getParentId() == Category::TREE_ROOT_ID) {
+ continue;
+ }
+ $requestPath = $this->productUrlPathGenerator
+ ->getUrlPathWithSuffix($product, $storeId, $category);
+ $targetPath = $this->productUrlPathGenerator
+ ->getCanonicalUrlPath($product, $category);
+ $urls[] = $this->urlRewriteFactory->create()
+ ->setEntityType(ProductUrlRewriteGenerator::ENTITY_TYPE)
+ ->setEntityId($product->getId())
+ ->setRequestPath($requestPath)
+ ->setTargetPath($targetPath)
+ ->setStoreId($storeId)
+ ->setMetadata(['category_id' => $category->getId()]);
+ }
+ return $urls;
+ }
}
diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php
index 7f987124040fd..0d0b0fb995706 100644
--- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php
+++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryUrlPathAutogeneratorObserver.php
@@ -3,6 +3,8 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\CatalogUrlRewrite\Observer;
use Magento\Catalog\Model\Category;
@@ -18,6 +20,14 @@
*/
class CategoryUrlPathAutogeneratorObserver implements ObserverInterface
{
+
+ /**
+ * Reserved endpoint names.
+ *
+ * @var string[]
+ */
+ private $invalidValues = [];
+
/**
* @var \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator
*/
@@ -38,22 +48,34 @@ class CategoryUrlPathAutogeneratorObserver implements ObserverInterface
*/
private $categoryRepository;
+ /**
+ * @var \Magento\Backend\App\Area\FrontNameResolver
+ */
+ private $frontNameResolver;
+
/**
* @param CategoryUrlPathGenerator $categoryUrlPathGenerator
* @param ChildrenCategoriesProvider $childrenCategoriesProvider
* @param \Magento\CatalogUrlRewrite\Service\V1\StoreViewService $storeViewService
* @param CategoryRepositoryInterface $categoryRepository
+ * @param \Magento\Backend\App\Area\FrontNameResolver $frontNameResolver
+ * @param string[] $invalidValues
*/
public function __construct(
CategoryUrlPathGenerator $categoryUrlPathGenerator,
ChildrenCategoriesProvider $childrenCategoriesProvider,
StoreViewService $storeViewService,
- CategoryRepositoryInterface $categoryRepository
+ CategoryRepositoryInterface $categoryRepository,
+ \Magento\Backend\App\Area\FrontNameResolver $frontNameResolver = null,
+ array $invalidValues = []
) {
$this->categoryUrlPathGenerator = $categoryUrlPathGenerator;
$this->childrenCategoriesProvider = $childrenCategoriesProvider;
$this->storeViewService = $storeViewService;
$this->categoryRepository = $categoryRepository;
+ $this->frontNameResolver = $frontNameResolver ?: \Magento\Framework\App\ObjectManager::getInstance()
+ ->get(\Magento\Backend\App\Area\FrontNameResolver::class);
+ $this->invalidValues = $invalidValues;
}
/**
@@ -72,7 +94,7 @@ public function execute(\Magento\Framework\Event\Observer $observer)
$resultUrlKey = $this->categoryUrlPathGenerator->getUrlKey($category);
$this->updateUrlKey($category, $resultUrlKey);
} elseif ($useDefaultAttribute) {
- if (!$category->isObjectNew()) {
+ if (!$category->isObjectNew() && $category->getStoreId() === Store::DEFAULT_STORE_ID) {
$resultUrlKey = $category->formatUrlKey($category->getOrigData('name'));
$this->updateUrlKey($category, $resultUrlKey);
}
@@ -93,6 +115,17 @@ private function updateUrlKey($category, $urlKey)
if (empty($urlKey)) {
throw new \Magento\Framework\Exception\LocalizedException(__('Invalid URL key'));
}
+
+ if (in_array($urlKey, $this->getInvalidValues())) {
+ throw new \Magento\Framework\Exception\LocalizedException(
+ __(
+ 'URL key "%1" matches a reserved endpoint name (%2). Use another URL key.',
+ $urlKey,
+ implode(', ', $this->getInvalidValues())
+ )
+ );
+ }
+
$category->setUrlKey($urlKey)
->setUrlPath($this->categoryUrlPathGenerator->getUrlPath($category));
if (!$category->isObjectNew()) {
@@ -103,6 +136,16 @@ private function updateUrlKey($category, $urlKey)
}
}
+ /**
+ * Get reserved endpoint names.
+ *
+ * @return array
+ */
+ private function getInvalidValues()
+ {
+ return array_unique(array_merge($this->invalidValues, [$this->frontNameResolver->getFrontName()]));
+ }
+
/**
* Update url path for children category.
*
diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php
index 083b39d621f2a..d1e78897e3269 100644
--- a/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php
+++ b/app/code/Magento/CatalogUrlRewrite/Observer/UrlRewriteHandler.php
@@ -7,8 +7,8 @@
namespace Magento\CatalogUrlRewrite\Observer;
-use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\Category;
+use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\ResourceModel\Product\Collection;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider;
@@ -118,13 +118,15 @@ public function __construct(
$this->productCollectionFactory = $productCollectionFactory;
$this->categoryBasedProductRewriteGenerator = $categoryBasedProductRewriteGenerator;
- $objectManager = ObjectManager::getInstance();
- $mergeDataProviderFactory = $mergeDataProviderFactory ?: $objectManager->get(MergeDataProviderFactory::class);
+ $mergeDataProviderFactory = $mergeDataProviderFactory
+ ?? ObjectManager::getInstance()->get(MergeDataProviderFactory::class);
$this->mergeDataProviderPrototype = $mergeDataProviderFactory->create();
- $this->serializer = $serializer ?: $objectManager->get(Json::class);
+ $this->serializer = $serializer
+ ?? ObjectManager::getInstance()->get(Json::class);
$this->productScopeRewriteGenerator = $productScopeRewriteGenerator
- ?: $objectManager->get(ProductScopeRewriteGenerator::class);
- $this->scopeConfig = $scopeConfig ?? $objectManager->get(ScopeConfigInterface::class);
+ ?? ObjectManager::getInstance()->get(ProductScopeRewriteGenerator::class);
+ $this->scopeConfig = $scopeConfig
+ ?? ObjectManager::getInstance()->get(ScopeConfigInterface::class);
}
/**
@@ -207,18 +209,14 @@ public function deleteCategoryRewritesForChildren(Category $category)
foreach ($categoryIds as $categoryId) {
$this->urlPersist->deleteByData(
[
- UrlRewrite::ENTITY_ID =>
- $categoryId,
- UrlRewrite::ENTITY_TYPE =>
- CategoryUrlRewriteGenerator::ENTITY_TYPE,
+ UrlRewrite::ENTITY_ID => $categoryId,
+ UrlRewrite::ENTITY_TYPE => CategoryUrlRewriteGenerator::ENTITY_TYPE,
]
);
$this->urlPersist->deleteByData(
[
- UrlRewrite::METADATA =>
- $this->serializer->serialize(['category_id' => $categoryId]),
- UrlRewrite::ENTITY_TYPE =>
- ProductUrlRewriteGenerator::ENTITY_TYPE,
+ UrlRewrite::METADATA => $this->serializer->serialize(['category_id' => $categoryId]),
+ UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE,
]
);
}
@@ -252,7 +250,7 @@ private function getCategoryProductsUrlRewrites(
->addAttributeToSelect('url_key')
->addAttributeToSelect('url_path');
- foreach ($productCollection as $product) {
+ foreach ($this->getProducts($productCollection) as $product) {
if (isset($this->isSkippedProduct[$category->getEntityId()]) &&
in_array($product->getId(), $this->isSkippedProduct[$category->getEntityId()])
) {
@@ -270,6 +268,27 @@ private function getCategoryProductsUrlRewrites(
return $mergeDataProvider->getData();
}
+ /**
+ * Get products from provided collection
+ *
+ * @param Collection $collection
+ * @return \Generator|Product[]
+ */
+ private function getProducts(Collection $collection): \Generator
+ {
+ $collection->setPageSize(1000);
+ $pageCount = $collection->getLastPageNumber();
+ $currentPage = 1;
+ while ($currentPage <= $pageCount) {
+ $collection->setCurPage($currentPage);
+ foreach ($collection as $key => $product) {
+ yield $key => $product;
+ }
+ $collection->clear();
+ $currentPage++;
+ }
+ }
+
/**
* Generates product URL rewrites.
*
diff --git a/app/code/Magento/CatalogUrlRewrite/Setup/Patch/Data/UpdateUrlKeySearchable.php b/app/code/Magento/CatalogUrlRewrite/Setup/Patch/Data/UpdateUrlKeySearchable.php
new file mode 100644
index 0000000000000..75f88a8573069
--- /dev/null
+++ b/app/code/Magento/CatalogUrlRewrite/Setup/Patch/Data/UpdateUrlKeySearchable.php
@@ -0,0 +1,79 @@
+moduleDataSetup = $moduleDataSetup;
+ $this->categorySetupFactory = $categorySetupFactory;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function apply()
+ {
+ /** @var CategorySetup $categorySetup */
+ $categorySetup = $this->categorySetupFactory->create(['setup' => $this->moduleDataSetup]);
+
+ $categorySetup->updateAttribute(
+ \Magento\Catalog\Model\Product::ENTITY,
+ 'url_key',
+ 'is_searchable',
+ true
+ );
+
+ $categorySetup->updateAttribute(
+ \Magento\Catalog\Model\Category::ENTITY,
+ 'url_key',
+ 'is_searchable',
+ true
+ );
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public static function getDependencies()
+ {
+ return [CreateUrlAttributes::class];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getAliases()
+ {
+ return [];
+ }
+}
diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Data/AdminCategoryRestrictedUrlMessageData.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Data/AdminCategoryRestrictedUrlMessageData.xml
new file mode 100644
index 0000000000000..b463b0524d5ff
--- /dev/null
+++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Data/AdminCategoryRestrictedUrlMessageData.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+ URL key "admin" matches a reserved endpoint name (admin, soap, rest, graphql, standard). Use another URL key.
+ URL key "soap" matches a reserved endpoint name (admin, soap, rest, graphql, standard). Use another URL key.
+ URL key "rest" matches a reserved endpoint name (admin, soap, rest, graphql, standard). Use another URL key.
+ URL key "graphql" matches a reserved endpoint name (admin, soap, rest, graphql, standard). Use another URL key.
+
+
diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminCategoryWithRestrictedUrlKeyNotCreatedTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminCategoryWithRestrictedUrlKeyNotCreatedTest.xml
new file mode 100644
index 0000000000000..58b489da5082b
--- /dev/null
+++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminCategoryWithRestrictedUrlKeyNotCreatedTest.xml
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Product/AnchorUrlRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Product/AnchorUrlRewriteGeneratorTest.php
new file mode 100644
index 0000000000000..662e156b8f100
--- /dev/null
+++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/Product/AnchorUrlRewriteGeneratorTest.php
@@ -0,0 +1,140 @@
+urlRewriteFactory = $this->getMockBuilder(\Magento\UrlRewrite\Service\V1\Data\UrlRewriteFactory::class)
+ ->setMethods(['create'])
+ ->disableOriginalConstructor()->getMock();
+ $this->urlRewrite = $this->getMockBuilder(\Magento\UrlRewrite\Service\V1\Data\UrlRewrite::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->categoryRepositoryInterface = $this->getMockBuilder(
+ \Magento\Catalog\Api\CategoryRepositoryInterface::class
+ )->disableOriginalConstructor()->getMock();
+ $this->categoryRegistry = $this->getMockBuilder(\Magento\CatalogUrlRewrite\Model\ObjectRegistry::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->productUrlPathGenerator = $this->getMockBuilder(
+ \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator::class
+ )->disableOriginalConstructor()->getMock();
+ $this->anchorUrlRewriteGenerator = (new ObjectManager($this))->getObject(
+ \Magento\CatalogUrlRewrite\Model\Product\AnchorUrlRewriteGenerator::class,
+ [
+ 'productUrlPathGenerator' => $this->productUrlPathGenerator,
+ 'urlRewriteFactory' => $this->urlRewriteFactory,
+ 'categoryRepository' => $this->categoryRepositoryInterface
+ ]
+ );
+ }
+
+ public function testGenerateEmpty()
+ {
+ $this->categoryRegistry->expects($this->any())->method('getList')->will($this->returnValue([]));
+
+ $this->assertEquals(
+ [],
+ $this->anchorUrlRewriteGenerator->generate(1, $this->product, $this->categoryRegistry)
+ );
+ }
+
+ public function testGenerateCategories()
+ {
+ $urlPathWithCategory = 'category1/category2/category3/simple-product.html';
+ $storeId = 10;
+ $productId = 12;
+ $canonicalUrlPathWithCategory = 'canonical-path-with-category';
+ $categoryParentId = '1';
+ $categoryIds = [$categoryParentId,'2','3','4'];
+ $urls = ['category1/simple-product.html',
+ 'category1/category2/simple-product.html',
+ 'category1/category2/category3/simple-product.html'];
+
+ $this->product->expects($this->any())->method('getId')->will($this->returnValue($productId));
+ $this->productUrlPathGenerator->expects($this->any())->method('getUrlPathWithSuffix')
+ ->will($this->returnValue($urlPathWithCategory));
+ $this->productUrlPathGenerator->expects($this->any())->method('getCanonicalUrlPath')
+ ->will($this->returnValue($canonicalUrlPathWithCategory));
+ $category = $this->createMock(\Magento\Catalog\Model\Category::class);
+ $category->expects($this->any())->method('getId')->will($this->returnValue($categoryIds));
+ $category->expects($this->any())->method('getAnchorsAbove')->will($this->returnValue($categoryIds));
+ $category->expects($this->any())->method('getParentId')->will(
+ $this->onConsecutiveCalls(
+ $categoryIds[0],
+ $categoryIds[1],
+ $categoryIds[2],
+ $categoryIds[3]
+ )
+ );
+ $this->categoryRepositoryInterface
+ ->expects($this->any())
+ ->method('get')
+ ->withConsecutive(
+ [ 'category_id' => $categoryIds[0]],
+ [ 'category_id' => $categoryIds[1]],
+ [ 'category_id' => $categoryIds[2]]
+ )
+ ->will($this->returnValue($category));
+ $this->categoryRegistry->expects($this->any())->method('getList')
+ ->will($this->returnValue([$category]));
+ $this->urlRewrite->expects($this->any())->method('setStoreId')
+ ->with($storeId)
+ ->will($this->returnSelf());
+ $this->urlRewrite->expects($this->any())->method('setEntityId')
+ ->with($productId)
+ ->will($this->returnSelf());
+ $this->urlRewrite->expects($this->any())->method('setEntityType')
+ ->with(ProductUrlRewriteGenerator::ENTITY_TYPE)
+ ->will($this->returnSelf());
+ $this->urlRewrite->expects($this->any())->method('setRequestPath')
+ ->will($this->returnSelf());
+ $this->urlRewrite->expects($this->any())->method('setTargetPath')
+ ->will($this->returnSelf());
+ $this->urlRewrite->expects($this->any())->method('setMetadata')
+ ->will(
+ $this->onConsecutiveCalls(
+ $urls[0],
+ $urls[1],
+ $urls[2]
+ )
+ );
+ $this->urlRewriteFactory->expects($this->any())->method('create')->will(
+ $this->returnValue($this->urlRewrite)
+ );
+
+ $this->assertEquals(
+ $urls,
+ $this->anchorUrlRewriteGenerator->generate($storeId, $this->product, $this->categoryRegistry)
+ );
+ }
+}
diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php
index 3984d949332d3..94fe6ae8c54dc 100644
--- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php
+++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/AfterImportDataObserverTest.php
@@ -153,24 +153,32 @@ class AfterImportDataObserverTest extends \PHPUnit\Framework\TestCase
*/
protected function setUp()
{
- $this->importProduct = $this->createPartialMock(\Magento\CatalogImportExport\Model\Import\Product::class, [
+ $this->importProduct = $this->createPartialMock(
+ \Magento\CatalogImportExport\Model\Import\Product::class,
+ [
'getNewSku',
'getProductCategories',
'getProductWebsites',
'getStoreIdByCode',
'getCategoryProcessor',
- ]);
- $this->catalogProductFactory = $this->createPartialMock(\Magento\Catalog\Model\ProductFactory::class, [
+ ]
+ );
+ $this->catalogProductFactory = $this->createPartialMock(
+ \Magento\Catalog\Model\ProductFactory::class,
+ [
'create',
- ]);
+ ]
+ );
$this->storeManager = $this
->getMockBuilder(
\Magento\Store\Model\StoreManagerInterface::class
)
->disableOriginalConstructor()
- ->setMethods([
- 'getWebsite',
- ])
+ ->setMethods(
+ [
+ 'getWebsite',
+ ]
+ )
->getMockForAbstractClass();
$this->event = $this->createPartialMock(\Magento\Framework\Event::class, ['getAdapter', 'getBunch']);
$this->event->expects($this->any())->method('getAdapter')->willReturn($this->importProduct);
@@ -202,9 +210,11 @@ protected function setUp()
);
$this->urlFinder = $this
->getMockBuilder(\Magento\UrlRewrite\Model\UrlFinderInterface::class)
- ->setMethods([
- 'findAllByData',
- ])
+ ->setMethods(
+ [
+ 'findAllByData',
+ ]
+ )
->disableOriginalConstructor()
->getMockForAbstractClass();
@@ -269,9 +279,12 @@ public function testAfterImportData()
$newSku = [['entity_id' => 'value'], ['entity_id' => 'value3']];
$websiteId = 'websiteId value';
$productsCount = count($this->products);
- $websiteMock = $this->createPartialMock(\Magento\Store\Model\Website::class, [
+ $websiteMock = $this->createPartialMock(
+ \Magento\Store\Model\Website::class,
+ [
'getStoreIds',
- ]);
+ ]
+ );
$storeIds = [1, Store::DEFAULT_STORE_ID];
$websiteMock
->expects($this->once())
@@ -315,13 +328,16 @@ public function testAfterImportData()
->expects($this->exactly(1))
->method('getStoreIdByCode')
->will($this->returnValueMap($map));
- $product = $this->createPartialMock(\Magento\Catalog\Model\Product::class, [
+ $product = $this->createPartialMock(
+ \Magento\Catalog\Model\Product::class,
+ [
'getId',
'setId',
'getSku',
'setStoreId',
'getStoreId',
- ]);
+ ]
+ );
$product
->expects($this->exactly($productsCount))
->method('setId')
@@ -341,17 +357,21 @@ public function testAfterImportData()
$product
->expects($this->exactly($productsCount))
->method('getSku')
- ->will($this->onConsecutiveCalls(
- $this->products[0]['sku'],
- $this->products[1]['sku']
- ));
+ ->will(
+ $this->onConsecutiveCalls(
+ $this->products[0]['sku'],
+ $this->products[1]['sku']
+ )
+ );
$product
->expects($this->exactly($productsCount))
->method('getStoreId')
- ->will($this->onConsecutiveCalls(
- $this->products[0][ImportProduct::COL_STORE],
- $this->products[1][ImportProduct::COL_STORE]
- ));
+ ->will(
+ $this->onConsecutiveCalls(
+ $this->products[0][ImportProduct::COL_STORE],
+ $this->products[1][ImportProduct::COL_STORE]
+ )
+ );
$product
->expects($this->exactly($productsCount))
->method('setStoreId')
@@ -540,7 +560,10 @@ public function testCategoriesUrlRewriteGenerate()
->expects($this->any())
->method('getId')
->will($this->returnValue($this->categoryId));
-
+ $category
+ ->expects($this->any())
+ ->method('getAnchorsAbove')
+ ->willReturn([]);
$categoryCollection = $this->getMockBuilder(CategoryCollection::class)
->disableOriginalConstructor()
->getMock();
diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php
index 0a570adab309a..9e2090e36f08e 100644
--- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php
+++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryUrlPathAutogeneratorObserverTest.php
@@ -159,6 +159,9 @@ public function testShouldThrowExceptionIfUrlKeyIsEmpty($useDefaultUrlKey, $isOb
$this->expectExceptionMessage('Invalid URL key');
$categoryData = ['use_default' => ['url_key' => $useDefaultUrlKey], 'url_key' => '', 'url_path' => ''];
$this->category->setData($categoryData);
+ $this->category
+ ->method('getStoreId')
+ ->willReturn(\Magento\Store\Model\Store::DEFAULT_STORE_ID);
$this->category->isObjectNew($isObjectNew);
$this->assertEquals($isObjectNew, $this->category->isObjectNew());
$this->assertEquals($categoryData['url_key'], $this->category->getUrlKey());
diff --git a/app/code/Magento/CatalogUrlRewrite/Ui/DataProvider/Product/Form/Modifier/ProductUrlRewrite.php b/app/code/Magento/CatalogUrlRewrite/Ui/DataProvider/Product/Form/Modifier/ProductUrlRewrite.php
index bcb5154e35501..10791eae5405f 100644
--- a/app/code/Magento/CatalogUrlRewrite/Ui/DataProvider/Product/Form/Modifier/ProductUrlRewrite.php
+++ b/app/code/Magento/CatalogUrlRewrite/Ui/DataProvider/Product/Form/Modifier/ProductUrlRewrite.php
@@ -53,7 +53,7 @@ public function __construct(
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function modifyMeta(array $meta)
{
@@ -65,7 +65,7 @@ public function modifyMeta(array $meta)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function modifyData(array $data)
{
@@ -95,16 +95,21 @@ protected function addUrlRewriteCheckbox(array $meta)
ScopeInterface::SCOPE_STORE,
$this->locator->getProduct()->getStoreId()
);
-
- $meta = $this->arrayManager->merge($containerPath, $meta, [
- 'arguments' => [
- 'data' => [
- 'config' => [
- 'component' => 'Magento_Ui/js/form/components/group',
+ $meta = $this->arrayManager->merge(
+ $containerPath,
+ $meta,
+ [
+ 'arguments' => [
+ 'data' => [
+ 'config' => [
+ 'component' => 'Magento_Ui/js/form/components/group',
+ 'label' => false,
+ 'required' => false,
+ ],
],
],
- ],
- ]);
+ ]
+ );
$checkbox['arguments']['data']['config'] = [
'componentType' => Field::NAME,
diff --git a/app/code/Magento/CatalogUrlRewrite/etc/di.xml b/app/code/Magento/CatalogUrlRewrite/etc/di.xml
index e6fbcaefd0768..5fb7d33546d60 100644
--- a/app/code/Magento/CatalogUrlRewrite/etc/di.xml
+++ b/app/code/Magento/CatalogUrlRewrite/etc/di.xml
@@ -45,4 +45,29 @@
+
+
+
+ - admin
+ - soap
+ - rest
+ - graphql
+ - standard
+
+
+
+
+
+
+ -
+
- catalog_product
+ - catalog_product
+
+ -
+
- catalog_category
+ - catalog_category
+
+
+
+
diff --git a/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv b/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv
index b3335dc3523ca..0f21e8ddf9fc9 100644
--- a/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv
+++ b/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv
@@ -5,4 +5,5 @@
"Product URL Suffix","Product URL Suffix"
"Use Categories Path for Product URLs","Use Categories Path for Product URLs"
"Create Permanent Redirect for URLs if URL Key Changed","Create Permanent Redirect for URLs if URL Key Changed"
-"Generate "category/product" URL Rewrites","Generate "category/product" URL Rewrites"
\ No newline at end of file
+"Generate "category/product" URL Rewrites","Generate "category/product" URL Rewrites"
+"URL key ""%1"" matches a reserved endpoint name (%2). Use another URL key.","URL key ""%1"" matches a reserved endpoint name (%2). Use another URL key."
diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php
new file mode 100644
index 0000000000000..59708d90c23b7
--- /dev/null
+++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php
@@ -0,0 +1,82 @@
+scopeConfig = $scopeConfig;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ): string {
+ /** @var StoreInterface $store */
+ $store = $context->getExtensionAttributes()->getStore();
+ $storeId = (int)$store->getId();
+ return $this->getCategoryUrlSuffix($storeId);
+ }
+
+ /**
+ * Retrieve category url suffix by store
+ *
+ * @param int $storeId
+ * @return string
+ */
+ private function getCategoryUrlSuffix(int $storeId): string
+ {
+ if (!isset($this->categoryUrlSuffix[$storeId])) {
+ $this->categoryUrlSuffix[$storeId] = $this->scopeConfig->getValue(
+ self::$xml_path_category_url_suffix,
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
+ }
+ return $this->categoryUrlSuffix[$storeId];
+ }
+}
diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php
new file mode 100644
index 0000000000000..9a0193ba36367
--- /dev/null
+++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php
@@ -0,0 +1,82 @@
+scopeConfig = $scopeConfig;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ): string {
+ /** @var StoreInterface $store */
+ $store = $context->getExtensionAttributes()->getStore();
+ $storeId = (int)$store->getId();
+ return $this->getProductUrlSuffix($storeId);
+ }
+
+ /**
+ * Retrieve product url suffix by store
+ *
+ * @param int $storeId
+ * @return string
+ */
+ private function getProductUrlSuffix(int $storeId): string
+ {
+ if (!isset($this->productUrlSuffix[$storeId])) {
+ $this->productUrlSuffix[$storeId] = $this->scopeConfig->getValue(
+ self::$xml_path_product_url_suffix,
+ ScopeInterface::SCOPE_STORE,
+ $storeId
+ );
+ }
+ return $this->productUrlSuffix[$storeId];
+ }
+}
diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json b/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json
index e276da0cc6fd8..202c573c2ae04 100644
--- a/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json
+++ b/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json
@@ -4,6 +4,7 @@
"type": "magento2-module",
"require": {
"php": "~7.1.3||~7.2.0||~7.3.0",
+ "magento/module-store": "*",
"magento/module-catalog": "*",
"magento/framework": "*"
},
diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/di.xml b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/di.xml
index 20e6b7e9c0053..8724972e71b17 100644
--- a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/di.xml
+++ b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/di.xml
@@ -14,4 +14,20 @@
+
+
+
+
+ - url_key
+
+
+
+
+
+
+
+ - Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator::ENTITY_TYPE
+
+
+
diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls
index 89108e578d673..82facf6959f3c 100644
--- a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/schema.graphqls
@@ -3,15 +3,24 @@
interface ProductInterface {
url_key: String @doc(description: "The part of the URL that identifies the product")
+ url_suffix: String @doc(description: "The part of the product URL that is appended after the url key") @resolver(class: "Magento\\CatalogUrlRewriteGraphQl\\Model\\Resolver\\ProductUrlSuffix")
url_path: String @deprecated(reason: "Use product's `canonical_url` or url rewrites instead")
url_rewrites: [UrlRewrite] @doc(description: "URL rewrites list") @resolver(class: "Magento\\UrlRewriteGraphQl\\Model\\Resolver\\UrlRewrite")
}
+interface CategoryInterface {
+ url_suffix: String @doc(description: "The part of the category URL that is appended after the url key") @resolver(class: "Magento\\CatalogUrlRewriteGraphQl\\Model\\Resolver\\CategoryUrlSuffix")
+}
+
input ProductFilterInput {
url_key: FilterTypeInput @doc(description: "The part of the URL that identifies the product")
url_path: FilterTypeInput @deprecated(reason: "Use product's `canonical_url` or url rewrites instead")
}
+input ProductAttributeFilterInput {
+ url_key: FilterEqualTypeInput @doc(description: "The part of the URL that identifies the product")
+}
+
input ProductSortInput {
url_key: SortEnum @doc(description: "The part of the URL that identifies the product")
url_path: SortEnum @deprecated(reason: "Use product's `canonical_url` or url rewrites instead")
diff --git a/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php b/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php
index e5fb20a58aea1..a712ae91cbfa9 100644
--- a/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php
+++ b/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php
@@ -11,6 +11,7 @@
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\ProductCategoryList;
+use Magento\Store\Model\Store;
/**
* Class Product
@@ -106,7 +107,7 @@ function ($attribute) {
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*
* @param array &$attributes
* @return void
@@ -164,6 +165,8 @@ public function addToCollection($collection)
}
/**
+ * Adds Attributes that belong to Global Scope
+ *
* @param \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute
* @param \Magento\Catalog\Model\ResourceModel\Product\Collection $collection
* @return $this
@@ -200,6 +203,8 @@ protected function addGlobalAttribute(
}
/**
+ * Adds Attributes that don't belong to Global Scope
+ *
* @param \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute
* @param \Magento\Catalog\Model\ResourceModel\Product\Collection $collection
* @return $this
@@ -208,7 +213,7 @@ protected function addNotGlobalAttribute(
\Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute,
\Magento\Catalog\Model\ResourceModel\Product\Collection $collection
) {
- $storeId = $this->storeManager->getStore()->getId();
+ $storeId = $this->storeManager->getStore()->getId();
$values = $collection->getAllAttributeValues($attribute);
$validEntities = [];
if ($values) {
@@ -218,7 +223,9 @@ protected function addNotGlobalAttribute(
$validEntities[] = $entityId;
}
} else {
- if ($this->validateAttribute($storeValues[\Magento\Store\Model\Store::DEFAULT_STORE_ID])) {
+ if (isset($storeValues[Store::DEFAULT_STORE_ID]) &&
+ $this->validateAttribute($storeValues[Store::DEFAULT_STORE_ID])
+ ) {
$validEntities[] = $entityId;
}
}
@@ -236,7 +243,7 @@ protected function addNotGlobalAttribute(
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*
* @return string
*/
@@ -257,7 +264,7 @@ public function getMappedSqlField()
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*
* @param \Magento\Catalog\Model\ResourceModel\Product\Collection $productCollection
* @return $this
diff --git a/app/code/Magento/CatalogWidget/composer.json b/app/code/Magento/CatalogWidget/composer.json
index 6722d0df93752..8c1bd220a0f32 100644
--- a/app/code/Magento/CatalogWidget/composer.json
+++ b/app/code/Magento/CatalogWidget/composer.json
@@ -14,7 +14,8 @@
"magento/module-rule": "*",
"magento/module-store": "*",
"magento/module-widget": "*",
- "magento/module-wishlist": "*"
+ "magento/module-wishlist": "*",
+ "magento/module-theme": "*"
},
"type": "magento2-module",
"license": [
diff --git a/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php b/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php
index 4941bf8451bf8..c99c9041941b1 100644
--- a/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php
+++ b/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php
@@ -85,7 +85,7 @@ class Renderer extends \Magento\Framework\View\Element\Template implements
protected $priceCurrency;
/**
- * @var \Magento\Framework\Module\ModuleManagerInterface
+ * @var \Magento\Framework\Module\Manager
*/
public $moduleManager;
@@ -105,7 +105,7 @@ class Renderer extends \Magento\Framework\View\Element\Template implements
* @param \Magento\Framework\Url\Helper\Data $urlHelper
* @param \Magento\Framework\Message\ManagerInterface $messageManager
* @param PriceCurrencyInterface $priceCurrency
- * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager
+ * @param \Magento\Framework\Module\Manager $moduleManager
* @param InterpretationStrategyInterface $messageInterpretationStrategy
* @param array $data
* @param ItemResolverInterface|null $itemResolver
@@ -120,7 +120,7 @@ public function __construct(
\Magento\Framework\Url\Helper\Data $urlHelper,
\Magento\Framework\Message\ManagerInterface $messageManager,
PriceCurrencyInterface $priceCurrency,
- \Magento\Framework\Module\ModuleManagerInterface $moduleManager,
+ \Magento\Framework\Module\Manager $moduleManager,
InterpretationStrategyInterface $messageInterpretationStrategy,
array $data = [],
ItemResolverInterface $itemResolver = null
diff --git a/app/code/Magento/Checkout/Block/Cart/Link.php b/app/code/Magento/Checkout/Block/Cart/Link.php
index 6ea5137521106..9e6db1754d9e4 100644
--- a/app/code/Magento/Checkout/Block/Cart/Link.php
+++ b/app/code/Magento/Checkout/Block/Cart/Link.php
@@ -13,7 +13,7 @@
class Link extends \Magento\Framework\View\Element\Html\Link
{
/**
- * @var \Magento\Framework\Module\ModuleManagerInterface
+ * @var \Magento\Framework\Module\Manager
*/
protected $_moduleManager;
@@ -24,14 +24,14 @@ class Link extends \Magento\Framework\View\Element\Html\Link
/**
* @param \Magento\Framework\View\Element\Template\Context $context
- * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager
+ * @param \Magento\Framework\Module\Manager $moduleManager
* @param \Magento\Checkout\Helper\Cart $cartHelper
* @param array $data
* @codeCoverageIgnore
*/
public function __construct(
\Magento\Framework\View\Element\Template\Context $context,
- \Magento\Framework\Module\ModuleManagerInterface $moduleManager,
+ \Magento\Framework\Module\Manager $moduleManager,
\Magento\Checkout\Helper\Cart $cartHelper,
array $data = []
) {
diff --git a/app/code/Magento/Checkout/Block/Link.php b/app/code/Magento/Checkout/Block/Link.php
index 3d0740181f4a5..4ab2981e9185e 100644
--- a/app/code/Magento/Checkout/Block/Link.php
+++ b/app/code/Magento/Checkout/Block/Link.php
@@ -13,7 +13,7 @@
class Link extends \Magento\Framework\View\Element\Html\Link
{
/**
- * @var \Magento\Framework\Module\ModuleManagerInterface
+ * @var \Magento\Framework\Module\Manager
*/
protected $_moduleManager;
@@ -24,14 +24,14 @@ class Link extends \Magento\Framework\View\Element\Html\Link
/**
* @param \Magento\Framework\View\Element\Template\Context $context
- * @param \Magento\Framework\Module\ModuleManagerInterface $moduleManager
+ * @param \Magento\Framework\Module\Manager $moduleManager
* @param \Magento\Checkout\Helper\Data $checkoutHelper
* @param array $data
* @codeCoverageIgnore
*/
public function __construct(
\Magento\Framework\View\Element\Template\Context $context,
- \Magento\Framework\Module\ModuleManagerInterface $moduleManager,
+ \Magento\Framework\Module\Manager $moduleManager,
\Magento\Checkout\Helper\Data $checkoutHelper,
array $data = []
) {
diff --git a/app/code/Magento/Checkout/Controller/Cart/UpdateItemQty.php b/app/code/Magento/Checkout/Controller/Cart/UpdateItemQty.php
index ac4a93e6066a4..9d17e32b2c93d 100644
--- a/app/code/Magento/Checkout/Controller/Cart/UpdateItemQty.php
+++ b/app/code/Magento/Checkout/Controller/Cart/UpdateItemQty.php
@@ -8,16 +8,25 @@
namespace Magento\Checkout\Controller\Cart;
use Magento\Checkout\Model\Cart\RequestQuantityProcessor;
+use Magento\Checkout\Model\Session as CheckoutSession;
+use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
+use Magento\Framework\App\Action\HttpPostActionInterface;
+use Magento\Framework\Data\Form\FormKey\Validator as FormKeyValidator;
use Magento\Framework\Exception\LocalizedException;
-use Magento\Checkout\Model\Session as CheckoutSession;
+use Magento\Framework\Exception\NotFoundException;
use Magento\Framework\Serialize\Serializer\Json;
-use Magento\Framework\Data\Form\FormKey\Validator as FormKeyValidator;
use Magento\Quote\Model\Quote\Item;
use Psr\Log\LoggerInterface;
-class UpdateItemQty extends \Magento\Framework\App\Action\Action
+/**
+ * UpdateItemQty ajax request
+ *
+ * @package Magento\Checkout\Controller\Cart
+ */
+class UpdateItemQty extends Action implements HttpPostActionInterface
{
+
/**
* @var RequestQuantityProcessor
*/
@@ -44,13 +53,16 @@ class UpdateItemQty extends \Magento\Framework\App\Action\Action
private $logger;
/**
- * @param Context $context,
+ * UpdateItemQty constructor
+ *
+ * @param Context $context
* @param RequestQuantityProcessor $quantityProcessor
* @param FormKeyValidator $formKeyValidator
* @param CheckoutSession $checkoutSession
* @param Json $json
* @param LoggerInterface $logger
*/
+
public function __construct(
Context $context,
RequestQuantityProcessor $quantityProcessor,
@@ -68,30 +80,26 @@ public function __construct(
}
/**
+ * Controller execute method
+ *
* @return void
*/
public function execute()
{
try {
- if (!$this->formKeyValidator->validate($this->getRequest())) {
- throw new LocalizedException(
- __('Something went wrong while saving the page. Please refresh the page and try again.')
- );
- }
+ $this->validateRequest();
+ $this->validateFormKey();
$cartData = $this->getRequest()->getParam('cart');
- if (!is_array($cartData)) {
- throw new LocalizedException(
- __('Something went wrong while saving the page. Please refresh the page and try again.')
- );
- }
+
+ $this->validateCartData($cartData);
$cartData = $this->quantityProcessor->process($cartData);
$quote = $this->checkoutSession->getQuote();
foreach ($cartData as $itemId => $itemInfo) {
$item = $quote->getItemById($itemId);
- $qty = isset($itemInfo['qty']) ? (double)$itemInfo['qty'] : 0;
+ $qty = isset($itemInfo['qty']) ? (double) $itemInfo['qty'] : 0;
if ($item) {
$this->updateItemQuantity($item, $qty);
}
@@ -111,11 +119,13 @@ public function execute()
*
* @param Item $item
* @param float $qty
+ * @return void
* @throws LocalizedException
*/
private function updateItemQuantity(Item $item, float $qty)
{
if ($qty > 0) {
+ $item->clearMessage();
$item->setQty($qty);
if ($item->getHasError()) {
@@ -145,9 +155,7 @@ private function jsonResponse(string $error = '')
*/
private function getResponseData(string $error = ''): array
{
- $response = [
- 'success' => true,
- ];
+ $response = ['success' => true];
if (!empty($error)) {
$response = [
@@ -158,4 +166,48 @@ private function getResponseData(string $error = ''): array
return $response;
}
+
+ /**
+ * Validates the Request HTTP method
+ *
+ * @return void
+ * @throws NotFoundException
+ */
+ private function validateRequest()
+ {
+ if ($this->getRequest()->isPost() === false) {
+ throw new NotFoundException(__('Page Not Found'));
+ }
+ }
+
+ /**
+ * Validates form key
+ *
+ * @return void
+ * @throws LocalizedException
+ */
+ private function validateFormKey()
+ {
+ if (!$this->formKeyValidator->validate($this->getRequest())) {
+ throw new LocalizedException(
+ __('Something went wrong while saving the page. Please refresh the page and try again.')
+ );
+ }
+ }
+
+ /**
+ * Validates cart data
+ *
+ * @param array|null $cartData
+ * @return void
+ * @throws LocalizedException
+ */
+ private function validateCartData($cartData = null)
+ {
+ if (!is_array($cartData)) {
+ throw new LocalizedException(
+ __('Something went wrong while saving the page. Please refresh the page and try again.')
+ );
+ }
+ }
}
diff --git a/app/code/Magento/Checkout/Model/DefaultConfigProvider.php b/app/code/Magento/Checkout/Model/DefaultConfigProvider.php
index 70352b50d8de4..fdf49d6765a29 100644
--- a/app/code/Magento/Checkout/Model/DefaultConfigProvider.php
+++ b/app/code/Magento/Checkout/Model/DefaultConfigProvider.php
@@ -397,6 +397,9 @@ private function getQuoteData()
if ($this->checkoutSession->getQuote()->getId()) {
$quote = $this->quoteRepository->get($this->checkoutSession->getQuote()->getId());
$quoteData = $quote->toArray();
+ if (null !== $quote->getExtensionAttributes()) {
+ $quoteData['extension_attributes'] = $quote->getExtensionAttributes()->__toArray();
+ }
$quoteData['is_virtual'] = $quote->getIsVirtual();
if (!$quote->getCustomer()->getId()) {
diff --git a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php
index da29482f0123f..cae78389d4120 100644
--- a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php
+++ b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php
@@ -193,7 +193,9 @@ private function limitShippingCarrier(Quote $quote) : void
$shippingAddress = $quote->getShippingAddress();
if ($shippingAddress && $shippingAddress->getShippingMethod()) {
$shippingRate = $shippingAddress->getShippingRateByCode($shippingAddress->getShippingMethod());
- $shippingAddress->setLimitCarrier($shippingRate->getCarrier());
+ if ($shippingRate) {
+ $shippingAddress->setLimitCarrier($shippingRate->getCarrier());
+ }
}
}
}
diff --git a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php
index 2eced5c642261..2f1a36318ebc8 100644
--- a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php
+++ b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php
@@ -124,9 +124,9 @@ public function savePaymentInformation(
$shippingAddress = $quote->getShippingAddress();
if ($shippingAddress && $shippingAddress->getShippingMethod()) {
$shippingRate = $shippingAddress->getShippingRateByCode($shippingAddress->getShippingMethod());
- $shippingAddress->setLimitCarrier(
- $shippingRate ? $shippingRate->getCarrier() : $shippingAddress->getShippingMethod()
- );
+ if ($shippingRate) {
+ $shippingAddress->setLimitCarrier($shippingRate->getCarrier());
+ }
}
}
$this->paymentMethodManagement->set($cartId, $paymentMethod);
diff --git a/app/code/Magento/Checkout/Model/Session.php b/app/code/Magento/Checkout/Model/Session.php
index a654c78853d7a..4a4861fa9ccd2 100644
--- a/app/code/Magento/Checkout/Model/Session.php
+++ b/app/code/Magento/Checkout/Model/Session.php
@@ -7,6 +7,8 @@
use Magento\Customer\Api\Data\CustomerInterface;
use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Quote\Api\Data\CartInterface;
use Magento\Quote\Model\Quote;
use Magento\Quote\Model\QuoteIdMaskFactory;
use Psr\Log\LoggerInterface;
@@ -21,9 +23,6 @@
*/
class Session extends \Magento\Framework\Session\SessionManager
{
- /**
- * Checkout state begin
- */
const CHECKOUT_STATE_BEGIN = 'begin';
/**
@@ -228,7 +227,7 @@ public function setLoadInactive($load = true)
*
* @return Quote
* @throws \Magento\Framework\Exception\LocalizedException
- * @throws \Magento\Framework\Exception\NoSuchEntityException
+ * @throws NoSuchEntityException
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
@@ -273,21 +272,17 @@ public function getQuote()
*/
$quote = $this->quoteRepository->get($this->getQuoteId());
}
- } catch (\Magento\Framework\Exception\NoSuchEntityException $e) {
+ } catch (NoSuchEntityException $e) {
$this->setQuoteId(null);
}
}
if (!$this->getQuoteId()) {
if ($this->_customerSession->isLoggedIn() || $this->_customer) {
- $customerId = $this->_customer
- ? $this->_customer->getId()
- : $this->_customerSession->getCustomerId();
- try {
- $quote = $this->quoteRepository->getActiveForCustomer($customerId);
- $this->setQuoteId($quote->getId());
- } catch (\Magento\Framework\Exception\NoSuchEntityException $e) {
- $this->logger->critical($e);
+ $quoteByCustomer = $this->getQuoteByCustomer();
+ if ($quoteByCustomer !== null) {
+ $this->setQuoteId($quoteByCustomer->getId());
+ $quote = $quoteByCustomer;
}
} else {
$quote->setIsCheckoutCart(true);
@@ -375,7 +370,7 @@ public function loadCustomerQuote()
try {
$customerQuote = $this->quoteRepository->getForCustomer($this->_customerSession->getCustomerId());
- } catch (\Magento\Framework\Exception\NoSuchEntityException $e) {
+ } catch (NoSuchEntityException $e) {
$customerQuote = $this->quoteFactory->create();
}
$customerQuote->setStoreId($this->_storeManager->getStore()->getId());
@@ -558,7 +553,7 @@ public function restoreQuote()
$this->replaceQuote($quote)->unsLastRealOrderId();
$this->_eventManager->dispatch('restore_quote', ['order' => $order, 'quote' => $quote]);
return true;
- } catch (\Magento\Framework\Exception\NoSuchEntityException $e) {
+ } catch (NoSuchEntityException $e) {
$this->logger->critical($e);
}
}
@@ -588,4 +583,22 @@ protected function isQuoteMasked()
{
return $this->isQuoteMasked;
}
+
+ /**
+ * Returns quote for customer if there is any
+ */
+ private function getQuoteByCustomer(): ?CartInterface
+ {
+ $customerId = $this->_customer
+ ? $this->_customer->getId()
+ : $this->_customerSession->getCustomerId();
+
+ try {
+ $quote = $this->quoteRepository->getActiveForCustomer($customerId);
+ } catch (NoSuchEntityException $e) {
+ $quote = null;
+ }
+
+ return $quote;
+ }
}
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertIsNotVisibleCartPagerTextActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertIsNotVisibleCartPagerTextActionGroup.xml
new file mode 100644
index 0000000000000..2072cb6df1dc1
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertIsNotVisibleCartPagerTextActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutSuccessActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutSuccessActionGroup.xml
new file mode 100644
index 0000000000000..9ff7e5a96fae7
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutSuccessActionGroup.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Verifies if the order is placed successfully on the 'one page checkout' page.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontMiniCartItemsActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontMiniCartItemsActionGroup.xml
index 176eebed142c8..8933ebbc1dd84 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontMiniCartItemsActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontMiniCartItemsActionGroup.xml
@@ -18,12 +18,24 @@
-
-
-
+
+
+
+
+
+ Validates that the provided Product details (Name, Price) are
+ not present in the Storefront Mini Shopping Cart.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontNotCalculatedValueInShippingTotalInOrderSummaryActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontNotCalculatedValueInShippingTotalInOrderSummaryActionGroup.xml
new file mode 100644
index 0000000000000..1ec42033a782b
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontNotCalculatedValueInShippingTotalInOrderSummaryActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Validates value of the Shipping total is not calculated.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontOrderCannotBePlacedActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontOrderCannotBePlacedActionGroup.xml
new file mode 100644
index 0000000000000..4f9555d84898d
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontOrderCannotBePlacedActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Validates order cannot be placed and checks error message.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShippingLabelDescriptionInOrderSummaryActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShippingLabelDescriptionInOrderSummaryActionGroup.xml
new file mode 100644
index 0000000000000..6a8efdb507c3e
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShippingLabelDescriptionInOrderSummaryActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Validates that the Shipping label description is present and correct.
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShoppingCartSummaryWithShippingActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShoppingCartSummaryWithShippingActionGroup.xml
index e74f5c24fb4f6..fe5887bbf6f7c 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShoppingCartSummaryWithShippingActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontShoppingCartSummaryWithShippingActionGroup.xml
@@ -16,9 +16,7 @@
-
-
-
-
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertToolbarTextIsVisibleInCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertToolbarTextIsVisibleInCartActionGroup.xml
new file mode 100644
index 0000000000000..6ede2a0dd5388
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertToolbarTextIsVisibleInCartActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml
index 7f6980d0c9744..4c7d4e31b2d6f 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutActionGroup.xml
@@ -64,6 +64,7 @@
+
@@ -170,6 +171,15 @@
+
+
+
+ Clicks next on Checkout Shipping step
+
+
+
+
+
@@ -221,7 +231,7 @@
- Validates the the provided Product appears in the Storefront Checkout 'Order Summary' section.
+ Validates the provided Product appears in the Storefront Checkout 'Order Summary' section.
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml
index 789a61a1700db..77734cc75497f 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewBillingAddressActionGroup.xml
@@ -28,6 +28,10 @@
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartDiscountAndSummaryActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartDiscountAndSummaryActionGroup.xml
index 112abfbb5897a..9f766742b545f 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartDiscountAndSummaryActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartDiscountAndSummaryActionGroup.xml
@@ -17,13 +17,12 @@
-
-
+
+
+
+
+
-
-
-
-
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenCartPageActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenCartPageActionGroup.xml
new file mode 100644
index 0000000000000..fe1e48e00c5bb
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontOpenCartPageActionGroup.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml
index c0a160fcb2a71..b07bcdccce674 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontProductCartActionGroup.xml
@@ -103,7 +103,7 @@
-
+
@@ -112,6 +112,7 @@
+
@@ -146,4 +147,15 @@
+
+
+
+ EXTENDS: StorefrontCheckCartActionGroup. Validates that the provided Discount is present in the Storefront Shopping Cart.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontRemoveCartItemActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontRemoveCartItemActionGroup.xml
new file mode 100644
index 0000000000000..f2d4088370a2b
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontRemoveCartItemActionGroup.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutCartPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutCartPage.xml
index a77b07a129dce..cf7f2baeb4b26 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutCartPage.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutCartPage.xml
@@ -9,6 +9,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml
index d3fa045e4654f..d6173dfa17916 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Page/CheckoutPage.xml
@@ -14,5 +14,6 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml
index 3ab3fa5857b78..af9d81249e8ac 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml
@@ -18,6 +18,7 @@
+
@@ -32,6 +33,7 @@
selector="//table[@id='shopping-cart-table']//tbody//tr[contains(@class,'item-actions')]//a[contains(@class,'action-delete')]"/>
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml
index 477451ef003ce..de71fc3f8ad0e 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml
@@ -9,6 +9,8 @@
+
+
@@ -20,7 +22,7 @@
-
+
@@ -33,5 +35,6 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutOrderSummarySection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutOrderSummarySection.xml
index d3ad2aed96946..026265656379a 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutOrderSummarySection.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutOrderSummarySection.xml
@@ -19,5 +19,7 @@
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml
index 903c21d7ec0ca..16fd373d3ae4d 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml
@@ -31,7 +31,8 @@
-
+
+
@@ -42,6 +43,7 @@
+
@@ -51,6 +53,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml
index 08a9d671a8d02..c486e13ecf58b 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml
@@ -14,6 +14,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCartToolbarSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCartToolbarSection.xml
new file mode 100644
index 0000000000000..ff40449369530
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontCartToolbarSection.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml
index 3e1de2b14ba62..80ed4f90c2cd0 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMiniCartSection.xml
@@ -13,7 +13,9 @@
-
+
+
+
@@ -23,6 +25,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.xml
index 9714b76a05613..163e71c50053f 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest.xml
@@ -30,7 +30,7 @@
-
+
@@ -147,7 +147,7 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteDownloadableProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteDownloadableProductFromShoppingCartTest.xml
index 1a51e1e02fe86..e16ef70c23e3d 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteDownloadableProductFromShoppingCartTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteDownloadableProductFromShoppingCartTest.xml
@@ -20,6 +20,7 @@
+
@@ -30,6 +31,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml
index 20015f76e08e3..c61545e51d535 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml
@@ -25,6 +25,9 @@
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml
index 5335ec2ad775d..4281a0eb77da8 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/EndToEndB2CGuestUserTest.xml
@@ -184,6 +184,194 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml
index 3c98f9177f4a7..fd6656b1d1b28 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml
@@ -26,6 +26,8 @@
100.00
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml
index 3ec73aec580d5..e85a47ab7a91d 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml
@@ -20,6 +20,7 @@
+
@@ -90,6 +91,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml
index 9c00f2be1d60b..d67800e21afc2 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml
@@ -109,7 +109,7 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml
index 09608eef7178a..e3090d6cb311b 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml
@@ -20,6 +20,9 @@
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddDownloadableProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddDownloadableProductToShoppingCartTest.xml
index 5a46dbc90e207..ec9852a6a939d 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddDownloadableProductToShoppingCartTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddDownloadableProductToShoppingCartTest.xml
@@ -18,6 +18,7 @@
+
@@ -31,6 +32,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml
index 09a5ce4c70373..8b8aed3ac6204 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml
@@ -15,9 +15,6 @@
-
-
-
@@ -280,7 +277,9 @@
-
-
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckPagerShoppingCartWithMoreThan20ProductsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckPagerShoppingCartWithMoreThan20ProductsTest.xml
new file mode 100644
index 0000000000000..9dbd5daba6f23
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckPagerShoppingCartWithMoreThan20ProductsTest.xml
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml
new file mode 100644
index 0000000000000..97eceae962bfb
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml
new file mode 100644
index 0000000000000..0e704e5336db9
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml
index 40b781df9b2ae..218ff959750d6 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest.xml
@@ -125,6 +125,10 @@
+
+
+
+
@@ -148,7 +152,7 @@
-
+
@@ -221,6 +225,10 @@
+
+
+
+
@@ -270,4 +278,4 @@
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteDownloadableProductFromMiniShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteDownloadableProductFromMiniShoppingCartTest.xml
index 6a3f6ab4f7058..0fa503e1783b5 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteDownloadableProductFromMiniShoppingCartTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteDownloadableProductFromMiniShoppingCartTest.xml
@@ -19,6 +19,7 @@
+
@@ -30,6 +31,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml
index a77341b8697b5..a0914cfc27138 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest.xml
@@ -23,9 +23,13 @@
+
+
+
+
-
+
@@ -77,11 +81,8 @@
-
+
-
-
-
@@ -109,10 +110,11 @@
-
+
+
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontMissingPagerShoppingCartWith20ProductsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontMissingPagerShoppingCartWith20ProductsTest.xml
new file mode 100644
index 0000000000000..afe4ebcfea40c
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontMissingPagerShoppingCartWith20ProductsTest.xml
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontNotApplicableShippingMethodInReviewAndPaymentStepTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontNotApplicableShippingMethodInReviewAndPaymentStepTest.xml
new file mode 100644
index 0000000000000..8ec89ce1fe8f0
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontNotApplicableShippingMethodInReviewAndPaymentStepTest.xml
@@ -0,0 +1,209 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 100
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutJsValidationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutJsValidationTest.xml
index 63751ad697ede..8ed8e590eb229 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutJsValidationTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutJsValidationTest.xml
@@ -11,6 +11,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest.xml
new file mode 100644
index 0000000000000..44bfe81b40dc0
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontShoppingCartPagerForOneItemPerPageAnd2ProductsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontShoppingCartPagerForOneItemPerPageAnd2ProductsTest.xml
new file mode 100644
index 0000000000000..744401cf24d13
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontShoppingCartPagerForOneItemPerPageAnd2ProductsTest.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKCustomerCheckoutWithCouponTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKCustomerCheckoutWithCouponTest.xml
index ebf24e710fe39..482e2fb6233a6 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKCustomerCheckoutWithCouponTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKCustomerCheckoutWithCouponTest.xml
@@ -18,7 +18,7 @@
-
+
20.00
@@ -38,10 +38,12 @@
+
+
@@ -106,6 +108,9 @@
+
+
+
@@ -127,4 +132,4 @@
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Checkout/Test/Unit/CustomerData/DirectoryDataTest.php b/app/code/Magento/Checkout/Test/Unit/CustomerData/DirectoryDataTest.php
new file mode 100644
index 0000000000000..3c0bae31c9c0d
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Unit/CustomerData/DirectoryDataTest.php
@@ -0,0 +1,96 @@
+objectManager = new ObjectManagerHelper($this);
+ $this->directoryHelperMock = $this->createMock(HelperData::class);
+
+ $this->model = $this->objectManager->getObject(
+ DirectoryData::class,
+ [
+ 'directoryHelper' => $this->directoryHelperMock
+ ]
+ );
+ }
+
+ /**
+ * Test getSectionData() function
+ */
+ public function testGetSectionData()
+ {
+ $regions = [
+ 'US' => [
+ 'TX' => [
+ 'code' => 'TX',
+ 'name' => 'Texas'
+ ]
+ ]
+ ];
+
+ $testCountryInfo = $this->objectManager->getObject(Country::class);
+ $testCountryInfo->setData('country_id', 'US');
+ $testCountryInfo->setData('iso2_code', 'US');
+ $testCountryInfo->setData('iso3_code', 'USA');
+ $testCountryInfo->setData('name_default', 'United States of America');
+ $testCountryInfo->setData('name_en_US', 'United States of America');
+ $countries = ['US' => $testCountryInfo];
+
+ $this->directoryHelperMock->expects($this->any())
+ ->method('getRegionData')
+ ->willReturn($regions);
+
+ $this->directoryHelperMock->expects($this->any())
+ ->method('getCountryCollection')
+ ->willReturn($countries);
+
+ /* Assert result */
+ $this->assertEquals(
+ [
+ 'US' => [
+ 'name' => 'United States of America',
+ 'regions' => [
+ 'TX' => [
+ 'code' => 'TX',
+ 'name' => 'Texas'
+ ]
+ ]
+ ]
+ ],
+ $this->model->getSectionData()
+ );
+ }
+}
diff --git a/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestQuantityProcessorTest.php b/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestQuantityProcessorTest.php
index daabb080b1c9a..82384fa83ab94 100644
--- a/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestQuantityProcessorTest.php
+++ b/app/code/Magento/Checkout/Test/Unit/Model/Cart/RequestQuantityProcessorTest.php
@@ -48,6 +48,9 @@ public function testProcess($cartData, $expected)
$this->assertEquals($this->requestProcessor->process($cartData), $expected);
}
+ /**
+ * @return array
+ */
public function cartDataProvider()
{
return [
diff --git a/app/code/Magento/Checkout/Test/Unit/Model/Layout/DepersonalizePluginTest.php b/app/code/Magento/Checkout/Test/Unit/Model/Layout/DepersonalizePluginTest.php
index 3cc80e14fd026..350f9954208fa 100644
--- a/app/code/Magento/Checkout/Test/Unit/Model/Layout/DepersonalizePluginTest.php
+++ b/app/code/Magento/Checkout/Test/Unit/Model/Layout/DepersonalizePluginTest.php
@@ -43,7 +43,7 @@ protected function setUp()
);
$this->checkoutSessionMock = $this->createPartialMock(\Magento\Checkout\Model\Session::class, ['clearStorage']);
$this->requestMock = $this->createMock(\Magento\Framework\App\Request\Http::class);
- $this->moduleManagerMock = $this->createMock(\Magento\Framework\Module\ModuleManagerInterface::class);
+ $this->moduleManagerMock = $this->createMock(\Magento\Framework\Module\Manager::class);
$this->cacheConfigMock = $this->createMock(\Magento\PageCache\Model\Config::class);
$this->depersonalizeCheckerMock = $this->createMock(\Magento\PageCache\Model\DepersonalizeChecker::class);
diff --git a/app/code/Magento/Checkout/Test/Unit/Model/SessionTest.php b/app/code/Magento/Checkout/Test/Unit/Model/SessionTest.php
index 26234992e6136..969631901adff 100644
--- a/app/code/Magento/Checkout/Test/Unit/Model/SessionTest.php
+++ b/app/code/Magento/Checkout/Test/Unit/Model/SessionTest.php
@@ -9,7 +9,8 @@
*/
namespace Magento\Checkout\Test\Unit\Model;
-use \Magento\Checkout\Model\Session;
+use Magento\Checkout\Model\Session;
+use Magento\Framework\Exception\NoSuchEntityException;
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
@@ -374,6 +375,68 @@ public function testGetStepData()
$this->assertEquals($stepData['complex']['key'], $session->getStepData('complex', 'key'));
}
+ /**
+ * Ensure that if quote not exist for customer quote will be null
+ *
+ * @return void
+ */
+ public function testGetQuote(): void
+ {
+ $storeManager = $this->getMockForAbstractClass(\Magento\Store\Model\StoreManagerInterface::class);
+ $customerSession = $this->createMock(\Magento\Customer\Model\Session::class);
+ $quoteRepository = $this->createMock(\Magento\Quote\Api\CartRepositoryInterface::class);
+ $quoteFactory = $this->createMock(\Magento\Quote\Model\QuoteFactory::class);
+ $quote = $this->createMock(\Magento\Quote\Model\Quote::class);
+ $logger = $this->createMock(\Psr\Log\LoggerInterface::class);
+ $loggerMethods = get_class_methods(\Psr\Log\LoggerInterface::class);
+
+ $quoteFactory->expects($this->once())
+ ->method('create')
+ ->willReturn($quote);
+ $customerSession->expects($this->exactly(3))
+ ->method('isLoggedIn')
+ ->willReturn(true);
+ $store = $this->getMockBuilder(\Magento\Store\Model\Store::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getWebsiteId', '__wakeup'])
+ ->getMock();
+ $storeManager->expects($this->any())
+ ->method('getStore')
+ ->will($this->returnValue($store));
+ $storage = $this->getMockBuilder(\Magento\Framework\Session\Storage::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['setData', 'getData'])
+ ->getMock();
+ $storage->expects($this->at(0))
+ ->method('getData')
+ ->willReturn(1);
+ $quoteRepository->expects($this->once())
+ ->method('getActiveForCustomer')
+ ->willThrowException(new NoSuchEntityException());
+
+ foreach ($loggerMethods as $method) {
+ $logger->expects($this->never())->method($method);
+ }
+
+ $quote->expects($this->once())
+ ->method('setCustomer')
+ ->with(null);
+
+ $constructArguments = $this->_helper->getConstructArguments(
+ \Magento\Checkout\Model\Session::class,
+ [
+ 'storeManager' => $storeManager,
+ 'quoteRepository' => $quoteRepository,
+ 'customerSession' => $customerSession,
+ 'storage' => $storage,
+ 'quoteFactory' => $quoteFactory,
+ 'logger' => $logger
+ ]
+ );
+ $this->_session = $this->_helper->getObject(\Magento\Checkout\Model\Session::class, $constructArguments);
+ $this->_session->getQuote();
+ }
+
public function testSetStepData()
{
$stepData = [
diff --git a/app/code/Magento/Checkout/etc/adminhtml/system.xml b/app/code/Magento/Checkout/etc/adminhtml/system.xml
index 2afa796443e75..399474a36bfc7 100644
--- a/app/code/Magento/Checkout/etc/adminhtml/system.xml
+++ b/app/code/Magento/Checkout/etc/adminhtml/system.xml
@@ -57,9 +57,9 @@