diff --git a/README.md b/README.md
index ec8bcdb292ea7..7662290cc09c2 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,10 @@ Welcome to Magento 2 installation! We're glad you chose to install Magento 2, a
* [Installation Guide](https://devdocs.magento.com/guides/v2.3/install-gde/bk-install-guide.html).
+## Learn More About GraphQL in Magento 2
+
+* [GraphQL Developer Guide](https://devdocs.magento.com/guides/v2.3/graphql/index.html)
+
Contributing to the Magento 2 Code Base
Contributions can take the form of new components or features, changes to existing features, tests, documentation (such as developer guides, user guides, examples, or specifications), bug fixes, optimizations, or just good suggestions.
diff --git a/app/code/Magento/AdminNotification/etc/db_schema.xml b/app/code/Magento/AdminNotification/etc/db_schema.xml
index 29d928ced2084..8849687611193 100644
--- a/app/code/Magento/AdminNotification/etc/db_schema.xml
+++ b/app/code/Magento/AdminNotification/etc/db_schema.xml
@@ -9,7 +9,7 @@
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
+ comment="Notification ID"/>
-
+
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()
@@ -197,6 +201,7 @@ protected function initTypeModels()
public function export()
{
//Execution time may be very long
+ // phpcs:ignore Magento2.Functions.DiscouragedFunction
set_time_limit(0);
$writer = $this->getWriter();
@@ -234,16 +239,6 @@ public function filterAttributeCollection(\Magento\Eav\Model\ResourceModel\Entit
foreach ($collection as $attribute) {
if (in_array($attribute->getAttributeCode(), $this->_disabledAttrs)) {
- if (isset($this->_parameters[\Magento\ImportExport\Model\Export::FILTER_ELEMENT_SKIP])) {
- if ($attribute->getAttributeCode() == ImportAdvancedPricing::COL_TIER_PRICE
- && in_array(
- $attribute->getId(),
- $this->_parameters[\Magento\ImportExport\Model\Export::FILTER_ELEMENT_SKIP]
- )
- ) {
- $this->_passTierPrice = 1;
- }
- }
$collection->removeItemByKey($attribute->getId());
}
}
@@ -363,6 +358,7 @@ private function prepareExportData(
$linkedTierPricesData = [];
foreach ($tierPricesData as $tierPriceData) {
$sku = $productLinkIdToSkuMap[$tierPriceData['product_link_id']];
+ // phpcs:ignore Magento2.Performance.ForeachArrayMerge
$linkedTierPricesData[] = array_merge(
$tierPriceData,
[ImportAdvancedPricing::COL_SKU => $sku]
@@ -471,7 +467,7 @@ private function fetchTierPrices(array $productIds): array
ImportAdvancedPricing::COL_TIER_PRICE_QTY => 'ap.qty',
ImportAdvancedPricing::COL_TIER_PRICE => 'ap.value',
ImportAdvancedPricing::COL_TIER_PRICE_PERCENTAGE_VALUE => 'ap.percentage_value',
- 'product_link_id' => 'ap.' .$productEntityLinkField,
+ 'product_link_id' => 'ap.' . $productEntityLinkField,
];
if ($exportFilter && array_key_exists('tier_price', $exportFilter)) {
if (!empty($exportFilter['tier_price'][0])) {
@@ -488,7 +484,7 @@ private function fetchTierPrices(array $productIds): array
$selectFields
)
->where(
- 'ap.'.$productEntityLinkField.' IN (?)',
+ 'ap.' . $productEntityLinkField . ' IN (?)',
$productIds
);
@@ -602,7 +598,7 @@ protected function _getWebsiteCode(int $websiteId): string
}
if ($storeName && $currencyCode) {
- $code = $storeName.' ['.$currencyCode.']';
+ $code = $storeName . ' [' . $currencyCode . ']';
} else {
$code = $storeName;
}
diff --git a/app/code/Magento/AdvancedSearch/etc/db_schema.xml b/app/code/Magento/AdvancedSearch/etc/db_schema.xml
index 2dd8c68e2d5fd..bf85a23782095 100644
--- a/app/code/Magento/AdvancedSearch/etc/db_schema.xml
+++ b/app/code/Magento/AdvancedSearch/etc/db_schema.xml
@@ -9,11 +9,11 @@
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
-
+
+ default="0" comment="Query ID"/>
+ default="0" comment="Relation ID"/>
diff --git a/app/code/Magento/Analytics/README.md b/app/code/Magento/Analytics/README.md
index aa424182e2ebd..7ec30c6dd484b 100644
--- a/app/code/Magento/Analytics/README.md
+++ b/app/code/Magento/Analytics/README.md
@@ -1,18 +1,27 @@
-# Magento_Analytics Module
+# Magento_Analytics module
The Magento_Analytics module integrates your Magento instance with the [Magento Business Intelligence (MBI)](https://magento.com/products/business-intelligence) to use [Advanced Reporting](https://devdocs.magento.com/guides/v2.3/advanced-reporting/modules.html) functionality.
The module implements the following functionality:
-* enabling subscription to the MBI and automatic re-subscription
-* changing the base URL with the same MBI account remained
-* declaring the configuration schemas for report data collection
-* collecting the Magento instance data as reports for the MBI
-* introducing API that provides the collected data
-* extending Magento configuration with the module parameters:
- * subscription status (enabled/disabled)
- * industry (a business area in which the instance website works)
- * time of data collection (time of the day when the module collects data)
+- Enabling subscription to Magento Business Intelligence (MBI) and automatic re-subscription
+- Declaring the configuration schemas for report data collection
+- Collecting the Magento instance data as reports for MBI
+- Introducing API that provides the collected data
+- Extending Magento configuration with the module parameters:
+ - Subscription status (enabled/disabled)
+ - Industry (a business area in which the instance website works)
+ - Time of data collection (time of the day when the module collects data)
+
+## Installation details
+
+Before disabling or uninstalling this module, note that the following modules depends on this module:
+- Magento_CatalogAnalytics
+- Magento_CustomerAnalytics
+- Magento_QuoteAnalytics
+- Magento_ReviewAnalytics
+- Magento_SalesAnalytics
+- Magento_WishlistAnalytics
## Structure
@@ -29,12 +38,12 @@ The subscription to the MBI service is enabled during the installation process o
Configuration settings for the Analytics module can be modified in the Admin Panel on the Stores > Configuration page under the General > Advanced Reporting tab.
The following options can be adjusted:
-* Advanced Reporting Service (Enabled/Disabled)
- * Alters the status of the Advanced Reporting subscription
-* Time of day to send data (Hour/Minute/Second in the store's time zone)
- * Defines when the data collection process for the Advanced Reporting service occurs
-* Industry
- * Defines the industry of the store in order to create a personalized Advanced Reporting experience
+- Advanced Reporting Service (Enabled/Disabled)
+ - Alters the status of the Advanced Reporting subscription
+- Time of day to send data (Hour/Minute/Second in the store's time zone)
+ - Defines when the data collection process for the Advanced Reporting service occurs
+- Industry
+ - Defines the industry of the store in order to create a personalized Advanced Reporting experience
## Extensibility
diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml
index 58e62500b8203..8ebd8cb594bee 100644
--- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml
+++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml
@@ -25,9 +25,9 @@
-
-
-
+
+
+
diff --git a/app/code/Magento/AsynchronousOperations/etc/adminhtml/system.xml b/app/code/Magento/AsynchronousOperations/etc/adminhtml/system.xml
index 7190b80750357..e373a4fc78b13 100644
--- a/app/code/Magento/AsynchronousOperations/etc/adminhtml/system.xml
+++ b/app/code/Magento/AsynchronousOperations/etc/adminhtml/system.xml
@@ -13,6 +13,7 @@
Bulk Actions
Days Saved in Log
+ validate-zero-or-greater validate-digits
diff --git a/app/code/Magento/Authorization/Model/Role.php b/app/code/Magento/Authorization/Model/Role.php
index fc32fbcaa2e98..042e95806ae18 100644
--- a/app/code/Magento/Authorization/Model/Role.php
+++ b/app/code/Magento/Authorization/Model/Role.php
@@ -52,6 +52,9 @@ public function __construct( //phpcs:ignore Generic.CodeAnalysis.UselessOverridi
/**
* @inheritDoc
+ *
+ * @SuppressWarnings(PHPMD.SerializationAware)
+ * @deprecated Do not use PHP serialization.
*/
public function __sleep()
{
@@ -61,6 +64,9 @@ public function __sleep()
/**
* @inheritDoc
+ *
+ * @SuppressWarnings(PHPMD.SerializationAware)
+ * @deprecated Do not use PHP serialization.
*/
public function __wakeup()
{
diff --git a/app/code/Magento/Authorizenet/README.md b/app/code/Magento/Authorizenet/README.md
index 380161d8b264e..62598837bee6d 100644
--- a/app/code/Magento/Authorizenet/README.md
+++ b/app/code/Magento/Authorizenet/README.md
@@ -1 +1,42 @@
+# Magento_Authorizenet module
+
The Magento_Authorizenet module implements the integration with the Authorize.Net payment gateway and makes the latter available as a payment method in Magento.
+
+## Extensibility
+
+Extension developers can interact with the Magento_Authorizenet module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html).
+
+[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Authorizenet module.
+
+### Events
+
+This module dispatches the following events:
+
+ - `checkout_directpost_placeOrder` event in the `\Magento\Authorizenet\Controller\Directpost\Payment\Place::placeCheckoutOrder()` method. Parameters:
+ - `result` is a data object (`\Magento\Framework\DataObject` class).
+ - `action` is a controller object (`\Magento\Authorizenet\Controller\Directpost\Payment\Place`).
+
+ - `order_cancel_after` event in the `\Magento\Authorizenet\Model\Directpost::declineOrder()` method. Parameters:
+ - `order` is an order object (`\Magento\Sales\Model\Order` class).
+
+
+This module observes the following events:
+
+ - `checkout_submit_all_after` event in the `Magento\Authorizenet\Observer\SaveOrderAfterSubmitObserver` file.
+ - `checkout_directpost_placeOrder` event in the `Magento\Authorizenet\Observer\AddFieldsToResponseObserver` file.
+
+For information about events in Magento 2, see [Events and observers](http://devdocs.magento.com/guides/v2.3/extension-dev-guide/events-and-observers.html#events).
+
+### Layouts
+
+This module introduces the following layouts and layout handles in the `view/adminhtml/layout` directory:
+
+- `adminhtml_authorizenet_directpost_payment_redirect`
+
+This module introduces the following layouts and layout handles in the `view/frontend/layout` directory:
+
+- `authorizenet_directpost_payment_backendresponse`
+- `authorizenet_directpost_payment_redirect`
+- `authorizenet_directpost_payment_response`
+
+For more information about layouts in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/layouts/layout-overview.html).
diff --git a/app/code/Magento/Backend/Model/Auth/Session.php b/app/code/Magento/Backend/Model/Auth/Session.php
index 809b78b7b98bc..6d2f8f6a21d4a 100644
--- a/app/code/Magento/Backend/Model/Auth/Session.php
+++ b/app/code/Magento/Backend/Model/Auth/Session.php
@@ -5,17 +5,20 @@
*/
namespace Magento\Backend\Model\Auth;
+use Magento\Framework\Acl;
+use Magento\Framework\AclFactory;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory;
use Magento\Framework\Stdlib\CookieManagerInterface;
+use Magento\Backend\Spi\SessionUserHydratorInterface;
+use Magento\Backend\Spi\SessionAclHydratorInterface;
+use Magento\User\Model\User;
+use Magento\User\Model\UserFactory;
/**
* Backend Auth session model
*
* @api
- * @method \Magento\User\Model\User|null getUser()
- * @method \Magento\Backend\Model\Auth\Session setUser(\Magento\User\Model\User $value)
- * @method \Magento\Framework\Acl|null getAcl()
- * @method \Magento\Backend\Model\Auth\Session setAcl(\Magento\Framework\Acl $value)
* @method int getUpdatedAt()
* @method \Magento\Backend\Model\Auth\Session setUpdatedAt(int $value)
*
@@ -56,6 +59,36 @@ class Session extends \Magento\Framework\Session\SessionManager implements \Mage
*/
protected $_config;
+ /**
+ * @var SessionUserHydratorInterface
+ */
+ private $userHydrator;
+
+ /**
+ * @var SessionAclHydratorInterface
+ */
+ private $aclHydrator;
+
+ /**
+ * @var UserFactory
+ */
+ private $userFactory;
+
+ /**
+ * @var AclFactory
+ */
+ private $aclFactory;
+
+ /**
+ * @var User|null
+ */
+ private $user;
+
+ /**
+ * @var Acl|null
+ */
+ private $acl;
+
/**
* @param \Magento\Framework\App\Request\Http $request
* @param \Magento\Framework\Session\SidResolverInterface $sidResolver
@@ -70,6 +103,10 @@ class Session extends \Magento\Framework\Session\SessionManager implements \Mage
* @param \Magento\Backend\Model\UrlInterface $backendUrl
* @param \Magento\Backend\App\ConfigInterface $config
* @throws \Magento\Framework\Exception\SessionException
+ * @param SessionUserHydratorInterface|null $userHydrator
+ * @param SessionAclHydratorInterface|null $aclHydrator
+ * @param UserFactory|null $userFactory
+ * @param AclFactory|null $aclFactory
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -84,11 +121,19 @@ public function __construct(
\Magento\Framework\App\State $appState,
\Magento\Framework\Acl\Builder $aclBuilder,
\Magento\Backend\Model\UrlInterface $backendUrl,
- \Magento\Backend\App\ConfigInterface $config
+ \Magento\Backend\App\ConfigInterface $config,
+ ?SessionUserHydratorInterface $userHydrator = null,
+ ?SessionAclHydratorInterface $aclHydrator = null,
+ ?UserFactory $userFactory = null,
+ ?AclFactory $aclFactory = null
) {
$this->_config = $config;
$this->_aclBuilder = $aclBuilder;
$this->_backendUrl = $backendUrl;
+ $this->userHydrator = $userHydrator ?? ObjectManager::getInstance()->get(SessionUserHydratorInterface::class);
+ $this->aclHydrator = $aclHydrator ?? ObjectManager::getInstance()->get(SessionAclHydratorInterface::class);
+ $this->userFactory = $userFactory ?? ObjectManager::getInstance()->get(UserFactory::class);
+ $this->aclFactory = $aclFactory ?? ObjectManager::getInstance()->get(AclFactory::class);
parent::__construct(
$request,
$sidResolver,
@@ -232,6 +277,16 @@ public function processLogin()
return $this;
}
+ /**
+ * @inheritDoc
+ */
+ public function destroy(array $options = null)
+ {
+ $this->user = null;
+ $this->acl = null;
+ parent::destroy($options);
+ }
+
/**
* Process of configuring of current auth storage when logout was performed
*
@@ -255,4 +310,142 @@ public function isValidForPath($path)
{
return true;
}
+
+ /**
+ * Logged-in user.
+ *
+ * @return User|null
+ */
+ public function getUser()
+ {
+ if (!$this->user) {
+ $userData = $this->getUserData();
+ if ($userData) {
+ /** @var User $user */
+ $user = $this->userFactory->create();
+ $this->userHydrator->hydrate($user, $userData);
+ $this->user = $user;
+ } elseif ($user = parent::getUser()) {
+ $this->setUser($user);
+ }
+ }
+
+ return $this->user;
+ }
+
+ /**
+ * Set logged-in user instance.
+ *
+ * @param User|null $user
+ * @return Session
+ */
+ public function setUser($user)
+ {
+ $this->setUserData(null);
+ if ($user) {
+ $this->setUserData($this->userHydrator->extract($user));
+ }
+ $this->user = $user;
+
+ return $this;
+ }
+
+ /**
+ * Is user logged in?
+ *
+ * @return bool
+ */
+ public function hasUser()
+ {
+ return (bool)$this->getUser();
+ }
+
+ /**
+ * Remove logged-in user.
+ *
+ * @return Session
+ */
+ public function unsUser()
+ {
+ $this->user = null;
+ parent::unsUser();
+ return $this->unsUserData();
+ }
+
+ /**
+ * Logged-in user's ACL data.
+ *
+ * @return Acl|null
+ */
+ public function getAcl()
+ {
+ if (!$this->acl) {
+ $aclData = $this->getUserAclData();
+ if ($aclData) {
+ /** @var Acl $acl */
+ $acl = $this->aclFactory->create();
+ $this->aclHydrator->hydrate($acl, $aclData);
+ $this->acl = $acl;
+ } elseif ($acl = parent::getAcl()) {
+ $this->setAcl($acl);
+ }
+ }
+
+ return $this->acl;
+ }
+
+ /**
+ * Set logged-in user's ACL data instance.
+ *
+ * @param Acl|null $acl
+ * @return Session
+ */
+ public function setAcl($acl)
+ {
+ $this->setUserAclData(null);
+ if ($acl) {
+ $this->setUserAclData($this->aclHydrator->extract($acl));
+ }
+ $this->acl = $acl;
+
+ return $this;
+ }
+
+ /**
+ * Whether ACL data is present.
+ *
+ * @return bool
+ */
+ public function hasAcl()
+ {
+ return (bool)$this->getAcl();
+ }
+
+ /**
+ * Remove ACL data.
+ *
+ * @return Session
+ */
+ public function unsAcl()
+ {
+ $this->acl = null;
+ parent::unsAcl();
+ return $this->unsUserAclData();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function writeClose()
+ {
+ //Updating data in session in case these objects has been changed.
+ if ($this->user) {
+ $this->setUser($this->user);
+ }
+ if ($this->acl) {
+ $this->setAcl($this->acl);
+ }
+
+ parent::writeClose();
+ }
}
diff --git a/app/code/Magento/Backend/Model/Auth/SessionAclHydrator.php b/app/code/Magento/Backend/Model/Auth/SessionAclHydrator.php
new file mode 100644
index 0000000000000..34e01be696672
--- /dev/null
+++ b/app/code/Magento/Backend/Model/Auth/SessionAclHydrator.php
@@ -0,0 +1,36 @@
+ $acl->_rules, 'resources' => $acl->_resources, 'roles' => $acl->_roleRegistry];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function hydrate(Acl $target, array $data): void
+ {
+ $target->_rules = $data['rules'];
+ $target->_resources = $data['resources'];
+ $target->_roleRegistry = $data['roles'];
+ }
+}
diff --git a/app/code/Magento/Backend/Model/Auth/SessionUserHydrator.php b/app/code/Magento/Backend/Model/Auth/SessionUserHydrator.php
new file mode 100644
index 0000000000000..6dee8b7b302c8
--- /dev/null
+++ b/app/code/Magento/Backend/Model/Auth/SessionUserHydrator.php
@@ -0,0 +1,54 @@
+roleFactory = $roleFactory;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function extract(User $user): array
+ {
+ return ['data' => $user->getData(), 'role_data' => $user->getRole()->getData()];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function hydrate(User $target, array $data): void
+ {
+ $target->setData($data['data']);
+ /** @var Role $role */
+ $role = $this->roleFactory->create();
+ $role->setData($data['role_data']);
+ $target->setData('extracted_role', $role);
+ $target->getRole();
+ }
+}
diff --git a/app/code/Magento/Backend/README.md b/app/code/Magento/Backend/README.md
index 03c7d86516b92..205051809328a 100644
--- a/app/code/Magento/Backend/README.md
+++ b/app/code/Magento/Backend/README.md
@@ -1,3 +1,112 @@
-The Backend module contains common infrastructure and assets for other modules to be defined and used in their
-administration user interface (UI). It does not contain anything specific to other modules. Among many things it
-handles the logic of authenticating and authorizing users.
+# Magento_Backend module
+
+The Magento_Backend module contains common infrastructure and assets for other modules to be defined and used in their
+administration user interface (UI).
+
+The Magento_Backend module does not contain anything specific to other modules. Among many things it handles the logic of authenticating and authorizing users.
+
+## Installation details
+
+Before disabling or uninstalling this module, note that the following modules depends on this module:
+
+- Magento_Analytics
+- Magento_Authorization
+- Magento_NewRelicReporting
+- Magento_ProductVideo
+- Magento_ReleaseNotification
+- Magento_Search
+- Magento_Security
+- Magento_Signifyd
+- Magento_Swatches
+- Magento_Ui
+- Magento_User
+- Magento_Webapi
+
+For information about module installation in Magento 2, see [Enable or disable modules](https://devdocs.magento.com/guides/v2.3/install-gde/install/cli/install-cli-subcommands-enable.html).
+
+## Structure
+
+Beyond the [usual module file structure](https://devdocs.magento.com/guides/v2.3/architecture/archi_perspectives/components/modules/mod_intro.html) the module contains a directory `Service/V1`.
+
+`Service/V1` - contains logic to provide a list of modules installed in Magento.
+
+For information about typical file structure of a module in Magento 2, see [Module file structure](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/build/module-file-structure.html#module-file-structure).
+
+## Extensibility
+
+Extension developers can interact with the Magento_Backend module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html).
+
+[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Backend module.
+
+### Events
+
+The module dispatches the following events:
+
+ - `adminhtml_block_html_before` event in the `\Magento\Backend\Block\Template::_toHtml()` method. Parameters:
+ - `block` is the backend block template (this) (`\Magento\Backend\Block\Template` class).
+ - `adminhtml_store_edit_form_prepare_form` event in the `\Magento\Backend\Block\System\Store\Edit\AbstractForm::_prepareForm()` method. Parameters:
+ - `block` is the AbstractForm block (this) (`\Magento\Backend\Block\System\Store\Edit\AbstractForm` class).
+ - `backend_block_widget_grid_prepare_grid_before` event in the `\Magento\Backend\Block\Widget\Grid::_prepareGrid()` method. Parameters:
+ - `grid` is the widget grid block (this) (`\Magento\Backend\Block\Widget\Grid` class)
+ - `collection` is the grid collection (`\Magento\Framework\Data\Collection` class).
+ - `adminhtml_cache_flush_system` event in the `\Magento\Backend\Console\Command\CacheCleanCommand::performAction()` method.
+ - `adminhtml_cache_flush_all` event in the `\Magento\Backend\Console\Command\CacheFlushCommand::performAction()` method.
+ - `clean_catalog_images_cache_after` event in the `\Magento\Backend\Controller\Adminhtml\Cache\CleanImages::execute()` method.
+ - `clean_media_cache_after` event in the `\Magento\Backend\Controller\Adminhtml\Cache\CleanMedia::execute()` method.
+ - `clean_static_files_cache_after` event in the `\Magento\Backend\Controller\Adminhtml\Cache\CleanStaticFiles::execute()` method.
+ - `adminhtml_cache_flush_all` event in the `\Magento\Backend\Controller\Adminhtml\Cache\FlushAll::execute()` method.
+ - `adminhtml_cache_flush_system` event in the `\Magento\Backend\Controller\Adminhtml\Cache\FlushSystem::execute()` method.
+ - `theme_save_after` event in the `\Magento\Backend\Controller\Adminhtml\System\Design\Save::execute()` method.
+ - `backend_auth_user_login_success` event in the `\Magento\Backend\Model\Auth::login()` method. Parameters:
+ - `user` is the credential storage object (`null | \Magento\Backend\Model\Auth\Credential\StorageInterface`)
+ - `backend_auth_user_login_failed` event in the `\Magento\Backend\Model\Auth::login()` method. Parameters:
+ - `user_name` is username extracted from the credential storage object (`null | \Magento\Backend\Model\Auth\Credential\StorageInterface`)
+ - `exception` any exception generated (`\Magento\Framework\Exception\LocalizedException | \Magento\Framework\Exception\Plugin\AuthenticationException`)
+
+For information about an event in Magento 2, see [Events and observers](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/events-and-observers.html#events).
+
+### Layouts
+
+This module introduces the following layouts and layout handles in the `view/adminhtml/layout` directory:
+
+- `admin_login`
+- `adminhtml_auth_login`
+- `adminhtml_cache_block`
+- `adminhtml_cache_index`
+- `adminhtml_dashboard_customersmost`
+- `adminhtml_dashboard_customersnewest`
+- `adminhtml_dashboard_index`
+- `adminhtml_dashboard_productsviewed`
+- `adminhtml_denied`
+- `adminhtml_noroute`
+- `adminhtml_system_account_index`
+- `adminhtml_system_design_edit`
+- `adminhtml_system_design_grid`
+- `adminhtml_system_design_grid_block`
+- `adminhtml_system_design_index`
+- `adminhtml_system_store_deletestore`
+- `adminhtml_system_store_editstore`
+- `adminhtml_system_store_grid_block`
+- `adminhtml_system_store_index`
+- `default`
+- `editor`
+- `empty`
+- `formkey`
+- `overlay_popup`
+- `popup`
+
+
+For more information about layouts in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/layouts/layout-overview.html).
+
+### UI components
+
+You can extend Magento_Backend module using the following configuration files:
+
+- `view/adminhtml/ui_component/design_config_form.xml`
+- `view/adminhtml/ui_component/design_config_listing.xml`
+
+For information about UI components in Magento 2, see [Overview of UI components](https://devdocs.magento.com/guides/v2.3/ui_comp_guide/bk-ui_comps.html).
+
+## Additional information
+
+For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html).
diff --git a/app/code/Magento/Backend/Spi/SessionAclHydratorInterface.php b/app/code/Magento/Backend/Spi/SessionAclHydratorInterface.php
new file mode 100644
index 0000000000000..7227cc92fcc8e
--- /dev/null
+++ b/app/code/Magento/Backend/Spi/SessionAclHydratorInterface.php
@@ -0,0 +1,34 @@
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminCheckLocaleAndDeveloperConfigInDeveloperModeTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminCheckLocaleAndDeveloperConfigInDeveloperModeTest.xml
new file mode 100644
index 0000000000000..59a6a7e261b87
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminCheckLocaleAndDeveloperConfigInDeveloperModeTest.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminCheckLocaleAndDeveloperConfigInProductionModeTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminCheckLocaleAndDeveloperConfigInProductionModeTest.xml
new file mode 100644
index 0000000000000..2dade727ca411
--- /dev/null
+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminCheckLocaleAndDeveloperConfigInProductionModeTest.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php b/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php
deleted file mode 100644
index f1a4bc355b08e..0000000000000
--- a/app/code/Magento/Backend/Test/Unit/Model/Auth/SessionTest.php
+++ /dev/null
@@ -1,273 +0,0 @@
-cookieMetadataFactory = $this->createPartialMock(
- \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory::class,
- ['createPublicCookieMetadata']
- );
-
- $this->config = $this->createPartialMock(\Magento\Backend\App\Config::class, ['getValue']);
- $this->cookieManager = $this->createPartialMock(
- \Magento\Framework\Stdlib\Cookie\PhpCookieManager::class,
- ['getCookie', 'setPublicCookie']
- );
- $this->storage = $this->createPartialMock(
- \Magento\Framework\Session\Storage::class,
- ['getUser', 'getAcl', 'setAcl']
- );
- $this->sessionConfig = $this->createPartialMock(
- \Magento\Framework\Session\Config::class,
- ['getCookiePath', 'getCookieDomain', 'getCookieSecure', 'getCookieHttpOnly']
- );
- $this->aclBuilder = $this->getMockBuilder(\Magento\Framework\Acl\Builder::class)
- ->disableOriginalConstructor()
- ->getMock();
- $objectManager = new ObjectManager($this);
- $this->session = $objectManager->getObject(
- \Magento\Backend\Model\Auth\Session::class,
- [
- 'config' => $this->config,
- 'sessionConfig' => $this->sessionConfig,
- 'cookieManager' => $this->cookieManager,
- 'cookieMetadataFactory' => $this->cookieMetadataFactory,
- 'storage' => $this->storage,
- 'aclBuilder' => $this->aclBuilder
- ]
- );
- }
-
- protected function tearDown()
- {
- $this->config = null;
- $this->sessionConfig = null;
- $this->session = null;
- }
-
- /**
- * @dataProvider refreshAclDataProvider
- * @param $isUserPassedViaParams
- */
- public function testRefreshAcl($isUserPassedViaParams)
- {
- $aclMock = $this->getMockBuilder(\Magento\Framework\Acl::class)->disableOriginalConstructor()->getMock();
- $this->aclBuilder->expects($this->any())->method('getAcl')->willReturn($aclMock);
- $userMock = $this->getMockBuilder(\Magento\User\Model\User::class)
- ->setMethods(['getReloadAclFlag', 'setReloadAclFlag', 'unsetData', 'save'])
- ->disableOriginalConstructor()
- ->getMock();
- $userMock->expects($this->any())->method('getReloadAclFlag')->willReturn(true);
- $userMock->expects($this->once())->method('setReloadAclFlag')->with('0')->willReturnSelf();
- $userMock->expects($this->once())->method('save');
- $this->storage->expects($this->once())->method('setAcl')->with($aclMock);
- $this->storage->expects($this->any())->method('getAcl')->willReturn($aclMock);
- if ($isUserPassedViaParams) {
- $this->session->refreshAcl($userMock);
- } else {
- $this->storage->expects($this->once())->method('getUser')->willReturn($userMock);
- $this->session->refreshAcl();
- }
- $this->assertSame($aclMock, $this->session->getAcl());
- }
-
- /**
- * @return array
- */
- public function refreshAclDataProvider()
- {
- return [
- 'User set via params' => [true],
- 'User set to session object' => [false]
- ];
- }
-
- public function testIsLoggedInPositive()
- {
- $user = $this->createPartialMock(\Magento\User\Model\User::class, ['getId', '__wakeup']);
- $user->expects($this->once())
- ->method('getId')
- ->will($this->returnValue(1));
-
- $this->storage->expects($this->any())
- ->method('getUser')
- ->will($this->returnValue($user));
-
- $this->assertTrue($this->session->isLoggedIn());
- }
-
- public function testProlong()
- {
- $name = session_name();
- $cookie = 'cookie';
- $lifetime = 900;
- $path = '/';
- $domain = 'magento2';
- $secure = true;
- $httpOnly = true;
-
- $this->config->expects($this->once())
- ->method('getValue')
- ->with(\Magento\Backend\Model\Auth\Session::XML_PATH_SESSION_LIFETIME)
- ->willReturn($lifetime);
- $cookieMetadata = $this->createMock(\Magento\Framework\Stdlib\Cookie\PublicCookieMetadata::class);
- $cookieMetadata->expects($this->once())
- ->method('setDuration')
- ->with($lifetime)
- ->will($this->returnSelf());
- $cookieMetadata->expects($this->once())
- ->method('setPath')
- ->with($path)
- ->will($this->returnSelf());
- $cookieMetadata->expects($this->once())
- ->method('setDomain')
- ->with($domain)
- ->will($this->returnSelf());
- $cookieMetadata->expects($this->once())
- ->method('setSecure')
- ->with($secure)
- ->will($this->returnSelf());
- $cookieMetadata->expects($this->once())
- ->method('setHttpOnly')
- ->with($httpOnly)
- ->will($this->returnSelf());
-
- $this->cookieMetadataFactory->expects($this->once())
- ->method('createPublicCookieMetadata')
- ->will($this->returnValue($cookieMetadata));
-
- $this->cookieManager->expects($this->once())
- ->method('getCookie')
- ->with($name)
- ->will($this->returnValue($cookie));
- $this->cookieManager->expects($this->once())
- ->method('setPublicCookie')
- ->with($name, $cookie, $cookieMetadata);
-
- $this->sessionConfig->expects($this->once())
- ->method('getCookiePath')
- ->will($this->returnValue($path));
- $this->sessionConfig->expects($this->once())
- ->method('getCookieDomain')
- ->will($this->returnValue($domain));
- $this->sessionConfig->expects($this->once())
- ->method('getCookieSecure')
- ->will($this->returnValue($secure));
- $this->sessionConfig->expects($this->once())
- ->method('getCookieHttpOnly')
- ->will($this->returnValue($httpOnly));
-
- $this->session->prolong();
-
- $this->assertLessThanOrEqual(time(), $this->session->getUpdatedAt());
- }
-
- /**
- * @dataProvider isAllowedDataProvider
- * @param bool $isUserDefined
- * @param bool $isAclDefined
- * @param bool $isAllowed
- * @param true $expectedResult
- */
- public function testIsAllowed($isUserDefined, $isAclDefined, $isAllowed, $expectedResult)
- {
- $userAclRole = 'userAclRole';
- if ($isAclDefined) {
- $aclMock = $this->getMockBuilder(\Magento\Framework\Acl::class)->disableOriginalConstructor()->getMock();
- $this->storage->expects($this->any())->method('getAcl')->willReturn($aclMock);
- }
- if ($isUserDefined) {
- $userMock = $this->getMockBuilder(\Magento\User\Model\User::class)->disableOriginalConstructor()->getMock();
- $this->storage->expects($this->once())->method('getUser')->willReturn($userMock);
- }
- if ($isAclDefined && $isUserDefined) {
- $userMock->expects($this->any())->method('getAclRole')->willReturn($userAclRole);
- $aclMock->expects($this->once())->method('isAllowed')->with($userAclRole)->willReturn($isAllowed);
- }
-
- $this->assertEquals($expectedResult, $this->session->isAllowed('resource'));
- }
-
- /**
- * @return array
- */
- public function isAllowedDataProvider()
- {
- return [
- "Negative: User not defined" => [false, true, true, false],
- "Negative: Acl not defined" => [true, false, true, false],
- "Negative: Permission denied" => [true, true, false, false],
- "Positive: Permission granted" => [true, true, false, false],
- ];
- }
-
- /**
- * @dataProvider firstPageAfterLoginDataProvider
- * @param bool $isFirstPageAfterLogin
- */
- public function testFirstPageAfterLogin($isFirstPageAfterLogin)
- {
- $this->session->setIsFirstPageAfterLogin($isFirstPageAfterLogin);
- $this->assertEquals($isFirstPageAfterLogin, $this->session->isFirstPageAfterLogin());
- }
-
- /**
- * @return array
- */
- public function firstPageAfterLoginDataProvider()
- {
- return [
- 'First page after login' => [true],
- 'Not first page after login' => [false],
- ];
- }
-}
diff --git a/app/code/Magento/Backend/Test/Unit/Model/Authorization/RoleLocatorTest.php b/app/code/Magento/Backend/Test/Unit/Model/Authorization/RoleLocatorTest.php
deleted file mode 100644
index 77c428a6a116a..0000000000000
--- a/app/code/Magento/Backend/Test/Unit/Model/Authorization/RoleLocatorTest.php
+++ /dev/null
@@ -1,39 +0,0 @@
-_sessionMock = $this->createPartialMock(
- \Magento\Backend\Model\Auth\Session::class,
- ['getUser', 'getAclRole', 'hasUser']
- );
- $this->_model = new \Magento\Backend\Model\Authorization\RoleLocator($this->_sessionMock);
- }
-
- public function testGetAclRoleIdReturnsCurrentUserAclRoleId()
- {
- $this->_sessionMock->expects($this->once())->method('hasUser')->will($this->returnValue(true));
- $this->_sessionMock->expects($this->once())->method('getUser')->will($this->returnSelf());
- $this->_sessionMock->expects($this->once())->method('getAclRole')->will($this->returnValue('some_role'));
- $this->assertEquals('some_role', $this->_model->getAclRoleId());
- }
-}
diff --git a/app/code/Magento/Backend/Test/Unit/Model/Locale/ManagerTest.php b/app/code/Magento/Backend/Test/Unit/Model/Locale/ManagerTest.php
deleted file mode 100644
index ce2b65a2249ac..0000000000000
--- a/app/code/Magento/Backend/Test/Unit/Model/Locale/ManagerTest.php
+++ /dev/null
@@ -1,130 +0,0 @@
-_session = $this->createMock(\Magento\Backend\Model\Session::class);
-
- $this->_authSession = $this->createPartialMock(\Magento\Backend\Model\Auth\Session::class, ['getUser']);
-
- $this->_backendConfig = $this->getMockForAbstractClass(
- \Magento\Backend\App\ConfigInterface::class,
- [],
- '',
- false
- );
-
- $userMock = new \Magento\Framework\DataObject();
-
- $this->_authSession->expects($this->any())->method('getUser')->will($this->returnValue($userMock));
-
- $this->_translator = $this->getMockBuilder(\Magento\Framework\TranslateInterface::class)
- ->setMethods(['init', 'setLocale'])
- ->getMockForAbstractClass();
-
- $this->_translator->expects($this->any())->method('setLocale')->will($this->returnValue($this->_translator));
-
- $this->_translator->expects($this->any())->method('init')->will($this->returnValue(false));
-
- $this->_model = new \Magento\Backend\Model\Locale\Manager(
- $this->_session,
- $this->_authSession,
- $this->_translator,
- $this->_backendConfig
- );
- }
-
- /**
- * @return array
- */
- public function switchBackendInterfaceLocaleDataProvider()
- {
- return ['case1' => ['locale' => 'de_DE'], 'case2' => ['locale' => 'en_US']];
- }
-
- /**
- * @param string $locale
- * @dataProvider switchBackendInterfaceLocaleDataProvider
- * @covers \Magento\Backend\Model\Locale\Manager::switchBackendInterfaceLocale
- */
- public function testSwitchBackendInterfaceLocale($locale)
- {
- $this->_model->switchBackendInterfaceLocale($locale);
-
- $userInterfaceLocale = $this->_authSession->getUser()->getInterfaceLocale();
- $this->assertEquals($userInterfaceLocale, $locale);
-
- $sessionLocale = $this->_session->getSessionLocale();
- $this->assertEquals($sessionLocale, null);
- }
-
- /**
- * @covers \Magento\Backend\Model\Locale\Manager::getUserInterfaceLocale
- */
- public function testGetUserInterfaceLocaleDefault()
- {
- $locale = $this->_model->getUserInterfaceLocale();
-
- $this->assertEquals($locale, Resolver::DEFAULT_LOCALE);
- }
-
- /**
- * @covers \Magento\Backend\Model\Locale\Manager::getUserInterfaceLocale
- */
- public function testGetUserInterfaceLocale()
- {
- $this->_model->switchBackendInterfaceLocale('de_DE');
- $locale = $this->_model->getUserInterfaceLocale();
-
- $this->assertEquals($locale, 'de_DE');
- }
-
- /**
- * @covers \Magento\Backend\Model\Locale\Manager::getUserInterfaceLocale
- */
- public function testGetUserInterfaceGeneralLocale()
- {
- $this->_backendConfig->expects($this->any())
- ->method('getValue')
- ->with('general/locale/code')
- ->willReturn('test_locale');
- $locale = $this->_model->getUserInterfaceLocale();
- $this->assertEquals($locale, 'test_locale');
- }
-}
diff --git a/app/code/Magento/Backend/composer.json b/app/code/Magento/Backend/composer.json
index 4862e701404f7..5a7884a9607fe 100644
--- a/app/code/Magento/Backend/composer.json
+++ b/app/code/Magento/Backend/composer.json
@@ -22,6 +22,7 @@
"magento/module-store": "*",
"magento/module-translation": "*",
"magento/module-ui": "*",
+ "magento/module-authorization": "*",
"magento/module-user": "*"
},
"suggest": {
diff --git a/app/code/Magento/Backend/etc/di.xml b/app/code/Magento/Backend/etc/di.xml
index c526703da9975..41db85b9323a8 100644
--- a/app/code/Magento/Backend/etc/di.xml
+++ b/app/code/Magento/Backend/etc/di.xml
@@ -198,4 +198,8 @@
Magento\Backend\Block\AnchorRenderer
+
+
diff --git a/app/code/Magento/Backup/README.md b/app/code/Magento/Backup/README.md
index 59688ea3e716e..e1167bc4f2429 100644
--- a/app/code/Magento/Backup/README.md
+++ b/app/code/Magento/Backup/README.md
@@ -1,3 +1,28 @@
-The Backup module allows administrators to perform backups and rollbacks. Types of backups include system, database and media backups. This module relies on the Cron module to schedule backups.
+# Magento_Backup module
-This module does not affect the storefront.
+The Magento_Backup module allows administrators to perform backups and rollbacks. Types of backups include system, database and media backups. This module relies on the Cron module to schedule backups.
+
+The Magento_Backup module does not affect the storefront.
+
+For more information about this module, see [Magento Backups](https://docs.magento.com/m2/ce/user_guide/system/backups.html)
+
+## Extensibility
+
+Extension developers can interact with the Magento_Backup module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html).
+
+[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Backup module.
+
+### Layouts
+
+This module introduces the following layouts and layout handles in the `view/adminhtml/layout` directory:
+
+`backup_index_block`
+`backup_index_disabled`
+`backup_index_grid`
+`backup_index_index`
+
+For more information about layouts in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/layouts/layout-overview.html).
+
+## Additional information
+
+For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html).
diff --git a/app/code/Magento/Braintree/Gateway/Request/BillingAddressDataBuilder.php b/app/code/Magento/Braintree/Gateway/Request/BillingAddressDataBuilder.php
new file mode 100644
index 0000000000000..403c4d72fe358
--- /dev/null
+++ b/app/code/Magento/Braintree/Gateway/Request/BillingAddressDataBuilder.php
@@ -0,0 +1,112 @@
+subjectReader = $subjectReader;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function build(array $buildSubject)
+ {
+ $paymentDO = $this->subjectReader->readPayment($buildSubject);
+
+ $result = [];
+ $order = $paymentDO->getOrder();
+
+ $billingAddress = $order->getBillingAddress();
+ if ($billingAddress) {
+ $result[self::BILLING_ADDRESS] = [
+ self::REGION => $billingAddress->getRegionCode(),
+ self::POSTAL_CODE => $billingAddress->getPostcode(),
+ self::COUNTRY_CODE => $billingAddress->getCountryId(),
+ self::FIRST_NAME => $billingAddress->getFirstname(),
+ self::STREET_ADDRESS => $billingAddress->getStreetLine1(),
+ self::LAST_NAME => $billingAddress->getLastname(),
+ self::COMPANY => $billingAddress->getCompany(),
+ self::EXTENDED_ADDRESS => $billingAddress->getStreetLine2(),
+ self::LOCALITY => $billingAddress->getCity()
+ ];
+ }
+
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Braintree/README.md b/app/code/Magento/Braintree/README.md
index 8c34b7ae1af67..66d872e55a21a 100644
--- a/app/code/Magento/Braintree/README.md
+++ b/app/code/Magento/Braintree/README.md
@@ -1 +1,47 @@
-Module Magento\Braintree implements integration with the Braintree payment system.
\ No newline at end of file
+# Magento_Braintree module
+
+The Magento_Braintree module implements integration with the Braintree payment system.
+
+## Extensibility
+
+Extension developers can interact with the Magento_Braintree module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html).
+
+[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_Braintree module.
+
+### Events
+
+This module observes the following events:
+
+ - `payment_method_assign_data_braintree` event in `Magento\Braintree\Observer\DataAssignObserver` file.
+ - `payment_method_assign_data_braintree_paypal` event in `Magento\Braintree\Observer\DataAssignObserver` file.
+ - `shortcut_buttons_container` event in `Magento\Braintree\Observer\AddPaypalShortcuts` file.
+
+For information about an event in Magento 2, see [Events and observers](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/events-and-observers.html#events).
+
+### Layouts
+
+This module interacts with the following layouts and layout handles in the `view/adminhtml/layout` directory:
+
+- `braintree_paypal_review`
+- `checkout_index_index`
+- `multishipping_checkout_billing`
+- `vault_cards_listaction`
+
+This module interacts with the following layout handles in the `view/frontend/layout` directory:
+
+- `adminhtml_system_config_edit`
+- `braintree_report_index`
+- `sales_order_create_index`
+- `sales_order_create_load_block_billing_method`
+
+For more information about layouts in Magento 2, see the [Layout documentation](https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/layouts/layout-overview.html).
+
+### UI components
+
+You can extend admin notifications using the `view/adminhtml/ui_component/braintree_report.xml` configuration file.
+
+For information about UI components in Magento 2, see [Overview of UI components](https://devdocs.magento.com/guides/v2.3/ui_comp_guide/bk-ui_comps.html).
+
+## Additional information
+
+For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html).
diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ErrorCodeProviderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ErrorCodeProviderTest.php
index cddb4852da0e3..605e9253fe2cc 100644
--- a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ErrorCodeProviderTest.php
+++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ErrorCodeProviderTest.php
@@ -74,9 +74,9 @@ public function getErrorCodeDataProvider(): array
'errors' => [],
'transaction' => [
'status' => 'processor_declined',
- 'processorResponseCode' => '1000'
+ 'processorResponseCode' => '2059'
],
- 'expectedResult' => ['1000']
+ 'expectedResult' => ['2059']
],
[
'errors' => [
diff --git a/app/code/Magento/Braintree/composer.json b/app/code/Magento/Braintree/composer.json
index 5b5eeaf2b3dd7..58049f7bf0f93 100644
--- a/app/code/Magento/Braintree/composer.json
+++ b/app/code/Magento/Braintree/composer.json
@@ -22,11 +22,11 @@
"magento/module-sales": "*",
"magento/module-ui": "*",
"magento/module-vault": "*",
- "magento/module-multishipping": "*"
+ "magento/module-multishipping": "*",
+ "magento/module-theme": "*"
},
"suggest": {
- "magento/module-checkout-agreements": "*",
- "magento/module-theme": "*"
+ "magento/module-checkout-agreements": "*"
},
"type": "magento2-module",
"license": [
diff --git a/app/code/Magento/Braintree/etc/braintree_error_mapping.xml b/app/code/Magento/Braintree/etc/braintree_error_mapping.xml
index 7155264b4e6ad..bffcc75705938 100644
--- a/app/code/Magento/Braintree/etc/braintree_error_mapping.xml
+++ b/app/code/Magento/Braintree/etc/braintree_error_mapping.xml
@@ -21,6 +21,7 @@
Cardholder name is too long.
CVV verification failed.
CVV verification failed.
+ Address Verification Failed.
Postal code verification failed.
Credit card number is prohibited.
Addresses must have at least one field filled in.
diff --git a/app/code/Magento/Braintree/etc/di.xml b/app/code/Magento/Braintree/etc/di.xml
index f6ad552fc9bef..ef12b5eae6835 100644
--- a/app/code/Magento/Braintree/etc/di.xml
+++ b/app/code/Magento/Braintree/etc/di.xml
@@ -381,7 +381,7 @@
- Magento\Braintree\Gateway\Request\CustomerDataBuilder
- Magento\Braintree\Gateway\Request\PaymentDataBuilder
- Magento\Braintree\Gateway\Request\ChannelDataBuilder
- - Magento\Braintree\Gateway\Request\AddressDataBuilder
+ - Magento\Braintree\Gateway\Request\BillingAddressDataBuilder
- Magento\Braintree\Gateway\Request\DescriptorDataBuilder
- Magento\Braintree\Gateway\Request\StoreConfigBuilder
- Magento\Braintree\Gateway\Request\MerchantAccountDataBuilder
diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js
index ae9f69c405c2b..ea5200e4ba51f 100644
--- a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js
+++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/paypal.js
@@ -17,7 +17,8 @@ define([
'Magento_Vault/js/view/payment/vault-enabler',
'Magento_Checkout/js/action/create-billing-address',
'Magento_Braintree/js/view/payment/kount',
- 'mage/translate'
+ 'mage/translate',
+ 'Magento_Ui/js/model/messageList'
], function (
$,
_,
@@ -31,7 +32,8 @@ define([
VaultEnabler,
createBillingAddress,
kount,
- $t
+ $t,
+ globalMessageList
) {
'use strict';
@@ -334,7 +336,7 @@ define([
}
return {
- line1: address.street[0],
+ line1: _.isUndefined(address.street) || _.isUndefined(address.street[0]) ? '' : address.street[0],
city: address.city,
state: address.regionCode,
postalCode: address.postcode,
@@ -415,6 +417,18 @@ define([
*/
onVaultPaymentTokenEnablerChange: function () {
this.reInitPayPal();
+ },
+
+ /**
+ * Show error message
+ *
+ * @param {String} errorMessage
+ * @private
+ */
+ showError: function (errorMessage) {
+ globalMessageList.addErrorMessage({
+ message: errorMessage
+ });
}
});
});
diff --git a/app/code/Magento/BraintreeGraphQl/etc/schema.graphqls b/app/code/Magento/BraintreeGraphQl/etc/schema.graphqls
index 0492f8aaf989b..08bd10fd4c2dd 100644
--- a/app/code/Magento/BraintreeGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/BraintreeGraphQl/etc/schema.graphqls
@@ -2,7 +2,7 @@
# See COPYING.txt for license details.
type Mutation {
- createBraintreeClientToken: String! @resolver(class: "\\Magento\\BraintreeGraphQl\\Model\\Resolver\\CreateBraintreeClientToken") @doc(description:"Creates Braintree Client Token for creating client-side nonce.")
+ createBraintreeClientToken: String! @resolver(class: "\\Magento\\BraintreeGraphQl\\Model\\Resolver\\CreateBraintreeClientToken") @doc(description:"Creates Client Token for Braintree Javascript SDK initialization.")
}
input PaymentMethodInput {
@@ -11,9 +11,9 @@ input PaymentMethodInput {
}
input BraintreeInput {
- payment_method_nonce: String!
- is_active_payment_token_enabler: Boolean!
- device_data: String
+ payment_method_nonce: String! @doc(description:"The one-time payment token generated by Braintree payment gateway based on card details. Required field to make sale transaction.")
+ is_active_payment_token_enabler: Boolean! @doc(description:"States whether an entered by a customer credit/debit card should be tokenized for later usage. Required only if Vault is enabled for Braintree payment integration.")
+ device_data: String @doc(description:"Contains a fingerprint provided by Braintree JS SDK and should be sent with sale transaction details to the Braintree payment gateway. Should be specified only in a case if Kount (advanced fraud protection) is enabled for Braintree payment integration.")
}
input BraintreeCcVaultInput {
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml
new file mode 100644
index 0000000000000..505a319c5c44f
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml
new file mode 100644
index 0000000000000..18316e41241e4
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml
@@ -0,0 +1,197 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 100.00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 10.00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 500.00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Bundle/etc/db_schema.xml b/app/code/Magento/Bundle/etc/db_schema.xml
index 97e86e5c17359..8eafa23cbd0f3 100644
--- a/app/code/Magento/Bundle/etc/db_schema.xml
+++ b/app/code/Magento/Bundle/etc/db_schema.xml
@@ -10,9 +10,9 @@
+ comment="Option ID"/>
+ comment="Parent ID"/>
+ comment="Value ID"/>
+ comment="Option ID"/>
+ comment="Store ID"/>
+ comment="Parent Product ID"/>
@@ -54,13 +54,13 @@
+ comment="Selection ID"/>
+ comment="Option ID"/>
+ comment="Parent Product ID"/>
+ comment="Product ID"/>
+ comment="Selection ID"/>
+ comment="Website ID"/>
+ comment="Parent Product ID"/>
@@ -122,7 +122,7 @@
+ comment="Website ID"/>
+ default="0" comment="Option ID"/>
@@ -246,9 +246,9 @@
+ default="0" comment="Option ID"/>
+ default="0" comment="Selection ID"/>
+ default="0" comment="Option ID"/>
+ default="0" comment="Selection ID"/>
+ default="0" comment="Option ID"/>
+ default="0" comment="Option ID"/>
getControllerAction();
if (!$captcha->isCorrect($this->captchaStringResolver->resolve($controller->getRequest(), $formId))) {
- $this->messageManager->addError(__('Incorrect CAPTCHA.'));
+ $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA.'));
$this->getDataPersistor()->set($formId, $controller->getRequest()->getPostValue());
$this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true);
$this->redirect->redirect($controller->getResponse(), 'contact/index/index');
diff --git a/app/code/Magento/Captcha/Observer/CheckForgotpasswordObserver.php b/app/code/Magento/Captcha/Observer/CheckForgotpasswordObserver.php
index 0736c7514a568..623d11903926e 100644
--- a/app/code/Magento/Captcha/Observer/CheckForgotpasswordObserver.php
+++ b/app/code/Magento/Captcha/Observer/CheckForgotpasswordObserver.php
@@ -7,6 +7,9 @@
use Magento\Framework\Event\ObserverInterface;
+/**
+ * Class CheckForgotpasswordObserver
+ */
class CheckForgotpasswordObserver implements ObserverInterface
{
/**
@@ -69,7 +72,7 @@ public function execute(\Magento\Framework\Event\Observer $observer)
/** @var \Magento\Framework\App\Action\Action $controller */
$controller = $observer->getControllerAction();
if (!$captchaModel->isCorrect($this->captchaStringResolver->resolve($controller->getRequest(), $formId))) {
- $this->messageManager->addError(__('Incorrect CAPTCHA'));
+ $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA'));
$this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true);
$this->redirect->redirect($controller->getResponse(), '*/*/forgotpassword');
}
diff --git a/app/code/Magento/Captcha/Observer/CheckUserCreateObserver.php b/app/code/Magento/Captcha/Observer/CheckUserCreateObserver.php
index 6d2ed4d1050ca..ef66116432f55 100644
--- a/app/code/Magento/Captcha/Observer/CheckUserCreateObserver.php
+++ b/app/code/Magento/Captcha/Observer/CheckUserCreateObserver.php
@@ -7,6 +7,11 @@
use Magento\Framework\Event\ObserverInterface;
+/**
+ * Class CheckUserCreateObserver
+ *
+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
+ */
class CheckUserCreateObserver implements ObserverInterface
{
/**
@@ -86,7 +91,7 @@ public function execute(\Magento\Framework\Event\Observer $observer)
/** @var \Magento\Framework\App\Action\Action $controller */
$controller = $observer->getControllerAction();
if (!$captchaModel->isCorrect($this->captchaStringResolver->resolve($controller->getRequest(), $formId))) {
- $this->messageManager->addError(__('Incorrect CAPTCHA'));
+ $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA'));
$this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true);
$this->_session->setCustomerFormData($controller->getRequest()->getPostValue());
$url = $this->_urlManager->getUrl('*/*/create', ['_nosecret' => true]);
diff --git a/app/code/Magento/Captcha/Observer/CheckUserEditObserver.php b/app/code/Magento/Captcha/Observer/CheckUserEditObserver.php
index 9d3cd8d367093..872bbec4ffa56 100644
--- a/app/code/Magento/Captcha/Observer/CheckUserEditObserver.php
+++ b/app/code/Magento/Captcha/Observer/CheckUserEditObserver.php
@@ -11,13 +11,12 @@
use Magento\Framework\App\Config\ScopeConfigInterface;
/**
- * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * Class CheckUserEditObserver
+ *
+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
*/
class CheckUserEditObserver implements ObserverInterface
{
- /**
- * Form ID
- */
const FORM_ID = 'user_edit';
/**
@@ -96,7 +95,8 @@ public function __construct(
* Check Captcha On Forgot Password Page
*
* @param \Magento\Framework\Event\Observer $observer
- * @return $this
+ * @return $this|void
+ * @throws \Magento\Framework\Exception\SessionException
*/
public function execute(\Magento\Framework\Event\Observer $observer)
{
@@ -119,9 +119,9 @@ public function execute(\Magento\Framework\Event\Observer $observer)
'The account is locked. Please wait and try again or contact %1.',
$this->scopeConfig->getValue('contact/email/recipient_email')
);
- $this->messageManager->addError($message);
+ $this->messageManager->addErrorMessage($message);
}
- $this->messageManager->addError(__('Incorrect CAPTCHA'));
+ $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA'));
$this->actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true);
$this->redirect->redirect($controller->getResponse(), '*/*/edit');
}
diff --git a/app/code/Magento/Captcha/Observer/CheckUserForgotPasswordBackendObserver.php b/app/code/Magento/Captcha/Observer/CheckUserForgotPasswordBackendObserver.php
index 2de93dcf6b59b..e11e48a527169 100644
--- a/app/code/Magento/Captcha/Observer/CheckUserForgotPasswordBackendObserver.php
+++ b/app/code/Magento/Captcha/Observer/CheckUserForgotPasswordBackendObserver.php
@@ -7,6 +7,11 @@
use Magento\Framework\Event\ObserverInterface;
+/**
+ * Class CheckUserForgotPasswordBackendObserver
+ *
+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
+ */
class CheckUserForgotPasswordBackendObserver implements ObserverInterface
{
/**
@@ -76,7 +81,7 @@ public function execute(\Magento\Framework\Event\Observer $observer)
) {
$this->_session->setEmail((string)$controller->getRequest()->getPost('email'));
$this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true);
- $this->messageManager->addError(__('Incorrect CAPTCHA'));
+ $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA'));
$controller->getResponse()->setRedirect(
$controller->getUrl('*/*/forgotpassword', ['_nosecret' => true])
);
diff --git a/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php b/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php
index dd4974c5d842c..27507423e77eb 100644
--- a/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php
+++ b/app/code/Magento/Captcha/Observer/CheckUserLoginObserver.php
@@ -6,10 +6,10 @@
namespace Magento\Captcha\Observer;
+use Magento\Customer\Api\CustomerRepositoryInterface;
use Magento\Customer\Model\AuthenticationInterface;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\Exception\NoSuchEntityException;
-use Magento\Customer\Api\CustomerRepositoryInterface;
/**
* Check captcha on user login page observer.
@@ -64,6 +64,8 @@ class CheckUserLoginObserver implements ObserverInterface
protected $authentication;
/**
+ * CheckUserLoginObserver constructor.
+ *
* @param \Magento\Captcha\Helper\Data $helper
* @param \Magento\Framework\App\ActionFlag $actionFlag
* @param \Magento\Framework\Message\ManagerInterface $messageManager
@@ -125,8 +127,7 @@ private function getAuthentication()
* Check captcha on user login page
*
* @param \Magento\Framework\Event\Observer $observer
- * @throws NoSuchEntityException
- * @return $this
+ * @return $this|void
*/
public function execute(\Magento\Framework\Event\Observer $observer)
{
@@ -143,10 +144,11 @@ public function execute(\Magento\Framework\Event\Observer $observer)
try {
$customer = $this->getCustomerRepository()->get($login);
$this->getAuthentication()->processAuthenticationFailure($customer->getId());
+ // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock
} catch (NoSuchEntityException $e) {
//do nothing as customer existence is validated later in authenticate method
}
- $this->messageManager->addError(__('Incorrect CAPTCHA'));
+ $this->messageManager->addErrorMessage(__('Incorrect CAPTCHA'));
$this->_actionFlag->set('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true);
$this->_session->setUsername($login);
$beforeUrl = $this->_session->getBeforeAuthUrl();
diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckContactUsFormObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckContactUsFormObserverTest.php
index 08f76aa74ac6d..83bfb2910f9f8 100644
--- a/app/code/Magento/Captcha/Test/Unit/Observer/CheckContactUsFormObserverTest.php
+++ b/app/code/Magento/Captcha/Test/Unit/Observer/CheckContactUsFormObserverTest.php
@@ -69,7 +69,10 @@ protected function setUp()
$this->messageManagerMock = $this->createMock(\Magento\Framework\Message\ManagerInterface::class);
$this->redirectMock = $this->createMock(\Magento\Framework\App\Response\RedirectInterface::class);
$this->captchaStringResolverMock = $this->createMock(\Magento\Captcha\Observer\CaptchaStringResolver::class);
- $this->sessionMock = $this->createPartialMock(\Magento\Framework\Session\SessionManager::class, ['addError']);
+ $this->sessionMock = $this->createPartialMock(
+ \Magento\Framework\Session\SessionManager::class,
+ ['addErrorMessage']
+ );
$this->dataPersistorMock = $this->getMockBuilder(\Magento\Framework\App\Request\DataPersistorInterface::class)
->getMockForAbstractClass();
@@ -116,7 +119,7 @@ public function testCheckContactUsFormWhenCaptchaIsRequiredAndValid()
$this->helperMock->expects($this->any())
->method('getCaptcha')
->with($formId)->willReturn($this->captchaMock);
- $this->sessionMock->expects($this->never())->method('addError');
+ $this->sessionMock->expects($this->never())->method('addErrorMessage');
$this->checkContactUsFormObserver->execute(
new \Magento\Framework\Event\Observer(['controller_action' => $controller])
@@ -163,7 +166,7 @@ public function testCheckContactUsFormRedirectsCustomerWithWarningMessageWhenCap
->method('getCaptcha')
->with($formId)
->willReturn($this->captchaMock);
- $this->messageManagerMock->expects($this->once())->method('addError')->with($warningMessage);
+ $this->messageManagerMock->expects($this->once())->method('addErrorMessage')->with($warningMessage);
$this->actionFlagMock->expects($this->once())
->method('set')
->with('', \Magento\Framework\App\Action\Action::FLAG_NO_DISPATCH, true);
diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckForgotpasswordObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckForgotpasswordObserverTest.php
index b05a3b2e34af0..93b58191cc334 100644
--- a/app/code/Magento/Captcha/Test/Unit/Observer/CheckForgotpasswordObserverTest.php
+++ b/app/code/Magento/Captcha/Test/Unit/Observer/CheckForgotpasswordObserverTest.php
@@ -138,7 +138,7 @@ public function testCheckForgotpasswordRedirects()
)->will(
$this->returnValue($this->_captcha)
);
- $this->_messageManager->expects($this->once())->method('addError')->with($warningMessage);
+ $this->_messageManager->expects($this->once())->method('addErrorMessage')->with($warningMessage);
$this->_actionFlag->expects(
$this->once()
)->method(
diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserCreateObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserCreateObserverTest.php
index 8dc67437f4879..a57faabda99eb 100644
--- a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserCreateObserverTest.php
+++ b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserCreateObserverTest.php
@@ -151,7 +151,7 @@ public function testCheckUserCreateRedirectsError()
)->will(
$this->returnValue($this->_captcha)
);
- $this->_messageManager->expects($this->once())->method('addError')->with($warningMessage);
+ $this->_messageManager->expects($this->once())->method('addErrorMessage')->with($warningMessage);
$this->_actionFlag->expects(
$this->once()
)->method(
diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserEditObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserEditObserverTest.php
index 26fd8fd928c56..0f08e5c569dfc 100644
--- a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserEditObserverTest.php
+++ b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserEditObserverTest.php
@@ -146,7 +146,7 @@ public function testExecute()
$message = __('The account is locked. Please wait and try again or contact %1.', $email);
$this->messageManagerMock->expects($this->exactly(2))
- ->method('addError')
+ ->method('addErrorMessage')
->withConsecutive([$message], [__('Incorrect CAPTCHA')]);
$this->actionFlagMock->expects($this->once())
diff --git a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserLoginObserverTest.php b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserLoginObserverTest.php
index 19dc096b9ef66..0499ec3255c51 100644
--- a/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserLoginObserverTest.php
+++ b/app/code/Magento/Captcha/Test/Unit/Observer/CheckUserLoginObserverTest.php
@@ -145,7 +145,7 @@ public function testExecute()
->with($customerId);
$this->messageManagerMock->expects($this->once())
- ->method('addError')
+ ->method('addErrorMessage')
->with(__('Incorrect CAPTCHA'));
$this->actionFlagMock->expects($this->once())
diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Button/Back.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Button/Back.php
index db42bb66c9bd1..60e17599f6dec 100644
--- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Button/Back.php
+++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Button/Back.php
@@ -11,15 +11,32 @@
class Back extends Generic
{
/**
+ * Get Button Data
+ *
* @return array
*/
public function getButtonData()
{
return [
'label' => __('Back'),
- 'on_click' => sprintf("location.href = '%s';", $this->getUrl('*/*/')),
+ 'on_click' => sprintf("location.href = '%s';", $this->getBackUrl()),
'class' => 'back',
'sort_order' => 10
];
}
+ /**
+ * Get URL for back
+ *
+ * @return string
+ */
+ private function getBackUrl()
+ {
+ if ($this->context->getRequestParam('customerId')) {
+ return $this->getUrl(
+ 'customer/index/edit',
+ ['id' => $this->context->getRequestParam('customerId')]
+ );
+ }
+ return $this->getUrl('*/*/');
+ }
}
diff --git a/app/code/Magento/Catalog/Block/Widget/Link.php b/app/code/Magento/Catalog/Block/Widget/Link.php
index 85e50dbd3dc27..a25af297111d2 100644
--- a/app/code/Magento/Catalog/Block/Widget/Link.php
+++ b/app/code/Magento/Catalog/Block/Widget/Link.php
@@ -4,17 +4,15 @@
* See COPYING.txt for license details.
*/
-/**
- * Widget to display catalog link
- *
- * @author Magento Core Team
- */
namespace Magento\Catalog\Block\Widget;
use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator;
use Magento\UrlRewrite\Model\UrlFinderInterface;
use Magento\UrlRewrite\Service\V1\Data\UrlRewrite;
+/**
+ * Render the URL of given entity
+ */
class Link extends \Magento\Framework\View\Element\Html\Link implements \Magento\Widget\Block\BlockInterface
{
/**
@@ -63,10 +61,9 @@ public function __construct(
/**
* Prepare url using passed id path and return it
- * or return false if path was not found in url rewrites.
*
* @throws \RuntimeException
- * @return string|false
+ * @return string|false if path was not found in url rewrites.
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
public function getHref()
@@ -93,7 +90,7 @@ public function getHref()
if ($rewrite) {
$href = $store->getUrl('', ['_direct' => $rewrite->getRequestPath()]);
- if (strpos($href, '___store') === false) {
+ if ($this->addStoreCodeParam($store, $href)) {
$href .= (strpos($href, '?') === false ? '?' : '&') . '___store=' . $store->getCode();
}
}
@@ -102,6 +99,22 @@ public function getHref()
return $this->_href;
}
+ /**
+ * Checks whether store code query param should be appended to the URL
+ *
+ * @param \Magento\Store\Model\Store $store
+ * @param string $url
+ * @return bool
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ */
+ private function addStoreCodeParam(\Magento\Store\Model\Store $store, string $url): bool
+ {
+ return $this->getStoreId()
+ && !$store->isUseStoreInUrl()
+ && $store->getId() !== $this->_storeManager->getStore()->getId()
+ && strpos($url, '___store') === false;
+ }
+
/**
* Parse id_path
*
@@ -121,6 +134,7 @@ protected function parseIdPath($idPath)
/**
* Prepare label using passed text as parameter.
+ *
* If anchor text was not specified get entity name from DB.
*
* @return string
@@ -150,9 +164,8 @@ public function getLabel()
/**
* Render block HTML
- * or return empty string if url can't be prepared
*
- * @return string
+ * @return string empty string if url can't be prepared
*/
protected function _toHtml()
{
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Add.php b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Add.php
index 733e270174e4c..8af59dfeaf76a 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Category/Add.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Category/Add.php
@@ -6,31 +6,41 @@
*/
namespace Magento\Catalog\Controller\Adminhtml\Category;
-use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface;
+use Magento\Backend\App\Action\Context;
+use Magento\Backend\Model\View\Result\Page;
+use Magento\Framework\Controller\ResultFactory;
+use Magento\Backend\Model\View\Result\Redirect;
+use Magento\Framework\Controller\ResultInterface;
+use Magento\Catalog\Controller\Adminhtml\Category;
+use Magento\Backend\Model\View\Result\ForwardFactory;
+use Magento\Framework\App\Action\HttpGetActionInterface;
/**
* Class Add Category
*
* @package Magento\Catalog\Controller\Adminhtml\Category
*/
-class Add extends \Magento\Catalog\Controller\Adminhtml\Category implements HttpGetActionInterface
+class Add extends Category implements HttpGetActionInterface
{
/**
* Forward factory for result
*
- * @var \Magento\Backend\Model\View\Result\ForwardFactory
+ * @deprecated Unused Class: ForwardFactory
+ * @see $this->resultFactory->create()
+ * @var ForwardFactory
+ *
*/
protected $resultForwardFactory;
/**
* Add category constructor
*
- * @param \Magento\Backend\App\Action\Context $context
- * @param \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory
+ * @param Context $context
+ * @param ForwardFactory $resultForwardFactory
*/
public function __construct(
- \Magento\Backend\App\Action\Context $context,
- \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory
+ Context $context,
+ ForwardFactory $resultForwardFactory
) {
parent::__construct($context);
$this->resultForwardFactory = $resultForwardFactory;
@@ -39,7 +49,7 @@ public function __construct(
/**
* Add new category form
*
- * @return \Magento\Backend\Model\View\Result\Forward
+ * @return ResultInterface
*/
public function execute()
{
@@ -47,7 +57,7 @@ public function execute()
$category = $this->_initCategory(true);
if (!$category || !$parentId || $category->getId()) {
- /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
+ /** @var Redirect $resultRedirect */
$resultRedirect = $this->resultRedirectFactory->create();
return $resultRedirect->setPath('catalog/*/', ['_current' => true, 'id' => null]);
}
@@ -61,9 +71,8 @@ public function execute()
$category->addData($categoryData);
}
- $resultPageFactory = $this->_objectManager->get(\Magento\Framework\View\Result\PageFactory::class);
- /** @var \Magento\Backend\Model\View\Result\Page $resultPage */
- $resultPage = $resultPageFactory->create();
+ /** @var Page $resultPage */
+ $resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE);
if ($this->getRequest()->getQuery('isAjax')) {
return $this->ajaxRequestResponse($category, $resultPage);
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/StockDataFilter.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/StockDataFilter.php
index f7e69bc72ea18..47324c5b70908 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/StockDataFilter.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/StockDataFilter.php
@@ -7,6 +7,7 @@
use Magento\CatalogInventory\Api\StockConfigurationInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
+use Magento\CatalogInventory\Model\Stock;
/**
* Class StockDataFilter
@@ -60,8 +61,8 @@ public function filter(array $stockData)
$stockData['qty'] = self::MAX_QTY_VALUE;
}
- if (isset($stockData['min_qty']) && (int)$stockData['min_qty'] < 0) {
- $stockData['min_qty'] = 0;
+ if (isset($stockData['min_qty'])) {
+ $stockData['min_qty'] = $this->purifyMinQty($stockData['min_qty'], $stockData['backorders']);
}
if (!isset($stockData['is_decimal_divided']) || $stockData['is_qty_decimal'] == 0) {
@@ -70,4 +71,27 @@ public function filter(array $stockData)
return $stockData;
}
+
+ /**
+ * Purifies min_qty.
+ *
+ * @param int $minQty
+ * @param int $backOrders
+ * @return float
+ */
+ private function purifyMinQty(int $minQty, int $backOrders): float
+ {
+ /**
+ * As described in the documentation if the Backorders Option is disabled
+ * it is recommended to set the Out Of Stock Threshold to a positive number.
+ * That's why to clarify the logic to the end user the code below prevent him to set a negative number so such
+ * a number will turn to zero.
+ * @see https://docs.magento.com/m2/ce/user_guide/catalog/inventory-backorders.html
+ */
+ if ($backOrders === Stock::BACKORDERS_NO && $minQty < 0) {
+ $minQty = 0;
+ }
+
+ return (float)$minQty;
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Layer/FilterList.php b/app/code/Magento/Catalog/Model/Layer/FilterList.php
index 9d7b71c981c6b..b8e9b8ad4aaa5 100644
--- a/app/code/Magento/Catalog/Model/Layer/FilterList.php
+++ b/app/code/Magento/Catalog/Model/Layer/FilterList.php
@@ -3,9 +3,13 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
namespace Magento\Catalog\Model\Layer;
+/**
+ * Layer navigation filters
+ */
class FilterList
{
const CATEGORY_FILTER = 'category';
@@ -106,9 +110,9 @@ protected function getAttributeFilterClass(\Magento\Catalog\Model\ResourceModel\
{
$filterClassName = $this->filterTypes[self::ATTRIBUTE_FILTER];
- if ($attribute->getAttributeCode() == 'price') {
+ if ($attribute->getFrontendInput() === 'price') {
$filterClassName = $this->filterTypes[self::PRICE_FILTER];
- } elseif ($attribute->getBackendType() == 'decimal') {
+ } elseif ($attribute->getBackendType() === 'decimal') {
$filterClassName = $this->filterTypes[self::DECIMAL_FILTER];
}
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php
index 2b4739ebeb736..6ac48c565e842 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php
@@ -159,7 +159,7 @@ public function prepareForCart()
if ($this->_dateExists()) {
if ($this->useCalendar()) {
- $timestamp += $this->_localeDate->date($value['date'], null, true, false)->getTimestamp();
+ $timestamp += $this->_localeDate->date($value['date'], null, false, false)->getTimestamp();
} else {
$timestamp += mktime(0, 0, 0, $value['month'], $value['day'], $value['year']);
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php
index 786cec391c460..797ce72ae9b7a 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php
@@ -9,13 +9,15 @@
*
* @author Magento Core Team
*/
+declare(strict_types=1);
+
namespace Magento\Catalog\Model\ResourceModel;
use Magento\Catalog\Model\Indexer\Category\Product\Processor;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\DataObject;
use Magento\Framework\EntityManager\EntityManager;
-use Magento\Catalog\Model\Category as CategoryEntity;
+use Magento\Catalog\Setup\CategorySetup;
/**
* Resource model for category entity
@@ -92,6 +94,7 @@ class Category extends AbstractResource
* @var Processor
*/
private $indexerProcessor;
+
/**
* Category constructor.
* @param \Magento\Eav\Model\Entity\Context $context
@@ -275,7 +278,7 @@ protected function _beforeSave(\Magento\Framework\DataObject $object)
if ($object->getPosition() === null) {
$object->setPosition($this->_getMaxPosition($object->getPath()) + 1);
}
- $path = explode('/', $object->getPath());
+ $path = explode('/', (string)$object->getPath());
$level = count($path) - ($object->getId() ? 1 : 0);
$toUpdateChild = array_diff($path, [$object->getId()]);
@@ -314,7 +317,7 @@ protected function _afterSave(\Magento\Framework\DataObject $object)
/**
* Add identifier for new category
*/
- if (substr($object->getPath(), -1) == '/') {
+ if (substr((string)$object->getPath(), -1) == '/') {
$object->setPath($object->getPath() . $object->getId());
$this->_savePath($object);
}
@@ -352,7 +355,7 @@ protected function _getMaxPosition($path)
{
$connection = $this->getConnection();
$positionField = $connection->quoteIdentifier('position');
- $level = count(explode('/', $path));
+ $level = count(explode('/', (string)$path));
$bind = ['c_level' => $level, 'c_path' => $path . '/%'];
$select = $connection->select()->from(
$this->getTable('catalog_category_entity'),
@@ -717,7 +720,7 @@ public function getCategories($parent, $recursionLevel = 0, $sorted = false, $as
*/
public function getParentCategories($category)
{
- $pathIds = array_reverse(explode(',', $category->getPathInStore()));
+ $pathIds = array_reverse(explode(',', (string)$category->getPathInStore()));
/** @var \Magento\Catalog\Model\ResourceModel\Category\Collection $categories */
$categories = $this->_categoryCollectionFactory->create();
return $categories->setStore(
@@ -1134,4 +1137,44 @@ private function getAggregateCount()
}
return $this->aggregateCount;
}
+
+ /**
+ * Get category with children.
+ *
+ * @param int $categoryId
+ * @return array
+ */
+ public function getCategoryWithChildren(int $categoryId): array
+ {
+ $connection = $this->getConnection();
+
+ $selectAttributeCode = $connection->select()
+ ->from(
+ ['eav_attribute' => $this->getTable('eav_attribute')],
+ ['attribute_id']
+ )->where('entity_type_id = ?', CategorySetup::CATEGORY_ENTITY_TYPE_ID)
+ ->where('attribute_code = ?', 'is_anchor')
+ ->limit(1);
+ $isAnchorAttributeCode = $connection->fetchOne($selectAttributeCode);
+ if (empty($isAnchorAttributeCode) || (int)$isAnchorAttributeCode <= 0) {
+ return [];
+ }
+
+ $select = $connection->select()
+ ->from(
+ ['cce' => $this->getTable('catalog_category_entity')],
+ ['entity_id', 'parent_id', 'path']
+ )->join(
+ ['cce_int' => $this->getTable('catalog_category_entity_int')],
+ 'cce.entity_id = cce_int.entity_id',
+ ['is_anchor' => 'cce_int.value']
+ )->where(
+ 'cce_int.attribute_id = ?',
+ $isAnchorAttributeCode
+ )->where(
+ "cce.path LIKE '%/{$categoryId}' OR cce.path LIKE '%/{$categoryId}/%'"
+ )->order('path');
+
+ return $connection->fetchAll($select);
+ }
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php
index 23f612582f42e..355561c5e384d 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php
@@ -845,6 +845,9 @@ public function afterDelete()
/**
* @inheritdoc
* @since 100.0.9
+ *
+ * @SuppressWarnings(PHPMD.SerializationAware)
+ * @deprecated Do not use PHP serialization.
*/
public function __sleep()
{
@@ -858,6 +861,9 @@ public function __sleep()
/**
* @inheritdoc
* @since 100.0.9
+ *
+ * @SuppressWarnings(PHPMD.SerializationAware)
+ * @deprecated Do not use PHP serialization.
*/
public function __wakeup()
{
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php
index dbd6a7a2e1094..42d55892b6ec6 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php
@@ -3,6 +3,7 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
namespace Magento\Catalog\Model\ResourceModel\Product;
@@ -22,6 +23,7 @@
use Magento\Framework\Indexer\DimensionFactory;
use Magento\Store\Model\Indexer\WebsiteDimensionProvider;
use Magento\Store\Model\Store;
+use Magento\Catalog\Model\ResourceModel\Category;
/**
* Product collection
@@ -302,6 +304,11 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac
*/
private $urlFinder;
+ /**
+ * @var Category
+ */
+ private $categoryResourceModel;
+
/**
* Collection constructor
*
@@ -330,6 +337,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac
* @param TableMaintainer|null $tableMaintainer
* @param PriceTableResolver|null $priceTableResolver
* @param DimensionFactory|null $dimensionFactory
+ * @param Category|null $categoryResourceModel
*
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
@@ -358,7 +366,8 @@ public function __construct(
MetadataPool $metadataPool = null,
TableMaintainer $tableMaintainer = null,
PriceTableResolver $priceTableResolver = null,
- DimensionFactory $dimensionFactory = null
+ DimensionFactory $dimensionFactory = null,
+ Category $categoryResourceModel = null
) {
$this->moduleManager = $moduleManager;
$this->_catalogProductFlatState = $catalogProductFlatState;
@@ -392,6 +401,8 @@ public function __construct(
$this->priceTableResolver = $priceTableResolver ?: ObjectManager::getInstance()->get(PriceTableResolver::class);
$this->dimensionFactory = $dimensionFactory
?: ObjectManager::getInstance()->get(DimensionFactory::class);
+ $this->categoryResourceModel = $categoryResourceModel ?: ObjectManager::getInstance()
+ ->get(Category::class);
}
/**
@@ -1673,7 +1684,11 @@ public function addFilterByRequiredOptions()
public function setVisibility($visibility)
{
$this->_productLimitationFilters['visibility'] = $visibility;
- $this->_applyProductLimitations();
+ if ($this->getStoreId() == Store::DEFAULT_STORE_ID) {
+ $this->addAttributeToFilter('visibility', $visibility);
+ } else {
+ $this->_applyProductLimitations();
+ }
return $this;
}
@@ -2053,12 +2068,13 @@ protected function _applyProductLimitations()
protected function _applyZeroStoreProductLimitations()
{
$filters = $this->_productLimitationFilters;
+ $categories = $this->getChildrenCategories((int)$filters['category_id']);
$conditions = [
'cat_pro.product_id=e.entity_id',
$this->getConnection()->quoteInto(
- 'cat_pro.category_id=?',
- $filters['category_id']
+ 'cat_pro.category_id IN (?)',
+ $categories
),
];
$joinCond = join(' AND ', $conditions);
@@ -2079,6 +2095,39 @@ protected function _applyZeroStoreProductLimitations()
return $this;
}
+ /**
+ * Get children categories.
+ *
+ * @param int $categoryId
+ * @return array
+ */
+ private function getChildrenCategories(int $categoryId): array
+ {
+ $categoryIds[] = $categoryId;
+ $anchorCategory = [];
+
+ $categories = $this->categoryResourceModel->getCategoryWithChildren($categoryId);
+ if (empty($categories)) {
+ return $categoryIds;
+ }
+
+ $firstCategory = array_shift($categories);
+ if ($firstCategory['is_anchor'] == 1) {
+ $anchorCategory[] = (int)$firstCategory['entity_id'];
+ foreach ($categories as $category) {
+ if (in_array($category['parent_id'], $categoryIds)
+ && in_array($category['parent_id'], $anchorCategory)) {
+ $categoryIds[] = (int)$category['entity_id'];
+ if ($category['is_anchor'] == 1) {
+ $anchorCategory[] = (int)$category['entity_id'];
+ }
+ }
+ }
+ }
+
+ return $categoryIds;
+ }
+
/**
* Add category ids to loaded items
*
diff --git a/app/code/Magento/Catalog/Plugin/Block/Topmenu.php b/app/code/Magento/Catalog/Plugin/Block/Topmenu.php
index 44f9193ab4012..b4aa5bd960b01 100644
--- a/app/code/Magento/Catalog/Plugin/Block/Topmenu.php
+++ b/app/code/Magento/Catalog/Plugin/Block/Topmenu.php
@@ -3,6 +3,8 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\Catalog\Plugin\Block;
use Magento\Catalog\Model\Category;
@@ -156,12 +158,13 @@ private function getCurrentCategory()
*/
private function getCategoryAsArray($category, $currentCategory, $isParentActive)
{
+ $categoryId = $category->getId();
return [
'name' => $category->getName(),
- 'id' => 'category-node-' . $category->getId(),
+ 'id' => 'category-node-' . $categoryId,
'url' => $this->catalogCategory->getCategoryUrl($category),
- 'has_active' => in_array((string)$category->getId(), explode('/', $currentCategory->getPath()), true),
- 'is_active' => $category->getId() == $currentCategory->getId(),
+ 'has_active' => in_array((string)$categoryId, explode('/', (string)$currentCategory->getPath()), true),
+ 'is_active' => $categoryId == $currentCategory->getId(),
'is_category' => true,
'is_parent_active' => $isParentActive
];
@@ -193,4 +196,22 @@ protected function getCategoryTree($storeId, $rootId)
return $collection;
}
+
+ /**
+ * Add active
+ *
+ * @param \Magento\Theme\Block\Html\Topmenu $subject
+ * @param string[] $result
+ * @return string[]
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterGetCacheKeyInfo(\Magento\Theme\Block\Html\Topmenu $subject, array $result)
+ {
+ $activeCategory = $this->getCurrentCategory();
+ if ($activeCategory) {
+ $result[] = Category::CACHE_TAG . '_' . $activeCategory->getId();
+ }
+
+ return $result;
+ }
}
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml
index 12bf5179a07d0..b9b31b780c8f7 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryActionGroup.xml
@@ -99,7 +99,11 @@
-
+
+
+ /magento-logo(_[0-9]+)*?\.png$/
+ grabCategoryFileName
+
@@ -128,7 +132,11 @@
-
+
+
+ /magento-logo(_[0-9]+)*?\.png$/
+ grabCategoryFileName
+
@@ -396,4 +404,44 @@
+
+
+
+ Requires navigation to category creation/edit page. Assign products to category - using "Products in Category" tab.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Deletes all children categories of Default Root Category.
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml
index 0bb73e7416b07..5c5ee0f9cb321 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml
@@ -134,8 +134,8 @@
-
-
+
+
@@ -149,9 +149,10 @@
- EXTENDS: saveProductForm. Removes 'seeSaveConfirmation'.
+ EXTENDS: saveProductForm. Removes 'waitProductSaveSuccessMessage' and 'seeSaveConfirmation'.
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml
index 6a260bbf22522..aef79e651b584 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridActionGroup.xml
@@ -423,4 +423,23 @@
+
+
+
+ Deletes all products in Admin Products grid page.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductAbsentOnCategoryPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductAbsentOnCategoryPageActionGroup.xml
new file mode 100644
index 0000000000000..4c641b621a504
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductAbsentOnCategoryPageActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Navigate to category page and verify product is absent.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml
index 341a00d3158d6..9393669f6e46d 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryActionGroup.xml
@@ -78,6 +78,15 @@
+
+
+ EXTENDS:StorefrontCheckCategorySimpleProduct. Removes 'AssertProductPrice', 'moveMouseOverProduct', 'AssertAddToCart'
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryPageSortProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryPageSortProductActionGroup.xml
new file mode 100644
index 0000000000000..64dd2c97a382f
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCategoryPageSortProductActionGroup.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ Select "Sort by" parameter for sorting Products on Category page
+
+
+
+
+
+
+
+
+ Set Ascending Direction for sorting Products on Category page
+
+
+
+
+
+ Set Descending Direction for sorting Products on Category page
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml
index 13951a0d197d1..6ffb4e1902424 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml
@@ -117,4 +117,14 @@
true
true
+
+ Default Category
+
+
+
+ CategoryExportImport
+
+
+ CustomAttributeCategoryNonAnchor
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml
index 1684bd0c8a2c3..7bd392f0aa74a 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/CustomAttributeData.xml
@@ -51,4 +51,8 @@
short_description
Short Fixedtest 555
+
+ is_anchor
+ 0
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml
index a2bdaa7dbc62f..e4ffdbde4368d 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/FrontendLabelData.xml
@@ -20,4 +20,8 @@
0
attributeThree
+
+ 0
+ attributeExportImport
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml
index 1f4b1470098e2..6e40499d0efeb 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ImageContentData.xml
@@ -18,4 +18,7 @@
image/png
magento-logo.png
+
+ magento-logo.png
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml
index 2deec6b8c1f8e..6614fa4b5dbeb 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml
@@ -278,6 +278,28 @@
false
ProductAttributeFrontendLabel
+
+ attribute
+ price
+ global
+ false
+ false
+ false
+ true
+ false
+ false
+ true
+ true
+ false
+ false
+ false
+ true
+ false
+ false
+ false
+ false
+ ProductAttributeFrontendLabel
+
text
defaultValue
@@ -376,4 +398,9 @@
Size
size_attr
+
+
+ attribute
+ ProductAttributeFrontendLabelForExportImport
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml
index 98c9a70e6aad4..75b4ef773a934 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMediaGalleryEntryData.xml
@@ -30,4 +30,9 @@
false
MagentoLogoImageContent
+
+
+ Magento Logo
+ MagentoLogoImageContentExportImport
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml
index fcb56cf298a98..bb0e85bcbb40b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeOptionData.xml
@@ -86,4 +86,11 @@
White
white
+
+
+ option1
+
+
+ option2
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml
index 517ab253b8238..e122615eb8aa4 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml
@@ -351,6 +351,11 @@
adobe-base
jpg
+
+ test_image
+ test_image.jpg
+ test_image
+
霁产品
simple
@@ -1165,6 +1170,15 @@
EavStock10
CustomAttributeProductAttribute
+
+
+ api-simple-one-export-import
+ Api Simple Product One Export Import
+
+
+ api-simple-two-export-import
+ Api Simple Product Two Export Import
+
simple-product_
simple
diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml
index e4c4ece5ac6cf..5c7cb4d51084f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminProductCreatePage.xml
@@ -18,7 +18,6 @@
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCatalogStorefrontConfigSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCatalogStorefrontConfigSection.xml
new file mode 100644
index 0000000000000..d0200f1e0a5b0
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCatalogStorefrontConfigSection.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml
index e3d224904671b..1cb095974d0fd 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryContentSection.xml
@@ -24,5 +24,6 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml
index fba28b3feaff1..bc552721e6ab8 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml
@@ -11,6 +11,8 @@
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml
index 80b4159167453..98b23a4669b72 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection.xml
@@ -215,6 +215,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryFilterSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryFilterSection.xml
index ddec4428f90e2..faee605e77319 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryFilterSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryFilterSection.xml
@@ -11,5 +11,6 @@
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml
index f3d3e653b260b..ee105320c5f29 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml
@@ -16,9 +16,6 @@
-
-
-
@@ -78,6 +75,8 @@
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminBackorderAllowedAddProductToCartTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminBackorderAllowedAddProductToCartTest.xml
index 88c524eff387c..679cab4159ebd 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminBackorderAllowedAddProductToCartTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminBackorderAllowedAddProductToCartTest.xml
@@ -23,13 +23,11 @@
-
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml
index f40a62c164ecc..1b72458747067 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml
@@ -41,6 +41,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -67,18 +77,36 @@
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
-
+
@@ -86,91 +114,92 @@
-
+
+
-
-
+
+
-
+
-
-
+
+
-
-
+
-
-
+
+
+
-
-
+
+
-
-
+
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
+
+
+
-
+
-
+
-
-
+
+
-
-
+
+
-
+
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml
index 4d581bae700d7..f5ad5b8079d1f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml
@@ -10,7 +10,7 @@
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd">
-
+
@@ -24,8 +24,8 @@
-
-
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKey.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKey.xml
new file mode 100644
index 0000000000000..bae81513de632
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKey.xml
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml
new file mode 100644
index 0000000000000..a11646cc46875
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml
new file mode 100644
index 0000000000000..ae54b72a5a702
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml
@@ -0,0 +1,336 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Category A
+
+
+
+
+
+
+
+
+ TEST
+
+
+ _test2
+
+
+ test 3
+
+
+ Category with several products
+
+
+ test 5
+
+
+ test 8
+
+
+ This is a very very very very very looong title
+
+
+ test 6
+
+
+ test 7
+
+
+ test 4
+
+
+ Category with image
+
+
+ test 0
+
+
+ Category with description & custom title
+
+
+ Category with children
+
+
+ level 1 test category very very very long name
+
+
+
+ level 1 test category name
+
+
+
+ level 1 with children
+
+
+
+ level 2 with children
+
+
+
+ level 3 test
+
+
+
+ level 4
+
+
+
+ level 4 test
+
+
+
+ level 5
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryHighlightedAndProductDisplayedTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryHighlightedAndProductDisplayedTest.xml
new file mode 100644
index 0000000000000..3e72df9133898
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryHighlightedAndProductDisplayedTest.xml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml
new file mode 100644
index 0000000000000..ac2605ff5f3e2
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml
@@ -0,0 +1,201 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml
new file mode 100644
index 0000000000000..6184a220f047c
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml
@@ -0,0 +1,226 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Widget/LinkTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Widget/LinkTest.php
index 8333ed22e1da0..dcbd3161733aa 100644
--- a/app/code/Magento/Catalog/Test/Unit/Block/Widget/LinkTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Block/Widget/LinkTest.php
@@ -5,54 +5,81 @@
*/
namespace Magento\Catalog\Test\Unit\Block\Widget;
+use Exception;
+use Magento\Catalog\Block\Widget\Link;
+use Magento\Catalog\Model\ResourceModel\AbstractResource;
use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator;
+use Magento\Framework\App\Config\ReinitableConfigInterface;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
+use Magento\Framework\Url;
+use Magento\Framework\Url\ModifierInterface;
+use Magento\Framework\View\Element\Template\Context;
+use Magento\Store\Model\Store;
+use Magento\Store\Model\StoreManagerInterface;
+use Magento\UrlRewrite\Model\UrlFinderInterface;
use Magento\UrlRewrite\Service\V1\Data\UrlRewrite;
+use PHPUnit\Framework\TestCase;
+use PHPUnit_Framework_MockObject_MockObject;
+use ReflectionClass;
+use RuntimeException;
-class LinkTest extends \PHPUnit\Framework\TestCase
+/**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ * @SuppressWarnings(PHPMD.UnusedLocalVariable)
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ */
+class LinkTest extends TestCase
{
/**
- * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\Store\Model\StoreManagerInterface
+ * @var PHPUnit_Framework_MockObject_MockObject|StoreManagerInterface
*/
protected $storeManager;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject|\Magento\UrlRewrite\Model\UrlFinderInterface
+ * @var PHPUnit_Framework_MockObject_MockObject|UrlFinderInterface
*/
protected $urlFinder;
/**
- * @var \Magento\Catalog\Block\Widget\Link
+ * @var Link
*/
protected $block;
/**
- * @var \Magento\Catalog\Model\ResourceModel\AbstractResource|\PHPUnit_Framework_MockObject_MockObject
+ * @var AbstractResource|PHPUnit_Framework_MockObject_MockObject
*/
protected $entityResource;
+ /**
+ * @inheritDoc
+ */
protected function setUp()
{
- $this->storeManager = $this->createMock(\Magento\Store\Model\StoreManagerInterface::class);
- $this->urlFinder = $this->createMock(\Magento\UrlRewrite\Model\UrlFinderInterface::class);
+ $this->storeManager = $this->createMock(StoreManagerInterface::class);
+ $this->urlFinder = $this->createMock(UrlFinderInterface::class);
- $context = $this->createMock(\Magento\Framework\View\Element\Template\Context::class);
+ $context = $this->createMock(Context::class);
$context->expects($this->any())
->method('getStoreManager')
->will($this->returnValue($this->storeManager));
$this->entityResource =
- $this->createMock(\Magento\Catalog\Model\ResourceModel\AbstractResource::class);
-
- $this->block = (new ObjectManager($this))->getObject(\Magento\Catalog\Block\Widget\Link::class, [
- 'context' => $context,
- 'urlFinder' => $this->urlFinder,
- 'entityResource' => $this->entityResource
- ]);
+ $this->createMock(AbstractResource::class);
+
+ $this->block = (new ObjectManager($this))->getObject(
+ Link::class,
+ [
+ 'context' => $context,
+ 'urlFinder' => $this->urlFinder,
+ 'entityResource' => $this->entityResource
+ ]
+ );
}
/**
- * @expectedException \RuntimeException
+ * Tests getHref with wrong id_path
+ *
+ * @expectedException RuntimeException
* @expectedExceptionMessage Parameter id_path is not set.
*/
public function testGetHrefWithoutSetIdPath()
@@ -61,7 +88,9 @@ public function testGetHrefWithoutSetIdPath()
}
/**
- * @expectedException \RuntimeException
+ * Tests getHref with wrong id_path
+ *
+ * @expectedException RuntimeException
* @expectedExceptionMessage Wrong id_path structure.
*/
public function testGetHrefIfSetWrongIdPath()
@@ -70,27 +99,30 @@ public function testGetHrefIfSetWrongIdPath()
$this->block->getHref();
}
+ /**
+ * Tests getHref with wrong store ID
+ *
+ * @expectedException Exception
+ */
public function testGetHrefWithSetStoreId()
{
$this->block->setData('id_path', 'type/id');
$this->block->setData('store_id', 'store_id');
-
$this->storeManager->expects($this->once())
- ->method('getStore')->with('store_id')
- // interrupt test execution
- ->will($this->throwException(new \Exception()));
-
- try {
- $this->block->getHref();
- } catch (\Exception $e) {
- }
+ ->method('getStore')
+ ->with('store_id')
+ ->will($this->throwException(new Exception()));
+ $this->block->getHref();
}
+ /**
+ * Tests getHref with not found URL
+ */
public function testGetHrefIfRewriteIsNotFound()
{
$this->block->setData('id_path', 'entity_type/entity_id');
- $store = $this->createPartialMock(\Magento\Store\Model\Store::class, ['getId', '__wakeUp']);
+ $store = $this->createPartialMock(Store::class, ['getId', '__wakeUp']);
$store->expects($this->any())
->method('getId');
@@ -105,52 +137,107 @@ public function testGetHrefIfRewriteIsNotFound()
}
/**
- * @param string $url
- * @param string $separator
+ * Tests getHref whether it should include the store code or not
+ *
* @dataProvider dataProviderForTestGetHrefWithoutUrlStoreSuffix
+ * @param string $path
+ * @param int|null $storeId
+ * @param bool $includeStoreCode
+ * @param string $expected
+ * @throws \ReflectionException
*/
- public function testGetHrefWithoutUrlStoreSuffix($url, $separator)
- {
- $storeId = 15;
- $storeCode = 'store-code';
- $requestPath = 'request-path';
+ public function testStoreCodeShouldBeIncludedInURLOnlyIfItIsConfiguredSo(
+ string $path,
+ ?int $storeId,
+ bool $includeStoreCode,
+ string $expected
+ ) {
$this->block->setData('id_path', 'entity_type/entity_id');
-
- $rewrite = $this->createMock(\Magento\UrlRewrite\Service\V1\Data\UrlRewrite::class);
- $rewrite->expects($this->once())
- ->method('getRequestPath')
- ->will($this->returnValue($requestPath));
-
- $store = $this->createPartialMock(
- \Magento\Store\Model\Store::class,
- ['getId', 'getUrl', 'getCode', '__wakeUp']
+ $this->block->setData('store_id', $storeId);
+ $objectManager = new ObjectManager($this);
+
+ $rewrite = $this->createPartialMock(UrlRewrite::class, ['getRequestPath']);
+ $url = $this->createPartialMock(Url::class, ['setScope', 'getUrl']);
+ $urlModifier = $this->getMockForAbstractClass(ModifierInterface::class);
+ $config = $this->getMockForAbstractClass(ReinitableConfigInterface::class);
+ $store = $objectManager->getObject(
+ Store::class,
+ [
+ 'storeManager' => $this->storeManager,
+ 'url' => $url,
+ 'config' => $config
+ ]
);
- $store->expects($this->once())
- ->method('getId')
- ->will($this->returnValue($storeId));
- $store->expects($this->once())
+ $property = (new ReflectionClass(get_class($store)))->getProperty('urlModifier');
+ $property->setAccessible(true);
+ $property->setValue($store, $urlModifier);
+
+ $urlModifier->expects($this->any())
+ ->method('execute')
+ ->willReturnArgument(0);
+ $config->expects($this->any())
+ ->method('getValue')
+ ->willReturnMap(
+ [
+ [Store::XML_PATH_USE_REWRITES, ReinitableConfigInterface::SCOPE_TYPE_DEFAULT, null, true],
+ [
+ Store::XML_PATH_STORE_IN_URL,
+ ReinitableConfigInterface::SCOPE_TYPE_DEFAULT,
+ null, $includeStoreCode
+ ]
+ ]
+ );
+
+ $url->expects($this->any())
+ ->method('setScope')
+ ->willReturnSelf();
+
+ $url->expects($this->any())
->method('getUrl')
- ->with('', ['_direct' => $requestPath])
- ->will($this->returnValue($url));
- $store->expects($this->once())
- ->method('getCode')
- ->will($this->returnValue($storeCode));
+ ->willReturnCallback(
+ function ($route, $params) use ($storeId) {
+ $baseUrl = rtrim($this->storeManager->getStore($storeId)->getBaseUrl(), '/');
+ return $baseUrl .'/' . ltrim($params['_direct'], '/');
+ }
+ );
- $this->storeManager->expects($this->once())
- ->method('getStore')
- ->will($this->returnValue($store));
+ $store->addData(['store_id' => 1, 'code' => 'french']);
- $this->urlFinder->expects($this->once())->method('findOneByData')
- ->with([
+ $store2 = clone $store;
+ $store2->addData(['store_id' => 2, 'code' => 'german']);
+
+ $this->storeManager
+ ->expects($this->any())
+ ->method('getStore')
+ ->willReturnMap(
+ [
+ [null, $store],
+ [1, $store],
+ [2, $store2],
+ ]
+ );
+
+ $this->urlFinder->expects($this->once())
+ ->method('findOneByData')
+ ->with(
+ [
UrlRewrite::ENTITY_ID => 'entity_id',
UrlRewrite::ENTITY_TYPE => 'entity_type',
- UrlRewrite::STORE_ID => $storeId,
- ])
+ UrlRewrite::STORE_ID => $this->storeManager->getStore($storeId)->getStoreId(),
+ ]
+ )
->will($this->returnValue($rewrite));
- $this->assertEquals($url . $separator . '___store=' . $storeCode, $this->block->getHref());
+ $rewrite->expects($this->once())
+ ->method('getRequestPath')
+ ->will($this->returnValue($path));
+
+ $this->assertEquals($expected, $this->block->getHref());
}
+ /**
+ * Tests getLabel with custom text
+ */
public function testGetLabelWithCustomText()
{
$customText = 'Some text';
@@ -158,6 +245,9 @@ public function testGetLabelWithCustomText()
$this->assertEquals($customText, $this->block->getLabel());
}
+ /**
+ * Tests getLabel without custom text
+ */
public function testGetLabelWithoutCustomText()
{
$category = 'Some text';
@@ -178,17 +268,25 @@ public function testGetLabelWithoutCustomText()
public function dataProviderForTestGetHrefWithoutUrlStoreSuffix()
{
return [
- ['url', '?'],
- ['url?some_parameter', '&'],
+ ['/accessories.html', null, true, 'french/accessories.html'],
+ ['/accessories.html', null, false, '/accessories.html'],
+ ['/accessories.html', 1, true, 'french/accessories.html'],
+ ['/accessories.html', 1, false, '/accessories.html'],
+ ['/accessories.html', 2, true, 'german/accessories.html'],
+ ['/accessories.html', 2, false, '/accessories.html?___store=german'],
+ ['/accessories.html?___store=german', 2, false, '/accessories.html?___store=german'],
];
}
+ /**
+ * Tests getHref with product entity and additional category id in the id_path
+ */
public function testGetHrefWithForProductWithCategoryIdParameter()
{
$storeId = 15;
$this->block->setData('id_path', ProductUrlRewriteGenerator::ENTITY_TYPE . '/entity_id/category_id');
- $store = $this->createPartialMock(\Magento\Store\Model\Store::class, ['getId', '__wakeUp']);
+ $store = $this->createPartialMock(Store::class, ['getId', '__wakeUp']);
$store->expects($this->any())
->method('getId')
->will($this->returnValue($storeId));
@@ -197,13 +295,16 @@ public function testGetHrefWithForProductWithCategoryIdParameter()
->method('getStore')
->will($this->returnValue($store));
- $this->urlFinder->expects($this->once())->method('findOneByData')
- ->with([
- UrlRewrite::ENTITY_ID => 'entity_id',
- UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE,
- UrlRewrite::STORE_ID => $storeId,
- UrlRewrite::METADATA => ['category_id' => 'category_id'],
- ])
+ $this->urlFinder->expects($this->once())
+ ->method('findOneByData')
+ ->with(
+ [
+ UrlRewrite::ENTITY_ID => 'entity_id',
+ UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE,
+ UrlRewrite::STORE_ID => $storeId,
+ UrlRewrite::METADATA => ['category_id' => 'category_id'],
+ ]
+ )
->will($this->returnValue(false));
$this->block->getHref();
diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/StockDataFilterTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/StockDataFilterTest.php
index 0214de8120bae..cb23d0c0a9a24 100644
--- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/StockDataFilterTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/StockDataFilterTest.php
@@ -6,11 +6,15 @@
namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Initialization;
use Magento\Catalog\Controller\Adminhtml\Product\Initialization\StockDataFilter;
+use Magento\CatalogInventory\Model\Stock;
+use Magento\Framework\App\Config\ScopeConfigInterface;
+use Magento\CatalogInventory\Model\Configuration;
+use PHPUnit\Framework\TestCase;
/**
- * Class StockDataFilterTest
+ * StockDataFilter test.
*/
-class StockDataFilterTest extends \PHPUnit\Framework\TestCase
+class StockDataFilterTest extends TestCase
{
/**
* @var \PHPUnit_Framework_MockObject_MockObject
@@ -27,17 +31,23 @@ class StockDataFilterTest extends \PHPUnit\Framework\TestCase
*/
protected $stockDataFilter;
- /** @var \PHPUnit_Framework_MockObject_MockObject */
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
protected $stockConfiguration;
+ /**
+ * @inheritdoc
+ */
protected function setUp()
{
- $this->scopeConfigMock = $this->createMock(\Magento\Framework\App\Config\ScopeConfigInterface::class);
+ $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class);
- $this->scopeConfigMock->expects($this->any())->method('getValue')->will($this->returnValue(1));
+ $this->scopeConfigMock->method('getValue')
+ ->will($this->returnValue(1));
$this->stockConfiguration = $this->createPartialMock(
- \Magento\CatalogInventory\Model\Configuration::class,
+ Configuration::class,
['getManageStock']
);
@@ -45,8 +55,11 @@ protected function setUp()
}
/**
+ * Tests filter method.
+ *
* @param array $inputStockData
* @param array $outputStockData
+ * @return void
*
* @covers \Magento\Catalog\Controller\Adminhtml\Product\Initialization\StockDataFilter::filter
* @dataProvider filterDataProvider
@@ -54,8 +67,7 @@ protected function setUp()
public function testFilter(array $inputStockData, array $outputStockData)
{
if (isset($inputStockData['use_config_manage_stock']) && $inputStockData['use_config_manage_stock'] === 1) {
- $this->stockConfiguration->expects($this->once())
- ->method('getManageStock')
+ $this->stockConfiguration->method('getManageStock')
->will($this->returnValue($outputStockData['manage_stock']));
}
@@ -93,8 +105,13 @@ public function filterDataProvider()
],
],
'case4' => [
- 'inputStockData' => ['min_qty' => -1],
- 'outputStockData' => ['min_qty' => 0, 'is_decimal_divided' => 0, 'use_config_manage_stock' => 0],
+ 'inputStockData' => ['min_qty' => -1, 'backorders' => Stock::BACKORDERS_NO],
+ 'outputStockData' => [
+ 'min_qty' => 0,
+ 'is_decimal_divided' => 0,
+ 'use_config_manage_stock' => 0,
+ 'backorders' => Stock::BACKORDERS_NO,
+ ],
],
'case5' => [
'inputStockData' => ['is_qty_decimal' => 0],
@@ -103,7 +120,25 @@ public function filterDataProvider()
'is_decimal_divided' => 0,
'use_config_manage_stock' => 0,
],
- ]
+ ],
+ 'case6' => [
+ 'inputStockData' => ['min_qty' => -1, 'backorders' => Stock::BACKORDERS_YES_NONOTIFY],
+ 'outputStockData' => [
+ 'min_qty' => -1,
+ 'is_decimal_divided' => 0,
+ 'use_config_manage_stock' => 0,
+ 'backorders' => Stock::BACKORDERS_YES_NONOTIFY,
+ ],
+ ],
+ 'case7' => [
+ 'inputStockData' => ['min_qty' => -1, 'backorders' => Stock::BACKORDERS_YES_NOTIFY],
+ 'outputStockData' => [
+ 'min_qty' => -1,
+ 'is_decimal_divided' => 0,
+ 'use_config_manage_stock' => 0,
+ 'backorders' => Stock::BACKORDERS_YES_NOTIFY,
+ ],
+ ],
];
}
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Layer/FilterListTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Layer/FilterListTest.php
index 8733f305ce091..731c5efd99746 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Layer/FilterListTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Layer/FilterListTest.php
@@ -3,6 +3,7 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
namespace Magento\Catalog\Test\Unit\Model\Layer;
@@ -72,9 +73,13 @@ public function testGetFilters($method, $value, $expectedClass)
$this->objectManagerMock->expects($this->at(1))
->method('create')
- ->with($expectedClass, [
- 'data' => ['attribute_model' => $this->attributeMock],
- 'layer' => $this->layerMock])
+ ->with(
+ $expectedClass,
+ [
+ 'data' => ['attribute_model' => $this->attributeMock],
+ 'layer' => $this->layerMock
+ ]
+ )
->will($this->returnValue('filter'));
$this->attributeMock->expects($this->once())
@@ -95,8 +100,8 @@ public function getFiltersDataProvider()
{
return [
[
- 'method' => 'getAttributeCode',
- 'value' => FilterList::PRICE_FILTER,
+ 'method' => 'getFrontendInput',
+ 'value' => 'price',
'expectedClass' => 'PriceFilterClass',
],
[
@@ -105,8 +110,8 @@ public function getFiltersDataProvider()
'expectedClass' => 'DecimalFilterClass',
],
[
- 'method' => 'getAttributeCode',
- 'value' => null,
+ 'method' => 'getFrontendInput',
+ 'value' => 'text',
'expectedClass' => 'AttributeFilterClass',
]
];
diff --git a/app/code/Magento/Catalog/composer.json b/app/code/Magento/Catalog/composer.json
index fa8daaabe5710..8023634fa074d 100644
--- a/app/code/Magento/Catalog/composer.json
+++ b/app/code/Magento/Catalog/composer.json
@@ -31,8 +31,7 @@
"magento/module-ui": "*",
"magento/module-url-rewrite": "*",
"magento/module-widget": "*",
- "magento/module-wishlist": "*",
- "magento/module-authorization": "*"
+ "magento/module-wishlist": "*"
},
"suggest": {
"magento/module-cookie": "*",
diff --git a/app/code/Magento/Catalog/etc/config.xml b/app/code/Magento/Catalog/etc/config.xml
index 3a842166a3825..20511f4ff2295 100644
--- a/app/code/Magento/Catalog/etc/config.xml
+++ b/app/code/Magento/Catalog/etc/config.xml
@@ -23,9 +23,9 @@
grid-list
- 9,15,30
+ 12,24,36
5,10,15,20,25
- 9
+ 12
10
0
position
diff --git a/app/code/Magento/Catalog/etc/db_schema.xml b/app/code/Magento/Catalog/etc/db_schema.xml
index 6fef4ca6e9128..3d17db7a66666 100644
--- a/app/code/Magento/Catalog/etc/db_schema.xml
+++ b/app/code/Magento/Catalog/etc/db_schema.xml
@@ -812,7 +812,7 @@
+ comment="Record ID"/>
@@ -1085,7 +1085,7 @@
+ default="0" comment="Original entity ID for attribute value"/>
@@ -1114,7 +1114,7 @@
+ default="0" comment="Original entity ID for attribute value"/>
@@ -1205,7 +1205,7 @@
+ comment="Default store ID for website"/>
@@ -1452,7 +1452,7 @@
+ default="0" comment="Original entity ID for attribute value"/>
@@ -1481,7 +1481,7 @@
+ default="0" comment="Original entity ID for attribute value"/>
@@ -1510,7 +1510,7 @@
+ default="0" comment="Original entity ID for attribute value"/>
@@ -1539,7 +1539,7 @@
+ default="0" comment="Original entity ID for attribute value"/>
@@ -1681,7 +1681,7 @@
+ default="0" comment="Original entity ID for attribute value"/>
@@ -1710,7 +1710,7 @@
+ default="0" comment="Original entity ID for attribute value"/>
@@ -1801,14 +1801,14 @@
+ comment="Product Action ID"/>
+ comment="Visitor ID"/>
+ comment="Customer ID"/>
+ comment="Product ID"/>
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..ed657ca9a9980
--- /dev/null
+++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php
@@ -0,0 +1,141 @@
+collection = $collectionFactory->create();
+ $this->valueFactory = $valueFactory;
+ $this->customerRepository = $customerRepository;
+ }
+
+ /**
+ * @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->getCustomerGroupId($context);
+ }
+
+ /** @var Product $product */
+ $product = $value['model'];
+ $productId = $product->getId();
+ $this->productIds[] = $productId;
+ $that = $this;
+
+ return $this->valueFactory->create(
+ function () use ($that, $productId, $context) {
+ $tierPrices = [];
+ if (empty($that->productIds)) {
+ return [];
+ }
+ if (!$that->collection->isLoaded()) {
+ $that->collection->addIdFilter($that->productIds);
+ $that->collection->addTierPriceDataByGroupId($that->customerGroupId);
+ }
+ /** @var \Magento\Catalog\Model\Product $item */
+ foreach ($that->collection as $item) {
+ if ($item->getId() === $productId) {
+ // Try to extract all requested fields from the loaded collection data
+ foreach ($item->getTierPrices() as $tierPrice) {
+ $tierPrices[] = $tierPrice->getData();
+ }
+ }
+ }
+ return $tierPrices;
+ }
+ );
+ }
+
+ /**
+ * Get the customer group Id.
+ *
+ * @param \Magento\GraphQl\Model\Query\ContextInterface $context
+ *
+ * @return int
+ */
+ private function getCustomerGroupId(\Magento\GraphQl\Model\Query\ContextInterface $context)
+ {
+ $currentUserId = $context->getUserId();
+ if (!$currentUserId) {
+ $customerGroupId = GroupManagement::NOT_LOGGED_IN_ID;
+ } else {
+ try {
+ $customer = $this->customerRepository->getById($currentUserId);
+ } catch (NoSuchEntityException $e) {
+ throw new GraphQlNoSuchEntityException(
+ __('Customer with id "%customer_id" does not exist.', ['customer_id' => $currentUserId]),
+ $e
+ );
+ }
+ $customerGroupId = $customer->getGroupId();
+ }
+ return $customerGroupId;
+ }
+}
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..30bb86a7523ed
--- /dev/null
+++ b/app/code/Magento/CatalogCustomerGraphQl/composer.json
@@ -0,0 +1,24 @@
+{
+ "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/module-catalog": "*",
+ "magento/module-customer": "*",
+ "magento/framework": "*",
+ "magento/module-graph-ql": "*"
+ },
+ "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..a1b2e29579721
--- /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..8cf6c6f874cfb
--- /dev/null
+++ b/app/code/Magento/CatalogCustomerGraphQl/etc/schema.graphqls
@@ -0,0 +1,14 @@
+# Copyright © Magento, Inc. All rights reserved.
+# See COPYING.txt for license details.
+
+interface ProductInterface {
+ tier_prices: [ProductTierPrices] @doc(description: "An array of ProductTierPrices objects.") @resolver(class: "Magento\\CatalogCustomerGraphQl\\Model\\Resolver\\TierPrices")
+}
+
+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.")
+}
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..7781473128754
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php
@@ -0,0 +1,107 @@
+ [
+ * 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
+ * @return array
+ * @throws \Zend_Db_Statement_Exception
+ */
+ public function getOptions(array $optionIds): 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',
+ ]
+ )
+ ->joinInner(
+ ['options' => $this->resourceConnection->getTableName('eav_attribute_option')],
+ 'a.attribute_id = options.attribute_id',
+ []
+ )
+ ->joinInner(
+ ['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',
+ ]
+ )
+ ->where('option_value.option_id IN (?)', $optionIds);
+
+ 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..b70c9f6165fc6
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php
@@ -0,0 +1,157 @@
+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 = [];
+ foreach ($this->getAttributeBuckets($aggregation) as $bucket) {
+ $attributeOptionIds[] = \array_map(
+ function (AggregationValueInterface $value) {
+ return $value->getValue();
+ },
+ $bucket->getValues()
+ );
+ }
+
+ if (!$attributeOptionIds) {
+ return [];
+ }
+
+ return $this->attributeOptionProvider->getOptions(\array_merge(...$attributeOptionIds));
+ }
+}
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/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/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/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/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/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/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..3912bab05ebbe
--- /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') {
+ continue;
+ }
+ foreach ($node->selectionSet->selections as $selection) {
+ if ($selection->name->value !== 'items') {
+ continue;
+ }
+ $fieldNames[] = $this->collectProductFieldNames($selection, $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/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/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..485ae792193e3 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,16 @@
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..fe3413dc3b218 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,14 @@
+
+
+
+
+ - Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Price
+ - Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Category
+ - Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Attribute
+
+
+
diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls
index d8a3b5e744d17..140aab37c9ceb 100644
--- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls
@@ -4,10 +4,10 @@
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 (
@@ -58,13 +58,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")
@@ -93,7 +86,6 @@ interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\
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")
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")
gift_message_available: String @doc(description: "Indicates whether a gift message is available.")
manufacturer: Int @doc(description: "A number representing the product's manufacturer.")
@@ -221,7 +213,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")
}
@@ -270,7 +262,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 +273,11 @@ 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 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 +330,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 +364,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 +383,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.")
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..1ae993ed99060 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_';
@@ -1886,6 +1886,7 @@ protected function _saveProducts()
return $this;
}
+ //phpcs:enable Generic.Metrics.NestingLevel
/**
* Prepare array with image states (visible or hidden from product page)
@@ -2736,8 +2737,6 @@ protected function _saveValidatedBunches()
try {
$rowData = $source->current();
} catch (\InvalidArgumentException $e) {
- $this->addRowError($e->getMessage(), $this->_processedRowsCount);
- $this->_processedRowsCount++;
$source->next();
continue;
}
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/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml
new file mode 100644
index 0000000000000..88f6e6c9f9039
--- /dev/null
+++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml
@@ -0,0 +1,216 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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/CatalogInventory/Model/ResourceModel/Stock/Item.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php
index edccad60231ec..3a214bd8cd7cb 100644
--- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php
+++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Item.php
@@ -14,6 +14,7 @@
use Magento\Framework\DB\Select;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\Stdlib\DateTime\DateTime;
+use Zend_Db_Expr;
/**
* Stock item resource model
@@ -183,16 +184,12 @@ public function updateSetOutOfStock(int $websiteId)
'is_in_stock = ' . Stock::STOCK_IN_STOCK,
'(use_config_manage_stock = 1 AND 1 = ' . $this->stockConfiguration->getManageStock() . ')'
. ' OR (use_config_manage_stock = 0 AND manage_stock = 1)',
- '(use_config_min_qty = 1 AND qty <= ' . $this->stockConfiguration->getMinQty() . ')'
- . ' OR (use_config_min_qty = 0 AND qty <= min_qty)',
+ '(' . $this->getBackordersExpr() .' = 0 AND qty <= ' . $this->getMinQtyExpr() . ')'
+ . ' OR (' . $this->getBackordersExpr() .' != 0 AND '
+ . $this->getMinQtyExpr() . ' != 0 AND qty <= ' . $this->getMinQtyExpr() . ')',
'product_id IN (' . $select->assemble() . ')',
];
- $backordersWhere = '(use_config_backorders = 0 AND backorders = ' . Stock::BACKORDERS_NO . ')';
- if (Stock::BACKORDERS_NO == $this->stockConfiguration->getBackorders()) {
- $where[] = $backordersWhere . ' OR use_config_backorders = 1';
- } else {
- $where[] = $backordersWhere;
- }
+
$connection->update($this->getMainTable(), $values, $where);
$this->stockIndexerProcessor->markIndexerAsInvalid();
@@ -215,8 +212,8 @@ public function updateSetInStock(int $websiteId)
$where = [
'website_id = ' . $websiteId,
'stock_status_changed_auto = 1',
- '(use_config_min_qty = 1 AND qty > ' . $this->stockConfiguration->getMinQty() . ')'
- . ' OR (use_config_min_qty = 0 AND qty > min_qty)',
+ '(qty > ' . $this->getMinQtyExpr() . ')'
+ . ' OR (' . $this->getBackordersExpr() . ' != 0 AND ' . $this->getMinQtyExpr() . ' = 0)', // If infinite
'product_id IN (' . $select->assemble() . ')',
];
$manageStockWhere = '(use_config_manage_stock = 0 AND manage_stock = 1)';
@@ -304,12 +301,12 @@ public function getBackordersExpr(string $tableAlias = ''): \Zend_Db_Expr
}
/**
- * Get Minimum Sale Quantity Expression
+ * Get Minimum Sale Quantity Expression.
*
* @param string $tableAlias
- * @return \Zend_Db_Expr
+ * @return Zend_Db_Expr
*/
- public function getMinSaleQtyExpr(string $tableAlias = ''): \Zend_Db_Expr
+ public function getMinSaleQtyExpr(string $tableAlias = ''): Zend_Db_Expr
{
if ($tableAlias) {
$tableAlias .= '.';
@@ -323,6 +320,26 @@ public function getMinSaleQtyExpr(string $tableAlias = ''): \Zend_Db_Expr
return $itemMinSaleQty;
}
+ /**
+ * Get Min Qty Expression
+ *
+ * @param string $tableAlias
+ * @return Zend_Db_Expr
+ */
+ public function getMinQtyExpr(string $tableAlias = ''): Zend_Db_Expr
+ {
+ if ($tableAlias) {
+ $tableAlias .= '.';
+ }
+ $itemBackorders = $this->getConnection()->getCheckSql(
+ $tableAlias . 'use_config_min_qty = 1',
+ $this->stockConfiguration->getMinQty(),
+ $tableAlias . 'min_qty'
+ );
+
+ return $itemBackorders;
+ }
+
/**
* Build select for products with types from config
*
diff --git a/app/code/Magento/CatalogInventory/Model/StockStateProvider.php b/app/code/Magento/CatalogInventory/Model/StockStateProvider.php
index 6851b05aa56a6..74271cdd97bf8 100644
--- a/app/code/Magento/CatalogInventory/Model/StockStateProvider.php
+++ b/app/code/Magento/CatalogInventory/Model/StockStateProvider.php
@@ -72,14 +72,31 @@ public function __construct(
*/
public function verifyStock(StockItemInterface $stockItem)
{
+ // Manage stock, but qty is null
if ($stockItem->getQty() === null && $stockItem->getManageStock()) {
return false;
}
+
+ // Backorders are not allowed and qty reached min qty
if ($stockItem->getBackorders() == StockItemInterface::BACKORDERS_NO
&& $stockItem->getQty() <= $stockItem->getMinQty()
) {
return false;
}
+
+ $backordersAllowed = [Stock::BACKORDERS_YES_NONOTIFY, Stock::BACKORDERS_YES_NOTIFY];
+ if (in_array($stockItem->getBackorders(), $backordersAllowed)) {
+ // Infinite - let it be In stock
+ if ($stockItem->getMinQty() == 0) {
+ return true;
+ }
+
+ // qty reached min qty - let it stand Out Of Stock
+ if ($stockItem->getQty() <= $stockItem->getMinQty()) {
+ return false;
+ }
+ }
+
return true;
}
@@ -245,15 +262,17 @@ public function checkQty(StockItemInterface $stockItem, $qty)
if (!$stockItem->getManageStock()) {
return true;
}
+
+ $backordersAllowed = [Stock::BACKORDERS_YES_NONOTIFY, Stock::BACKORDERS_YES_NOTIFY];
+ // Infinite check
+ if ($stockItem->getMinQty() == 0 && in_array($stockItem->getBackorders(), $backordersAllowed)) {
+ return true;
+ }
+
if ($stockItem->getQty() - $stockItem->getMinQty() - $qty < 0) {
- switch ($stockItem->getBackorders()) {
- case \Magento\CatalogInventory\Model\Stock::BACKORDERS_YES_NONOTIFY:
- case \Magento\CatalogInventory\Model\Stock::BACKORDERS_YES_NOTIFY:
- break;
- default:
- return false;
- }
+ return false;
}
+
return true;
}
diff --git a/app/code/Magento/CatalogInventory/Model/System/Config/Backend/Minqty.php b/app/code/Magento/CatalogInventory/Model/System/Config/Backend/Minqty.php
index f49d41b5dd656..268f1846161d4 100644
--- a/app/code/Magento/CatalogInventory/Model/System/Config/Backend/Minqty.php
+++ b/app/code/Magento/CatalogInventory/Model/System/Config/Backend/Minqty.php
@@ -6,21 +6,36 @@
namespace Magento\CatalogInventory\Model\System\Config\Backend;
+use Magento\CatalogInventory\Model\Stock;
+
/**
- * Minimum product qty backend model
+ * Minimum product qty backend model.
*/
class Minqty extends \Magento\Framework\App\Config\Value
{
/**
- * Validate minimum product qty value
+ * Validate minimum product qty value.
*
* @return $this
*/
public function beforeSave()
{
parent::beforeSave();
- $minQty = (int) $this->getValue() >= 0 ? (int) $this->getValue() : (int) $this->getOldValue();
+ $minQty = (float)$this->getValue();
+
+ /**
+ * As described in the documentation if the Backorders Option is disabled
+ * it is recommended to set the Out Of Stock Threshold to a positive number.
+ * That's why to clarify the logic to the end user the code below prevent him to set a negative number so such
+ * a number will turn to zero.
+ * @see https://docs.magento.com/m2/ce/user_guide/catalog/inventory-backorders.html
+ */
+ if ($this->getFieldsetDataValue("backorders") == Stock::BACKORDERS_NO && $minQty < 0) {
+ $minQty = 0;
+ }
+
$this->setValue((string) $minQty);
+
return $this;
}
}
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/Data/CatalogInventryConfigData.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventryConfigData.xml
deleted file mode 100644
index 3a49b821ead5f..0000000000000
--- a/app/code/Magento/CatalogInventory/Test/Mftf/Data/CatalogInventryConfigData.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
- cataloginventory/options/can_subtract
- 0
- Yes
- 1
-
-
- cataloginventory/options/can_subtract
- 0
- No
- 0
-
-
\ No newline at end of file
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..f4b79b17b3fc3
--- /dev/null
+++ b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormSection.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
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/StockStateProviderTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/StockStateProviderTest.php
new file mode 100644
index 0000000000000..942d77063a8e3
--- /dev/null
+++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/StockStateProviderTest.php
@@ -0,0 +1,223 @@
+objectManager = new ObjectManager($this);
+
+ $this->stockItem = $this->getMockBuilder(StockItemInterface::class)
+ ->getMock();
+
+ $this->model = $this->objectManager->getObject(StockStateProvider::class);
+ }
+
+ /**
+ * Tests verifyStock method.
+ *
+ * @param int $qty
+ * @param int $backOrders
+ * @param int $minQty
+ * @param int $manageStock
+ * @param int $expected
+ *
+ * @return void
+ *
+ * @dataProvider stockItemDataProvider
+ * @covers \Magento\CatalogInventory\Model\StockStateProvider::verifyStock
+ */
+ public function testVerifyStock(
+ ?int $qty,
+ ?int $backOrders,
+ ?int $minQty,
+ ?int $manageStock,
+ bool $expected
+ ): void {
+ $this->stockItem->method('getQty')
+ ->willReturn($qty);
+ $this->stockItem->method('getBackOrders')
+ ->willReturn($backOrders);
+ $this->stockItem->method('getMinQty')
+ ->willReturn($minQty);
+ $this->stockItem->method('getManageStock')
+ ->willReturn($manageStock);
+
+ $result = $this->model->verifyStock($this->stockItem);
+
+ self::assertEquals($expected, $result);
+ }
+
+ /**
+ * StockItem data provider.
+ *
+ * @return array
+ */
+ public function stockItemDataProvider(): array
+ {
+ return [
+ 'qty_is_null_manage_stock_on' => [
+ 'qty' => null,
+ 'backorders' => null,
+ 'min_qty' => null,
+ 'manage_stock' => 1,
+ 'expected' => false,
+ ],
+ 'qty_reached_threshold_without_backorders' => [
+ 'qty' => 3,
+ 'backorders' => Stock::BACKORDERS_NO,
+ 'min_qty' => 3,
+ 'manage_stock' => 1,
+ 'expected' => false,
+ ],
+ 'backorders_are_ininite' => [
+ 'qty' => -100,
+ 'backorders' => Stock::BACKORDERS_YES_NONOTIFY,
+ 'min_qty' => 0,
+ 'manage_stock' => 1,
+ 'expected' => true,
+ ],
+ 'limited_backorders_and_qty_reached_threshold' => [
+ 'qty' => -100,
+ 'backorders' => Stock::BACKORDERS_YES_NONOTIFY,
+ 'min_qty' => -100,
+ 'manage_stock' => 1,
+ 'expected' => false,
+ ],
+ 'qty_not_yet_reached_threshold_1' => [
+ 'qty' => -99,
+ 'backorders' => Stock::BACKORDERS_YES_NONOTIFY,
+ 'min_qty' => -100,
+ 'manage_stock' => 1,
+ 'expected' => true,
+ ],
+ 'qty_not_yet_reached_threshold_2' => [
+ 'qty' => 1,
+ 'backorders' => Stock::BACKORDERS_NO,
+ 'min_qty' => 0,
+ 'manage_stock' => 1,
+ 'expected' => true,
+ ],
+ ];
+ }
+
+ /**
+ * Tests checkQty method.
+ *
+ * @return void
+ *
+ * @dataProvider stockItemAndQtyDataProvider
+ * @covers \Magento\CatalogInventory\Model\StockStateProvider::verifyStock
+ */
+ public function testCheckQty(
+ bool $manageStock,
+ int $qty,
+ int $minQty,
+ int $backOrders,
+ int $orderQty,
+ bool $expected
+ ): void {
+ $this->stockItem->method('getManageStock')
+ ->willReturn($manageStock);
+ $this->stockItem->method('getQty')
+ ->willReturn($qty);
+ $this->stockItem->method('getMinQty')
+ ->willReturn($minQty);
+ $this->stockItem->method('getBackOrders')
+ ->willReturn($backOrders);
+
+ $result = $this->model->checkQty($this->stockItem, $orderQty);
+
+ self::assertEquals($expected, $result);
+ }
+
+ /**
+ * StockItem and qty data provider.
+ *
+ * @return array
+ */
+ public function stockItemAndQtyDataProvider(): array
+ {
+ return [
+ 'disabled_manage_stock' => [
+ 'manage_stock' => false,
+ 'qty' => 0,
+ 'min_qty' => 0,
+ 'backorders' => 0,
+ 'order_qty' => 0,
+ 'expected' => true,
+ ],
+ 'infinite_backorders' => [
+ 'manage_stock' => true,
+ 'qty' => -100,
+ 'min_qty' => 0,
+ 'backorders' => Stock::BACKORDERS_YES_NONOTIFY,
+ 'order_qty' => 100,
+ 'expected' => true,
+ ],
+ 'qty_reached_threshold' => [
+ 'manage_stock' => true,
+ 'qty' => -100,
+ 'min_qty' => -100,
+ 'backorders' => Stock::BACKORDERS_YES_NOTIFY,
+ 'order_qty' => 1,
+ 'expected' => false,
+ ],
+ 'qty_yet_not_reached_threshold' => [
+ 'manage_stock' => true,
+ 'qty' => -100,
+ 'min_qty' => -100,
+ 'backorders' => Stock::BACKORDERS_YES_NOTIFY,
+ 'order_qty' => 1,
+ 'expected' => false,
+ ]
+ ];
+ }
+
+ /**
+ * Tests checkQty method when check is not applicable.
+ *
+ * @return void
+ */
+ public function testCheckQtyWhenCheckIsNotApplicable(): void
+ {
+ $model = $this->objectManager->getObject(StockStateProvider::class, ['qtyCheckApplicable' => false]);
+
+ $result = $model->checkQty($this->stockItem, 3);
+
+ self::assertTrue($result);
+ }
+}
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..d903d09dd1f85 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"/>
[GLOBAL]
+ true
true
Maximum Qty Allowed in Shopping Cart
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/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/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/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/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..edc65490a3c67 100644
--- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php
+++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php
@@ -391,7 +391,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..14305359a71b3 100644
--- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php
+++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php
@@ -485,12 +485,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 +505,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/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/Test/Mftf/Test/LayerNavigationOfCatalogSearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/LayerNavigationOfCatalogSearchTest.xml
new file mode 100644
index 0000000000000..210b474af2e02
--- /dev/null
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/LayerNavigationOfCatalogSearchTest.xml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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/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/etc/di.xml b/app/code/Magento/CatalogSearch/etc/di.xml
index 28d5035308dee..da0a60dad1f77 100644
--- a/app/code/Magento/CatalogSearch/etc/di.xml
+++ b/app/code/Magento/CatalogSearch/etc/di.xml
@@ -281,6 +281,7 @@
\Magento\CatalogSearch\Model\Search\RequestGenerator\General
- Magento\CatalogSearch\Model\Search\RequestGenerator\Decimal
+ - Magento\CatalogSearch\Model\Search\RequestGenerator\Price
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/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..e99f89477e807 100644
--- a/app/code/Magento/CatalogUrlRewriteGraphQl/etc/di.xml
+++ b/app/code/Magento/CatalogUrlRewriteGraphQl/etc/di.xml
@@ -14,4 +14,12 @@
+
+
+
+
+ - url_key
+
+
+
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/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/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/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/etc/frontend/sections.xml b/app/code/Magento/Checkout/etc/frontend/sections.xml
index 90c2878f501cf..021cd930c74c0 100644
--- a/app/code/Magento/Checkout/etc/frontend/sections.xml
+++ b/app/code/Magento/Checkout/etc/frontend/sections.xml
@@ -9,6 +9,7 @@
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Customer:etc/sections.xsd">
+
@@ -41,7 +42,6 @@
-
diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js
index d152f94397730..4adc1cd88c0ae 100644
--- a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js
+++ b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js
@@ -103,8 +103,8 @@ define([
});
if (
- cartData().website_id !== window.checkout.websiteId &&
- cartData().website_id !== undefined
+ cartData().website_id !== window.checkout.websiteId && cartData().website_id !== undefined ||
+ cartData().storeId !== window.checkout.storeId && cartData().storeId !== undefined
) {
customerData.reload(['cart'], false);
}
diff --git a/app/code/Magento/CheckoutAgreements/etc/db_schema.xml b/app/code/Magento/CheckoutAgreements/etc/db_schema.xml
index 09cd1c5b63965..43da8d7d27dd9 100644
--- a/app/code/Magento/CheckoutAgreements/etc/db_schema.xml
+++ b/app/code/Magento/CheckoutAgreements/etc/db_schema.xml
@@ -9,7 +9,7 @@
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
+ comment="Agreement ID"/>
@@ -26,9 +26,9 @@
+ comment="Agreement ID"/>
+ comment="Store ID"/>
diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml
index 5baf75d43c53f..03edc69e6d625 100644
--- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml
@@ -16,9 +16,6 @@
-
-
-
diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml
index e63a6be51bcc0..205850f888797 100644
--- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml
@@ -16,9 +16,6 @@
-
-
-
diff --git a/app/code/Magento/Config/Model/Config/Backend/Encrypted.php b/app/code/Magento/Config/Model/Config/Backend/Encrypted.php
index 62d6531978d8a..80ce061a0a17e 100644
--- a/app/code/Magento/Config/Model/Config/Backend/Encrypted.php
+++ b/app/code/Magento/Config/Model/Config/Backend/Encrypted.php
@@ -48,6 +48,9 @@ public function __construct(
* Magic method called during class serialization
*
* @return string[]
+ *
+ * @SuppressWarnings(PHPMD.SerializationAware)
+ * @deprecated Do not use PHP serialization.
*/
public function __sleep()
{
@@ -59,6 +62,9 @@ public function __sleep()
* Magic method called during class un-serialization
*
* @return void
+ *
+ * @SuppressWarnings(PHPMD.SerializationAware)
+ * @deprecated Do not use PHP serialization.
*/
public function __wakeup()
{
diff --git a/app/code/Magento/Config/Model/Config/Backend/File/Pdf.php b/app/code/Magento/Config/Model/Config/Backend/File/Pdf.php
new file mode 100644
index 0000000000000..8716fe5a23ad3
--- /dev/null
+++ b/app/code/Magento/Config/Model/Config/Backend/File/Pdf.php
@@ -0,0 +1,21 @@
+
- */
namespace Magento\Config\Model\Config\Backend\Image;
/**
+ * System config PDF field backend model.
+ *
* @api
* @since 100.0.2
+ * @see \Magento\Config\Model\Config\Backend\File\Pdf
*/
class Pdf extends \Magento\Config\Model\Config\Backend\Image
{
/**
+ * Returns the list of allowed file extensions.
+ *
* @return string[]
*/
protected function _getAllowedExtensions()
{
- return ['tif', 'tiff', 'png', 'jpg', 'jpe', 'jpeg'];
+ return ['tif', 'tiff', 'png', 'jpg', 'jpe', 'jpeg', 'pdf'];
}
}
diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminExpandConfigTabActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminExpandConfigTabActionGroup.xml
new file mode 100644
index 0000000000000..79505e0627865
--- /dev/null
+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminExpandConfigTabActionGroup.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ Goes to the 'Configuration' page and expands main level configuration tab passed via argument as Tab Name.
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenConfigNavItemActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenConfigNavItemActionGroup.xml
new file mode 100644
index 0000000000000..eaca27f86f49a
--- /dev/null
+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenConfigNavItemActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Clicks on config nav item selected by passed argument.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigDeveloperPageActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigDeveloperPageActionGroup.xml
new file mode 100644
index 0000000000000..4c5d21a890973
--- /dev/null
+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigDeveloperPageActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ Go to admin store configuration developer page.
+
+
+
+
+
+
diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigPageActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigPageActionGroup.xml
new file mode 100644
index 0000000000000..43343fd0851e4
--- /dev/null
+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigPageActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ Go to admin store configuration page.
+
+
+
+
+
+
diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminSaveConfigActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminSaveConfigActionGroup.xml
index 6ed0cfe95cb94..bd23292d3ee6a 100644
--- a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminSaveConfigActionGroup.xml
+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminSaveConfigActionGroup.xml
@@ -10,7 +10,7 @@
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd">
-
-
+
+
diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/GeneralConfigurationActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/GeneralConfigurationActionGroup.xml
index 45d84a338a30b..d65376828e2c4 100644
--- a/app/code/Magento/Config/Test/Mftf/ActionGroup/GeneralConfigurationActionGroup.xml
+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/GeneralConfigurationActionGroup.xml
@@ -27,7 +27,6 @@
-
Selects the provided Countries under 'Top destinations' on the 'General' section of the 'Configuration' page. Clicks on the Save button.
diff --git a/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml b/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml
index 7a62dfff8323b..8fafdc202bf09 100644
--- a/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml
+++ b/app/code/Magento/Config/Test/Mftf/Page/AdminConfigPage.xml
@@ -21,4 +21,7 @@
+
+
+
diff --git a/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml
index fd49c1482c133..ffe3f0076ca8d 100644
--- a/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml
+++ b/app/code/Magento/Config/Test/Mftf/Section/AdminConfigSection.xml
@@ -7,6 +7,11 @@
-->
+
+
+
+
+
diff --git a/app/code/Magento/Config/etc/db_schema.xml b/app/code/Magento/Config/etc/db_schema.xml
index 8aeac802fbd91..46dd77959b9d4 100644
--- a/app/code/Magento/Config/etc/db_schema.xml
+++ b/app/code/Magento/Config/etc/db_schema.xml
@@ -9,10 +9,10 @@
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
+ comment="Config ID"/>
+ default="0" comment="Config Scope ID"/>
diff --git a/app/code/Magento/Config/i18n/en_US.csv b/app/code/Magento/Config/i18n/en_US.csv
index 9770bf4b94c27..ceb1efdc8b77d 100644
--- a/app/code/Magento/Config/i18n/en_US.csv
+++ b/app/code/Magento/Config/i18n/en_US.csv
@@ -118,3 +118,4 @@ Dashboard,Dashboard
"Web Section","Web Section"
"Store Email Addresses Section","Store Email Addresses Section"
"Email to a Friend","Email to a Friend"
+"Taiwan","Taiwan, Province of China"
diff --git a/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/UsedProductsCache.php b/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/UsedProductsCache.php
new file mode 100644
index 0000000000000..19a1b8d3ca17f
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/UsedProductsCache.php
@@ -0,0 +1,190 @@
+metadataPool = $metadataPool;
+ $this->cache = $cache;
+ $this->serializer = $serializer;
+ $this->productFactory = $productFactory;
+ $this->customerSession = $customerSession;
+ }
+
+ /**
+ * Retrieve used products for configurable product
+ *
+ * @param Configurable $subject
+ * @param callable $proceed
+ * @param Product $product
+ * @param array|null $requiredAttributeIds
+ * @return ProductInterface[]
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function aroundGetUsedProducts(
+ Configurable $subject,
+ callable $proceed,
+ $product,
+ $requiredAttributeIds = null
+ ) {
+ $cacheKey = $this->getCacheKey($product, $requiredAttributeIds);
+ $usedProducts = $this->readUsedProductsCacheData($cacheKey);
+ if ($usedProducts === null) {
+ $usedProducts = $proceed($product, $requiredAttributeIds);
+ $this->saveUsedProductsCacheData($product, $usedProducts, $cacheKey);
+ }
+
+ return $usedProducts;
+ }
+
+ /**
+ * Generate cache key for product
+ *
+ * @param Product $product
+ * @param array|null $requiredAttributeIds
+ * @return string
+ */
+ private function getCacheKey($product, $requiredAttributeIds = null): string
+ {
+ $metadata = $this->metadataPool->getMetadata(ProductInterface::class);
+ $keyParts = [
+ 'getUsedProducts',
+ $product->getData($metadata->getLinkField()),
+ $product->getStoreId(),
+ $this->customerSession->getCustomerGroupId(),
+ ];
+ if ($requiredAttributeIds !== null) {
+ sort($requiredAttributeIds);
+ $keyParts[] = implode('', $requiredAttributeIds);
+ }
+ $cacheKey = sha1(implode('_', $keyParts));
+
+ return $cacheKey;
+ }
+
+ /**
+ * Read used products data from cache
+ *
+ * Looking for cache record stored under provided $cacheKey
+ * In case data exists turns it into array of products
+ *
+ * @param string $cacheKey
+ * @return ProductInterface[]|null
+ */
+ private function readUsedProductsCacheData(string $cacheKey): ?array
+ {
+ $data = $this->cache->load($cacheKey);
+ if (!$data) {
+ return null;
+ }
+
+ $items = $this->serializer->unserialize($data);
+ if (!$items) {
+ return null;
+ }
+
+ $usedProducts = [];
+ foreach ($items as $item) {
+ /** @var Product $productItem */
+ $productItem = $this->productFactory->create();
+ $productItem->setData($item);
+ $usedProducts[] = $productItem;
+ }
+
+ return $usedProducts;
+ }
+
+ /**
+ * Save $subProducts to cache record identified with provided $cacheKey
+ *
+ * Cached data will be tagged with combined list of product tags and data specific tags i.e. 'price' etc.
+ *
+ * @param Product $product
+ * @param ProductInterface[] $subProducts
+ * @param string $cacheKey
+ * @return bool
+ */
+ private function saveUsedProductsCacheData(Product $product, array $subProducts, string $cacheKey): bool
+ {
+ $metadata = $this->metadataPool->getMetadata(ProductInterface::class);
+ $data = $this->serializer->serialize(
+ array_map(
+ function ($item) {
+ return $item->getData();
+ },
+ $subProducts
+ )
+ );
+ $tags = array_merge(
+ $product->getIdentities(),
+ [
+ Category::CACHE_TAG,
+ Product::CACHE_TAG,
+ 'price',
+ Configurable::TYPE_CODE . '_' . $product->getData($metadata->getLinkField()),
+ ]
+ );
+ $result = $this->cache->save($data, $cacheKey, $tags);
+
+ return (bool) $result;
+ }
+}
diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php
index a849d964eaed5..c60953e33e9eb 100644
--- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php
+++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php
@@ -1233,28 +1233,22 @@ public function isPossibleBuyFromList($product)
* Returns array of sub-products for specified configurable product
*
* $requiredAttributeIds - one dimensional array, if provided
- *
* Result array contains all children for specified configurable product
*
- * @param \Magento\Catalog\Model\Product $product
- * @param array $requiredAttributeIds
+ * @param \Magento\Catalog\Model\Product $product
+ * @param array $requiredAttributeIds
* @return ProductInterface[]
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function getUsedProducts($product, $requiredAttributeIds = null)
{
- $metadata = $this->getMetadataPool()->getMetadata(ProductInterface::class);
- $keyParts = [
- __METHOD__,
- $product->getData($metadata->getLinkField()),
- $product->getStoreId(),
- $this->getCustomerSession()->getCustomerGroupId()
- ];
- if ($requiredAttributeIds !== null) {
- sort($requiredAttributeIds);
- $keyParts[] = implode('', $requiredAttributeIds);
+ if (!$product->hasData($this->_usedProducts)) {
+ $collection = $this->getConfiguredUsedProductCollection($product, false);
+ $usedProducts = array_values($collection->getItems());
+ $product->setData($this->_usedProducts, $usedProducts);
}
- $cacheKey = $this->getUsedProductsCacheKey($keyParts);
- return $this->loadUsedProducts($product, $cacheKey);
+
+ return $product->getData($this->_usedProducts);
}
/**
diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php
index b013916cc221a..01549ffcd2755 100644
--- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php
+++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Attribute.php
@@ -113,11 +113,12 @@ public function afterSave()
}
/**
- * Load configurable attribute by product and product's attribute
+ * Load configurable attribute by product and product's attribute.
*
* @param \Magento\Catalog\Model\Product $product
* @param \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute
* @throws LocalizedException
+ * @return void
*/
public function loadByProductAndAttribute($product, $attribute)
{
@@ -263,6 +264,9 @@ public function setProductId($value)
/**
* @inheritdoc
+ *
+ * @SuppressWarnings(PHPMD.SerializationAware)
+ * @deprecated Do not use PHP serialization.
*/
public function __sleep()
{
@@ -274,6 +278,9 @@ public function __sleep()
/**
* @inheritdoc
+ *
+ * @SuppressWarnings(PHPMD.SerializationAware)
+ * @deprecated Do not use PHP serialization.
*/
public function __wakeup()
{
diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php
index 8f2cc6ddb43ce..3aa90c7b3ce57 100644
--- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php
+++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Attribute/Collection.php
@@ -302,7 +302,9 @@ protected function _loadLabels()
}
/**
- * Load attribute options.
+ * Load related options' data.
+ *
+ * @return void
*/
protected function loadOptions()
{
@@ -355,6 +357,9 @@ protected function getIncludedOptions(array $usedProducts, AbstractAttribute $pr
/**
* @inheritdoc
* @since 100.0.6
+ *
+ * @SuppressWarnings(PHPMD.SerializationAware)
+ * @deprecated Do not use PHP serialization.
*/
public function __sleep()
{
@@ -374,6 +379,9 @@ public function __sleep()
/**
* @inheritdoc
* @since 100.0.6
+ *
+ * @SuppressWarnings(PHPMD.SerializationAware)
+ * @deprecated Do not use PHP serialization.
*/
public function __wakeup()
{
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml
index de6714a9b959e..3f21c98068d8a 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductData.xml
@@ -78,4 +78,10 @@
EavStockItem
CustomAttributeCategoryIds
+
+
+ api-configurable-export-import-product
+ API Configurable Export Import Product
+ api-configurable-export-import-product
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml
index 1defecbc7c285..336f95aa55576 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection.xml
@@ -39,6 +39,9 @@
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml
new file mode 100644
index 0000000000000..fa21d20eb4456
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php
index c351d12fa813d..165e479d99348 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php
+++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/ConfigurableTest.php
@@ -266,10 +266,12 @@ public function testSave()
->with('_cache_instance_used_product_attribute_ids')
->willReturn(true);
$extensionAttributes = $this->getMockBuilder(ProductExtensionInterface::class)
- ->setMethods([
- 'getConfigurableProductOptions',
- 'getConfigurableProductLinks'
- ])
+ ->setMethods(
+ [
+ 'getConfigurableProductOptions',
+ 'getConfigurableProductLinks'
+ ]
+ )
->getMockForAbstractClass();
$this->entityMetadata->expects($this->any())
->method('getLinkField')
@@ -344,25 +346,13 @@ public function testCanUseAttribute()
public function testGetUsedProducts()
{
- $productCollectionItemData = ['array'];
+ $productCollectionItem = $this->createMock(\Magento\Catalog\Model\Product::class);
+ $attributeCollection = $this->createMock(Collection::class);
+ $product = $this->createMock(\Magento\Catalog\Model\Product::class);
+ $productCollection = $this->createMock(ProductCollection::class);
- $productCollectionItem = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
- ->disableOriginalConstructor()
- ->getMock();
- $attributeCollection = $this->getMockBuilder(Collection::class)
- ->disableOriginalConstructor()
- ->getMock();
- $product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
- ->disableOriginalConstructor()
- ->getMock();
- $productCollection = $this->getMockBuilder(ProductCollection::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $productCollectionItem->expects($this->once())->method('getData')->willReturn($productCollectionItemData);
$attributeCollection->expects($this->any())->method('setProductFilter')->willReturnSelf();
$product->expects($this->atLeastOnce())->method('getStoreId')->willReturn(5);
- $product->expects($this->once())->method('getIdentities')->willReturn(['123']);
$product->expects($this->exactly(2))
->method('hasData')
@@ -388,59 +378,10 @@ public function testGetUsedProducts()
$productCollection->expects($this->once())->method('setStoreId')->with(5)->willReturn([]);
$productCollection->expects($this->once())->method('getItems')->willReturn([$productCollectionItem]);
- $this->serializer->expects($this->once())
- ->method('serialize')
- ->with([$productCollectionItemData])
- ->willReturn('result');
-
$this->productCollectionFactory->expects($this->any())->method('create')->willReturn($productCollection);
$this->model->getUsedProducts($product);
}
- public function testGetUsedProductsWithDataInCache()
- {
- $product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
- ->disableOriginalConstructor()
- ->getMock();
- $childProduct = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $dataKey = '_cache_instance_products';
- $usedProductsData = [['first']];
- $usedProducts = [$childProduct];
-
- $product->expects($this->once())
- ->method('hasData')
- ->with($dataKey)
- ->willReturn(false);
- $product->expects($this->once())
- ->method('setData')
- ->with($dataKey, $usedProducts);
- $product->expects($this->any())
- ->method('getData')
- ->willReturnOnConsecutiveCalls(1, $usedProducts);
-
- $childProduct->expects($this->once())
- ->method('setData')
- ->with($usedProductsData[0]);
-
- $this->productFactory->expects($this->once())
- ->method('create')
- ->willReturn($childProduct);
-
- $this->cache->expects($this->once())
- ->method('load')
- ->willReturn($usedProductsData);
-
- $this->serializer->expects($this->once())
- ->method('unserialize')
- ->with($usedProductsData)
- ->willReturn($usedProductsData);
-
- $this->assertEquals($usedProducts, $this->model->getUsedProducts($product));
- }
-
/**
* @param int $productStore
*
@@ -878,12 +819,12 @@ public function testSetImageFromChildProduct()
->method('getLinkField')
->willReturn('link');
$productMock->expects($this->any())->method('hasData')
- ->withConsecutive(['store_id'], ['_cache_instance_products'])
- ->willReturnOnConsecutiveCalls(true, true);
+ ->withConsecutive(['_cache_instance_products'])
+ ->willReturnOnConsecutiveCalls(true);
$productMock->expects($this->any())->method('getData')
- ->withConsecutive(['image'], ['image'], ['link'], ['store_id'], ['_cache_instance_products'])
- ->willReturnOnConsecutiveCalls('no_selection', 'no_selection', 1, 1, [$childProductMock]);
+ ->withConsecutive(['image'], ['image'], ['_cache_instance_products'])
+ ->willReturnOnConsecutiveCalls('no_selection', 'no_selection', [$childProductMock]);
$childProductMock->expects($this->any())->method('getData')->with('image')->willReturn('image_data');
$productMock->expects($this->once())->method('setImage')->with('image_data')->willReturnSelf();
diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml
index b8f7ed67a9868..c8a278df92dc6 100644
--- a/app/code/Magento/ConfigurableProduct/etc/di.xml
+++ b/app/code/Magento/ConfigurableProduct/etc/di.xml
@@ -256,4 +256,12 @@
+
+
+ Magento\Framework\App\Cache\Type\Collection
+
+
+ Magento\Framework\Serialize\Serializer\Json
+
+
diff --git a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml
index df96829b354c8..b2d50f54f5334 100644
--- a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml
+++ b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml
@@ -13,4 +13,7 @@
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml
index c11a1adc19896..240c5e65c79c3 100644
--- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml
+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/config.phtml
@@ -64,7 +64,7 @@
"productsProvider": "configurable_associated_product_listing.data_source",
"productsMassAction": "configurable_associated_product_listing.configurable_associated_product_listing.product_columns.ids",
"productsColumns": "configurable_associated_product_listing.configurable_associated_product_listing.product_columns",
- "productsGridUrl": "= /* @noEscape */ $block->getUrl('catalog/product/associated_grid', ['componentJson' => true]) ?>",
+ "productsGridUrl": "= /* @noEscape */ $block->getUrl('catalog/product_associated/grid', ['componentJson' => true]) ?>",
"configurableVariations": "configurableVariations"
}
}
diff --git a/app/code/Magento/Cron/etc/adminhtml/system.xml b/app/code/Magento/Cron/etc/adminhtml/system.xml
index 95d8d4c8a6966..c8753f1b0b56f 100644
--- a/app/code/Magento/Cron/etc/adminhtml/system.xml
+++ b/app/code/Magento/Cron/etc/adminhtml/system.xml
@@ -15,21 +15,27 @@
Cron configuration options for group:
Generate Schedules Every
+ validate-zero-or-greater validate-digits
Schedule Ahead for
+ validate-zero-or-greater validate-digits
Missed if Not Run Within
+ validate-zero-or-greater validate-digits
History Cleanup Every
+ validate-zero-or-greater validate-digits
Success History Lifetime
+ validate-zero-or-greater validate-digits
Failure History Lifetime
+ validate-zero-or-greater validate-digits
Use Separate Process
diff --git a/app/code/Magento/Cron/etc/db_schema.xml b/app/code/Magento/Cron/etc/db_schema.xml
index b3061eefa6313..206b8f64f3ae7 100644
--- a/app/code/Magento/Cron/etc/db_schema.xml
+++ b/app/code/Magento/Cron/etc/db_schema.xml
@@ -9,7 +9,7 @@
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
+ comment="Schedule ID"/>
diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart.php
index db560f7de3ecb..3709f4914c477 100644
--- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart.php
+++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart.php
@@ -75,7 +75,7 @@ public function __construct(
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
protected function _construct()
{
@@ -119,7 +119,7 @@ protected function _prepareCollection()
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
protected function _prepareColumns()
{
@@ -201,7 +201,7 @@ public function getCustomerId()
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function getGridUrl()
{
@@ -224,7 +224,13 @@ public function getGridParentHtml()
*/
public function getRowUrl($row)
{
- return $this->getUrl('catalog/product/edit', ['id' => $row->getProductId()]);
+ return $this->getUrl(
+ 'catalog/product/edit',
+ [
+ 'id' => $row->getProductId(),
+ 'customerId' => $this->getCustomerId()
+ ]
+ );
}
/**
diff --git a/app/code/Magento/Customer/Block/SectionNamesProvider.php b/app/code/Magento/Customer/Block/SectionNamesProvider.php
new file mode 100644
index 0000000000000..92029d1715d4b
--- /dev/null
+++ b/app/code/Magento/Customer/Block/SectionNamesProvider.php
@@ -0,0 +1,39 @@
+sectionPool = $sectionPool;
+ }
+
+ /**
+ * Return array of section names based on config.
+ *
+ * @return array
+ */
+ public function getSectionNames()
+ {
+ return $this->sectionPool->getSectionNames();
+ }
+}
diff --git a/app/code/Magento/Customer/Controller/Account/Confirmation.php b/app/code/Magento/Customer/Controller/Account/Confirmation.php
index a3e2db0207630..59def8640328c 100644
--- a/app/code/Magento/Customer/Controller/Account/Confirmation.php
+++ b/app/code/Magento/Customer/Controller/Account/Confirmation.php
@@ -1,21 +1,26 @@
storeManager->getStore()->getWebsiteId()
);
- $this->messageManager->addSuccess(__('Please check your email for confirmation key.'));
+ $this->messageManager->addSuccessMessage(__('Please check your email for confirmation key.'));
} catch (InvalidTransitionException $e) {
- $this->messageManager->addSuccess(__('This email does not require confirmation.'));
+ $this->messageManager->addSuccessMessage(__('This email does not require confirmation.'));
} catch (\Exception $e) {
- $this->messageManager->addException($e, __('Wrong email.'));
+ $this->messageManager->addExceptionMessage($e, __('Wrong email.'));
$resultRedirect->setPath('*/*/*', ['email' => $email, '_secure' => true]);
return $resultRedirect;
}
diff --git a/app/code/Magento/Customer/CustomerData/SectionPool.php b/app/code/Magento/Customer/CustomerData/SectionPool.php
index efea1762d9de6..eef2854cf363e 100644
--- a/app/code/Magento/Customer/CustomerData/SectionPool.php
+++ b/app/code/Magento/Customer/CustomerData/SectionPool.php
@@ -62,6 +62,16 @@ public function getSectionsData(array $sectionNames = null, $forceNewTimestamp =
return $sectionsData;
}
+ /**
+ * Return array of section names.
+ *
+ * @return array
+ */
+ public function getSectionNames()
+ {
+ return array_keys($this->sectionSourceMap);
+ }
+
/**
* Get section sources by section names
*
diff --git a/app/code/Magento/Customer/Model/Attribute.php b/app/code/Magento/Customer/Model/Attribute.php
index 98a97872f15f4..d05bf14fbc97d 100644
--- a/app/code/Magento/Customer/Model/Attribute.php
+++ b/app/code/Magento/Customer/Model/Attribute.php
@@ -202,6 +202,9 @@ public function canBeFilterableInGrid()
/**
* @inheritdoc
+ *
+ * @SuppressWarnings(PHPMD.SerializationAware)
+ * @deprecated Do not use PHP serialization.
*/
public function __sleep()
{
@@ -214,6 +217,9 @@ public function __sleep()
/**
* @inheritdoc
+ *
+ * @SuppressWarnings(PHPMD.SerializationAware)
+ * @deprecated Do not use PHP serialization.
*/
public function __wakeup()
{
diff --git a/app/code/Magento/Customer/Model/Customer.php b/app/code/Magento/Customer/Model/Customer.php
index 1287dbe5df708..1f8f7d90f6d0d 100644
--- a/app/code/Magento/Customer/Model/Customer.php
+++ b/app/code/Magento/Customer/Model/Customer.php
@@ -394,7 +394,9 @@ public function getSharingConfig()
public function authenticate($login, $password)
{
$this->loadByEmail($login);
- if ($this->getConfirmation() && $this->isConfirmationRequired()) {
+ if ($this->getConfirmation() &&
+ $this->accountConfirmation->isConfirmationRequired($this->getWebsiteId(), $this->getId(), $this->getEmail())
+ ) {
throw new EmailNotConfirmedException(
__("This account isn't confirmed. Verify and try again.")
);
@@ -415,8 +417,9 @@ public function authenticate($login, $password)
/**
* Load customer by email
*
- * @param string $customerEmail
- * @return $this
+ * @param string $customerEmail
+ * @return $this
+ * @throws \Magento\Framework\Exception\LocalizedException
*/
public function loadByEmail($customerEmail)
{
@@ -427,8 +430,9 @@ public function loadByEmail($customerEmail)
/**
* Change customer password
*
- * @param string $newPassword
- * @return $this
+ * @param string $newPassword
+ * @return $this
+ * @throws \Magento\Framework\Exception\LocalizedException
*/
public function changePassword($newPassword)
{
@@ -440,6 +444,7 @@ public function changePassword($newPassword)
* Get full customer name
*
* @return string
+ * @throws \Magento\Framework\Exception\LocalizedException
*/
public function getName()
{
@@ -462,8 +467,9 @@ public function getName()
/**
* Add address to address collection
*
- * @param Address $address
- * @return $this
+ * @param Address $address
+ * @return $this
+ * @throws \Magento\Framework\Exception\LocalizedException
*/
public function addAddress(Address $address)
{
@@ -487,6 +493,7 @@ public function getAddressById($addressId)
*
* @param int $addressId
* @return Address
+ * @throws \Magento\Framework\Exception\LocalizedException
*/
public function getAddressItemById($addressId)
{
@@ -507,6 +514,7 @@ public function getAddressCollection()
* Customer addresses collection
*
* @return \Magento\Customer\Model\ResourceModel\Address\Collection
+ * @throws \Magento\Framework\Exception\LocalizedException
*/
public function getAddressesCollection()
{
@@ -538,6 +546,7 @@ public function getAddresses()
* Retrieve all customer attributes
*
* @return Attribute[]
+ * @throws \Magento\Framework\Exception\LocalizedException
*/
public function getAttributes()
{
@@ -592,6 +601,7 @@ public function hashPassword($password, $salt = true)
*
* @param string $password
* @return boolean
+ * @throws \Exception
*/
public function validatePassword($password)
{
@@ -805,6 +815,7 @@ public function isConfirmationRequired()
*/
public function getRandomConfirmationKey()
{
+ // phpcs:ignore Magento2.Security.InsecureFunction
return md5(uniqid());
}
diff --git a/app/code/Magento/Customer/Model/ResourceModel/Customer.php b/app/code/Magento/Customer/Model/ResourceModel/Customer.php
index 94196df6fe093..1477287f79f4b 100644
--- a/app/code/Magento/Customer/Model/ResourceModel/Customer.php
+++ b/app/code/Magento/Customer/Model/ResourceModel/Customer.php
@@ -6,6 +6,7 @@
namespace Magento\Customer\Model\ResourceModel;
+use Magento\Customer\Model\AccountConfirmation;
use Magento\Customer\Model\Customer\NotificationStorage;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\Validator\Exception as ValidatorException;
@@ -42,12 +43,19 @@ class Customer extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity
*/
protected $storeManager;
+ /**
+ * @var AccountConfirmation
+ */
+ private $accountConfirmation;
+
/**
* @var NotificationStorage
*/
private $notificationStorage;
/**
+ * Customer constructor.
+ *
* @param \Magento\Eav\Model\Entity\Context $context
* @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot
* @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite $entityRelationComposite
@@ -56,6 +64,7 @@ class Customer extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity
* @param \Magento\Framework\Stdlib\DateTime $dateTime
* @param \Magento\Store\Model\StoreManagerInterface $storeManager
* @param array $data
+ * @param AccountConfirmation $accountConfirmation
*/
public function __construct(
\Magento\Eav\Model\Entity\Context $context,
@@ -65,15 +74,19 @@ public function __construct(
\Magento\Framework\Validator\Factory $validatorFactory,
\Magento\Framework\Stdlib\DateTime $dateTime,
\Magento\Store\Model\StoreManagerInterface $storeManager,
- $data = []
+ $data = [],
+ AccountConfirmation $accountConfirmation = null
) {
parent::__construct($context, $entitySnapshot, $entityRelationComposite, $data);
+
$this->_scopeConfig = $scopeConfig;
$this->_validatorFactory = $validatorFactory;
$this->dateTime = $dateTime;
- $this->storeManager = $storeManager;
+ $this->accountConfirmation = $accountConfirmation ?: ObjectManager::getInstance()
+ ->get(AccountConfirmation::class);
$this->setType('customer');
$this->setConnection('customer_read', 'customer_write');
+ $this->storeManager = $storeManager;
}
/**
@@ -144,7 +157,13 @@ protected function _beforeSave(\Magento\Framework\DataObject $customer)
}
// set confirmation key logic
- if (!$customer->getId() && $customer->isConfirmationRequired()) {
+ if (!$customer->getId() &&
+ $this->accountConfirmation->isConfirmationRequired(
+ $customer->getWebsiteId(),
+ $customer->getId(),
+ $customer->getEmail()
+ )
+ ) {
$customer->setConfirmation($customer->getRandomConfirmationKey());
}
// remove customer confirmation key from database, if empty
diff --git a/app/code/Magento/Customer/Model/Session.php b/app/code/Magento/Customer/Model/Session.php
index 5900fed218edf..047327a0b6c29 100644
--- a/app/code/Magento/Customer/Model/Session.php
+++ b/app/code/Magento/Customer/Model/Session.php
@@ -10,6 +10,7 @@
use Magento\Customer\Api\GroupManagementInterface;
use Magento\Customer\Model\Config\Share;
use Magento\Customer\Model\ResourceModel\Customer as ResourceCustomer;
+use Magento\Framework\App\ObjectManager;
/**
* Customer session model
@@ -17,6 +18,7 @@
* @api
* @method string getNoReferer()
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
* @since 100.0.2
*/
class Session extends \Magento\Framework\Session\SessionManager
@@ -107,6 +109,8 @@ class Session extends \Magento\Framework\Session\SessionManager
protected $response;
/**
+ * Session constructor.
+ *
* @param \Magento\Framework\App\Request\Http $request
* @param \Magento\Framework\Session\SidResolverInterface $sidResolver
* @param \Magento\Framework\Session\Config\ConfigInterface $sessionConfig
@@ -118,7 +122,7 @@ class Session extends \Magento\Framework\Session\SessionManager
* @param \Magento\Framework\App\State $appState
* @param Share $configShare
* @param \Magento\Framework\Url\Helper\Data $coreUrl
- * @param \Magento\Customer\Model\Url $customerUrl
+ * @param Url $customerUrl
* @param ResourceCustomer $customerResource
* @param CustomerFactory $customerFactory
* @param \Magento\Framework\UrlFactory $urlFactory
@@ -128,6 +132,7 @@ class Session extends \Magento\Framework\Session\SessionManager
* @param CustomerRepositoryInterface $customerRepository
* @param GroupManagementInterface $groupManagement
* @param \Magento\Framework\App\Response\Http $response
+ * @param AccountConfirmation $accountConfirmation
* @throws \Magento\Framework\Exception\SessionException
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
@@ -152,7 +157,8 @@ public function __construct(
\Magento\Framework\App\Http\Context $httpContext,
CustomerRepositoryInterface $customerRepository,
GroupManagementInterface $groupManagement,
- \Magento\Framework\App\Response\Http $response
+ \Magento\Framework\App\Response\Http $response,
+ AccountConfirmation $accountConfirmation = null
) {
$this->_coreUrl = $coreUrl;
$this->_customerUrl = $customerUrl;
@@ -177,6 +183,8 @@ public function __construct(
);
$this->groupManagement = $groupManagement;
$this->response = $response;
+ $this->accountConfirmation = $accountConfirmation ?: ObjectManager::getInstance()
+ ->get(AccountConfirmation::class);
$this->_eventManager->dispatch('customer_session_init', ['customer_session' => $this]);
}
@@ -216,6 +224,8 @@ public function setCustomerData(CustomerData $customer)
* Retrieve customer model object
*
* @return CustomerData
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
*/
public function getCustomerData()
{
@@ -266,8 +276,14 @@ public function setCustomer(Customer $customerModel)
\Magento\Customer\Model\Group::NOT_LOGGED_IN_ID
);
$this->setCustomerId($customerModel->getId());
- if (!$customerModel->isConfirmationRequired() && $customerModel->getConfirmation()) {
- $customerModel->setConfirmation(null)->save();
+ $accountConfirmationRequired = $this->accountConfirmation->isConfirmationRequired(
+ $customerModel->getWebsiteId(),
+ $customerModel->getId(),
+ $customerModel->getEmail()
+ );
+ if (!$accountConfirmationRequired && $customerModel->getConfirmation() && $customerModel->getId()) {
+ $customerModel->setConfirmation(null);
+ $this->_customerResource->save($customerModel);
}
/**
@@ -354,10 +370,11 @@ public function setCustomerGroupId($id)
}
/**
- * Get customer group id
- * If customer is not logged in system, 'not logged in' group id will be returned
+ * Get customer group id. If customer is not logged in system, 'not logged in' group id will be returned.
*
* @return int
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
*/
public function getCustomerGroupId()
{
@@ -407,6 +424,8 @@ public function checkCustomerId($customerId)
}
/**
+ * Sets customer as logged in
+ *
* @param Customer $customer
* @return $this
*/
@@ -420,6 +439,8 @@ public function setCustomerAsLoggedIn($customer)
}
/**
+ * Sets customer data as logged in
+ *
* @param CustomerData $customer
* @return $this
*/
@@ -521,6 +542,8 @@ protected function _setAuthUrl($key, $url)
* Logout without dispatching event
*
* @return $this
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
*/
protected function _logout()
{
@@ -567,6 +590,8 @@ public function regenerateId()
}
/**
+ * Creates URL factory
+ *
* @return \Magento\Framework\UrlInterface
*/
protected function _createUrl()
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountCreatePageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountCreatePageActionGroup.xml
index b013b1db1c8e7..31a988ac9da0d 100644
--- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountCreatePageActionGroup.xml
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontOpenCustomerAccountCreatePageActionGroup.xml
@@ -16,4 +16,15 @@
+
+
+
+ Goes to the Storefront Customer Create page using Store code in URL option.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml
index 9bd382da8eb92..79fb18afaad53 100644
--- a/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminEditCustomerPage.xml
@@ -12,6 +12,8 @@
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerCartSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerCartSection.xml
new file mode 100644
index 0000000000000..5c8b8907db43a
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerCartSection.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerInformationSection.xml
new file mode 100644
index 0000000000000..d680015230b9d
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerInformationSection.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminProductBackRedirectNavigateFromCustomerViewCartProduct.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminProductBackRedirectNavigateFromCustomerViewCartProduct.xml
new file mode 100644
index 0000000000000..9de2339f2e217
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminProductBackRedirectNavigateFromCustomerViewCartProduct.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php
index b245702ce07f9..069ddc63d74d7 100644
--- a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php
+++ b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/GroupTest.php
@@ -127,7 +127,7 @@ public function testSaveWithReservedId()
]
)
->getMockForAbstractClass();
- $dbAdapter->expects($this->any())->method('describeTable')->willReturn([]);
+ $dbAdapter->expects($this->any())->method('describeTable')->willReturn(['customer_group_id' => []]);
$dbAdapter->expects($this->any())->method('update')->willReturnSelf();
$dbAdapter->expects($this->once())->method('lastInsertId')->willReturn($expectedId);
$selectMock = $this->getMockBuilder(\Magento\Framework\DB\Select::class)
diff --git a/app/code/Magento/Customer/Test/Unit/Model/SessionTest.php b/app/code/Magento/Customer/Test/Unit/Model/SessionTest.php
index 7efc61af800d3..8565790990df1 100644
--- a/app/code/Magento/Customer/Test/Unit/Model/SessionTest.php
+++ b/app/code/Magento/Customer/Test/Unit/Model/SessionTest.php
@@ -66,7 +66,7 @@ protected function setUp()
$this->urlFactoryMock = $this->createMock(\Magento\Framework\UrlFactory::class);
$this->customerFactoryMock = $this->getMockBuilder(\Magento\Customer\Model\CustomerFactory::class)
->disableOriginalConstructor()
- ->setMethods(['create'])
+ ->setMethods(['create', 'save'])
->getMock();
$this->customerRepositoryMock = $this->createMock(\Magento\Customer\Api\CustomerRepositoryInterface::class);
$helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
@@ -192,15 +192,12 @@ protected function prepareLoginDataMock($customerId)
$customerMock = $this->createPartialMock(
\Magento\Customer\Model\Customer::class,
- ['getId', 'isConfirmationRequired', 'getConfirmation', 'updateData', 'getGroupId']
+ ['getId', 'getConfirmation', 'updateData', 'getGroupId']
);
- $customerMock->expects($this->once())
+ $customerMock->expects($this->exactly(3))
->method('getId')
->will($this->returnValue($customerId));
$customerMock->expects($this->once())
- ->method('isConfirmationRequired')
- ->will($this->returnValue(true));
- $customerMock->expects($this->never())
->method('getConfirmation')
->will($this->returnValue($customerId));
diff --git a/app/code/Magento/Customer/etc/db_schema.xml b/app/code/Magento/Customer/etc/db_schema.xml
index c699db06d30dc..e07d7d8708a43 100644
--- a/app/code/Magento/Customer/etc/db_schema.xml
+++ b/app/code/Magento/Customer/etc/db_schema.xml
@@ -15,7 +15,7 @@
-
+
-
+
+ comment="Value ID"/>
+ default="0" comment="Attribute ID"/>
@@ -155,9 +155,9 @@
+ comment="Value ID"/>
+ default="0" comment="Attribute ID"/>
+ default="0" comment="Tax Class ID"/>
+ comment="Attribute ID"/>
@@ -461,7 +461,7 @@
+ comment="Attribute ID"/>
@@ -476,9 +476,9 @@
+ comment="Attribute ID"/>
+ comment="Website ID"/>
+ comment="Customer ID"/>
diff --git a/app/code/Magento/Customer/etc/di.xml b/app/code/Magento/Customer/etc/di.xml
index a181d6dd217fd..3b7191e7ed326 100644
--- a/app/code/Magento/Customer/etc/di.xml
+++ b/app/code/Magento/Customer/etc/di.xml
@@ -157,6 +157,11 @@
SectionInvalidationConfigData
+
+
+ SectionInvalidationConfigData
+
+
diff --git a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_form.xml b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_form.xml
index 692cb2ecb964d..3af0172b3fca8 100644
--- a/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_form.xml
+++ b/app/code/Magento/Customer/view/adminhtml/ui_component/customer_address_form.xml
@@ -191,13 +191,6 @@
text
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Customer/view/frontend/layout/default.xml b/app/code/Magento/Customer/view/frontend/layout/default.xml
index 94e46fda194b0..3976fc6bd9090 100644
--- a/app/code/Magento/Customer/view/frontend/layout/default.xml
+++ b/app/code/Magento/Customer/view/frontend/layout/default.xml
@@ -41,9 +41,12 @@
-
+
+ Magento\Customer\Block\SectionNamesProvider
+
+
+
diff --git a/app/code/Magento/Customer/view/frontend/templates/js/section-config.phtml b/app/code/Magento/Customer/view/frontend/templates/js/section-config.phtml
index ebbd16164d7e8..e6511a0674e1d 100644
--- a/app/code/Magento/Customer/view/frontend/templates/js/section-config.phtml
+++ b/app/code/Magento/Customer/view/frontend/templates/js/section-config.phtml
@@ -15,7 +15,9 @@
"baseUrls": = /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode(array_unique([
$block->getUrl(null, ['_secure' => true]),
$block->getUrl(null, ['_secure' => false]),
- ])) ?>
+ ])) ?>,
+ "sectionNames": = /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)
+ ->jsonEncode($block->getData('sectionNamesProvider')->getSectionNames()) ?>
}
}
}
diff --git a/app/code/Magento/Customer/view/frontend/web/js/customer-data.js b/app/code/Magento/Customer/view/frontend/web/js/customer-data.js
index 41cf05df2b1d5..de3ff10bb057b 100644
--- a/app/code/Magento/Customer/view/frontend/web/js/customer-data.js
+++ b/app/code/Magento/Customer/view/frontend/web/js/customer-data.js
@@ -198,30 +198,9 @@ define([
* Customer data initialization
*/
init: function () {
- var privateContentVersion = 'private_content_version',
- privateContent = $.cookieStorage.get(privateContentVersion),
- localPrivateContent = $.localStorage.get(privateContentVersion),
- needVersion = 'need_version',
- expiredSectionNames = this.getExpiredSectionNames();
-
- if (privateContent &&
- !$.cookieStorage.isSet(privateContentVersion) &&
- !$.localStorage.isSet(privateContentVersion)
- ) {
- $.cookieStorage.set(privateContentVersion, needVersion);
- $.localStorage.set(privateContentVersion, needVersion);
- this.reload([], false);
- } else if (localPrivateContent !== privateContent) {
- if (!$.cookieStorage.isSet(privateContentVersion)) {
- privateContent = needVersion;
- $.cookieStorage.set(privateContentVersion, privateContent);
- }
- $.localStorage.set(privateContentVersion, privateContent);
- _.each(dataProvider.getFromStorage(storage.keys()), function (sectionData, sectionName) {
- buffer.notify(sectionName, sectionData);
- });
- this.reload([], false);
- } else if (expiredSectionNames.length > 0) {
+ var expiredSectionNames = this.getExpiredSectionNames();
+
+ if (expiredSectionNames.length > 0) {
_.each(dataProvider.getFromStorage(storage.keys()), function (sectionData, sectionName) {
buffer.notify(sectionName, sectionData);
});
@@ -341,7 +320,9 @@ define([
var sectionDataIds,
sectionsNamesForInvalidation;
- sectionsNamesForInvalidation = _.contains(sectionNames, '*') ? buffer.keys() : sectionNames;
+ sectionsNamesForInvalidation = _.contains(sectionNames, '*') ? sectionConfig.getSectionNames() :
+ sectionNames;
+
$(document).trigger('customer-data-invalidate', [sectionsNamesForInvalidation]);
buffer.remove(sectionsNamesForInvalidation);
sectionDataIds = $.cookieStorage.get('section_data_ids') || {};
diff --git a/app/code/Magento/Customer/view/frontend/web/js/section-config.js b/app/code/Magento/Customer/view/frontend/web/js/section-config.js
index 76fe7f2515a3a..d346d5b070729 100644
--- a/app/code/Magento/Customer/view/frontend/web/js/section-config.js
+++ b/app/code/Magento/Customer/view/frontend/web/js/section-config.js
@@ -6,7 +6,7 @@
define(['underscore'], function (_) {
'use strict';
- var baseUrls, sections, clientSideSections, canonize;
+ var baseUrls, sections, clientSideSections, sectionNames, canonize;
/**
* @param {String} url
@@ -70,6 +70,15 @@ define(['underscore'], function (_) {
return _.contains(clientSideSections, sectionName);
},
+ /**
+ * Returns array of section names.
+ *
+ * @returns {Array}
+ */
+ getSectionNames: function () {
+ return sectionNames;
+ },
+
/**
* @param {Object} options
* @constructor
@@ -78,6 +87,7 @@ define(['underscore'], function (_) {
baseUrls = options.baseUrls;
sections = options.sections;
clientSideSections = options.clientSideSections;
+ sectionNames = options.sectionNames;
}
};
});
diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php
index a4649bccc02e8..8741bff7aa88d 100644
--- a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php
+++ b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php
@@ -125,6 +125,8 @@ public function execute(AddressInterface $address): array
}
$addressData = array_merge($addressData, $customAttributes);
+ $addressData['customer_id'] = null;
+
return $addressData;
}
}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php
index de37482aca056..542165b49dc13 100644
--- a/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php
+++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php
@@ -101,8 +101,11 @@ public function execute(CustomerInterface $customer): array
}
}
$customerData = array_merge($customerData, $customAttributes);
-
+ //Field is deprecated and should not be exposed on storefront.
+ $customerData['group_id'] = null;
$customerData['model'] = $customer;
+ $customerData['id'] = null;
+
return $customerData;
}
}
diff --git a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls
index d27debdc39c64..b5cd2f595a4c9 100644
--- a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls
@@ -36,13 +36,13 @@ input CustomerAddressInput {
prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.")
suffix: String @doc(description: "A value such as Sr., Jr., or III")
vat_id: String @doc(description: "The customer's Tax/VAT number (for corporate customers)")
- custom_attributes: [CustomerAddressAttributeInput] @doc(description: "Address custom attributes")
+ custom_attributes: [CustomerAddressAttributeInput] @doc(description: "Deprecated: Custom attributes should not be put into container.")
}
input CustomerAddressRegionInput @doc(description: "CustomerAddressRegionInput defines the customer's state or province") {
region_code: String @doc(description: "The address region code")
region: String @doc(description: "The state or province name")
- region_id: Int @doc(description: "Uniquely identifies the region")
+ region_id: Int @doc(description: "region_id is deprecated. Region ID is excessive on storefront and region code should suffice for all scenarios")
}
input CustomerAddressAttributeInput {
@@ -78,7 +78,7 @@ type RevokeCustomerTokenOutput {
type Customer @doc(description: "Customer defines the customer name and address and other details") {
created_at: String @doc(description: "Timestamp indicating when the account was created")
- group_id: Int @doc(description: "The group assigned to the user. Default values are 0 (Not logged in), 1 (General), 2 (Wholesale), and 3 (Retailer)")
+ group_id: Int @deprecated(reason: "Customer group should not be exposed in the storefront scenarios")
prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.")
firstname: String @doc(description: "The customer's first name")
middlename: String @doc(description: "The customer's middle name")
@@ -89,7 +89,7 @@ type Customer @doc(description: "Customer defines the customer name and address
default_shipping: String @doc(description: "The ID assigned to the shipping address")
dob: String @doc(description: "The customer's date of birth")
taxvat: String @doc(description: "The customer's Tax/VAT number (for corporate customers)")
- id: Int @doc(description: "The ID assigned to the customer")
+ id: Int @doc(description: "The ID assigned to the customer") @deprecated(reason: "id is not needed as part of Customer because on server side it can be identified based on customer token used for authentication. There is no need to know customer ID on the client side.")
is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\IsSubscribed")
addresses: [CustomerAddress] @doc(description: "An array containing the customer's shipping and billing addresses") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CustomerAddresses")
gender: Int @doc(description: "The customer's gender(Male - 1, Female - 2)")
@@ -97,9 +97,9 @@ type Customer @doc(description: "Customer defines the customer name and address
type CustomerAddress @doc(description: "CustomerAddress contains detailed information about a customer's billing and shipping addresses"){
id: Int @doc(description: "The ID assigned to the address object")
- customer_id: Int @doc(description: "The customer ID")
+ customer_id: Int @doc(description: "The customer ID") @deprecated(reason: "customer_id is not needed as part of CustomerAddress, address ID (id) is unique identifier for the addresses.")
region: CustomerAddressRegion @doc(description: "An object containing the region name, region code, and region ID")
- region_id: Int @doc(description: "A number that uniquely identifies the state, province, or other area")
+ region_id: Int @deprecated(reason: "Region ID is excessive on storefront and region code should suffice for all scenarios")
country_id: String @doc(description: "The customer's country")
street: [String] @doc(description: "An array of strings that define the street number and name")
company: String @doc(description: "The customer's company")
@@ -115,14 +115,14 @@ type CustomerAddress @doc(description: "CustomerAddress contains detailed inform
vat_id: String @doc(description: "The customer's Tax/VAT number (for corporate customers)")
default_shipping: Boolean @doc(description: "Indicates whether the address is the default shipping address")
default_billing: Boolean @doc(description: "Indicates whether the address is the default billing address")
- custom_attributes: [CustomerAddressAttribute] @doc(description: "Address custom attributes")
+ custom_attributes: [CustomerAddressAttribute] @deprecated(reason: "Custom attributes should not be put into container")
extension_attributes: [CustomerAddressAttribute] @doc(description: "Address extension attributes")
}
type CustomerAddressRegion @doc(description: "CustomerAddressRegion defines the customer's state or province") {
region_code: String @doc(description: "The address region code")
region: String @doc(description: "The state or province name")
- region_id: Int @doc(description: "Uniquely identifies the region")
+ region_id: Int @deprecated(reason: "Region ID is excessive on storefront and region code should suffice for all scenarios")
}
type CustomerAddressAttribute {
diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php
index 14759bd130f2b..f86ebaea69730 100644
--- a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php
+++ b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php
@@ -3,6 +3,8 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\CustomerImportExport\Model\Import;
use Magento\Customer\Api\Data\CustomerInterface;
@@ -21,7 +23,7 @@
class Customer extends AbstractCustomer
{
/**
- * Attribute collection name
+ * Collection name attribute
*/
const ATTRIBUTE_COLLECTION_NAME = \Magento\Customer\Model\ResourceModel\Attribute\Collection::class;
@@ -519,8 +521,10 @@ protected function _importData()
);
} elseif ($this->getBehavior($rowData) == \Magento\ImportExport\Model\Import::BEHAVIOR_ADD_UPDATE) {
$processedData = $this->_prepareDataForUpdate($rowData);
+ // phpcs:disable Magento2.Performance.ForeachArrayMerge
$entitiesToCreate = array_merge($entitiesToCreate, $processedData[self::ENTITIES_TO_CREATE_KEY]);
$entitiesToUpdate = array_merge($entitiesToUpdate, $processedData[self::ENTITIES_TO_UPDATE_KEY]);
+ // phpcs:enable
foreach ($processedData[self::ATTRIBUTES_TO_SAVE_KEY] as $tableName => $customerAttributes) {
if (!isset($attributesToSave[$tableName])) {
$attributesToSave[$tableName] = [];
@@ -598,14 +602,18 @@ protected function _validateRowForUpdate(array $rowData, $rowNumber)
$isFieldNotSetAndCustomerDoesNotExist =
!isset($rowData[$attributeCode]) && !$this->_getCustomerId($email, $website);
$isFieldSetAndTrimmedValueIsEmpty
- = isset($rowData[$attributeCode]) && '' === trim($rowData[$attributeCode]);
+ = isset($rowData[$attributeCode]) && '' === trim((string)$rowData[$attributeCode]);
if ($isFieldRequired && ($isFieldNotSetAndCustomerDoesNotExist || $isFieldSetAndTrimmedValueIsEmpty)) {
$this->addRowError(self::ERROR_VALUE_IS_REQUIRED, $rowNumber, $attributeCode);
continue;
}
- if (isset($rowData[$attributeCode]) && strlen($rowData[$attributeCode])) {
+ if (isset($rowData[$attributeCode]) && strlen((string)$rowData[$attributeCode])) {
+ if ($attributeParams['type'] == 'select') {
+ continue;
+ }
+
$this->isAttributeValid(
$attributeCode,
$attributeParams,
diff --git a/app/code/Magento/Deploy/Service/Bundle.php b/app/code/Magento/Deploy/Service/Bundle.php
index f16b93a185595..26e61624c219e 100644
--- a/app/code/Magento/Deploy/Service/Bundle.php
+++ b/app/code/Magento/Deploy/Service/Bundle.php
@@ -216,7 +216,7 @@ private function isExcluded($filePath, $area, $theme)
$excludedFiles = $this->bundleConfig->getExcludedFiles($area, $theme);
foreach ($excludedFiles as $excludedFileId) {
$excludedFilePath = $this->prepareExcludePath($excludedFileId);
- if ($excludedFilePath === $filePath) {
+ if ($excludedFilePath === $filePath || $excludedFilePath === str_replace('.min.js', '.js', $filePath)) {
return true;
}
}
diff --git a/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoDeveloperModeOnlyTestSuite.xml b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoDeveloperModeOnlyTestSuite.xml
new file mode 100644
index 0000000000000..3a7d3663c8875
--- /dev/null
+++ b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoDeveloperModeOnlyTestSuite.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoProductionModeOnlyTestSuite.xml b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoProductionModeOnlyTestSuite.xml
new file mode 100644
index 0000000000000..bf7014cdbb49d
--- /dev/null
+++ b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoProductionModeOnlyTestSuite.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Directory/Model/Observer.php b/app/code/Magento/Directory/Model/Observer.php
index e35c2de5cee5b..6d227e7148261 100644
--- a/app/code/Magento/Directory/Model/Observer.php
+++ b/app/code/Magento/Directory/Model/Observer.php
@@ -12,6 +12,11 @@
namespace Magento\Directory\Model;
+/**
+ * Class Observer
+ *
+ * @package Magento\Directory\Model
+ */
class Observer
{
const CRON_STRING_PATH = 'crontab/default/jobs/currency_rates_update/schedule/cron_expr';
@@ -83,6 +88,8 @@ public function __construct(
}
/**
+ * Schedule update currency rates
+ *
* @param mixed $schedule
* @return void
* @throws \Exception
@@ -122,7 +129,7 @@ public function scheduledUpdateCurrencyRates($schedule)
$importWarnings[] = __('FATAL ERROR:') . ' ' . __('Please specify the correct Import Service.');
}
- if (sizeof($errors) > 0) {
+ if (count($errors) > 0) {
foreach ($errors as $error) {
$importWarnings[] = __('WARNING:') . ' ' . $error;
}
@@ -132,7 +139,7 @@ public function scheduledUpdateCurrencyRates($schedule)
self::XML_PATH_ERROR_RECIPIENT,
\Magento\Store\Model\ScopeInterface::SCOPE_STORE
);
- if (sizeof($importWarnings) == 0) {
+ if (count($importWarnings) == 0) {
$this->_currencyFactory->create()->saveRates($rates);
} elseif ($errorRecipient) {
//if $errorRecipient is not set, there is no sense send email to nobody
diff --git a/app/code/Magento/Directory/etc/crontab.xml b/app/code/Magento/Directory/etc/crontab.xml
index d6868ff6aa0d6..589cd394d7cf1 100644
--- a/app/code/Magento/Directory/etc/crontab.xml
+++ b/app/code/Magento/Directory/etc/crontab.xml
@@ -7,6 +7,8 @@
-->
-
+
+ crontab/default/jobs/currency_rates_update/schedule/cron_expr
+
diff --git a/app/code/Magento/Directory/etc/db_schema.xml b/app/code/Magento/Directory/etc/db_schema.xml
index c11e0ee525e37..163e972423b98 100644
--- a/app/code/Magento/Directory/etc/db_schema.xml
+++ b/app/code/Magento/Directory/etc/db_schema.xml
@@ -8,7 +8,7 @@
-
+ comment="Country Format ID"/>
+
@@ -31,9 +31,9 @@
+ comment="Region ID"/>
+ comment="Country ID in ISO-2"/>
@@ -47,7 +47,7 @@
comment="Directory Country Region Name">
+ default="0" comment="Region ID"/>
diff --git a/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/StorefrontOpenDownloadableLinkActionGroup.xml b/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/StorefrontOpenDownloadableLinkActionGroup.xml
new file mode 100644
index 0000000000000..565439655138e
--- /dev/null
+++ b/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/StorefrontOpenDownloadableLinkActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/StorefrontOpenDownloadableSampleActionGroup.xml b/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/StorefrontOpenDownloadableSampleActionGroup.xml
new file mode 100644
index 0000000000000..25ac45317fe42
--- /dev/null
+++ b/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/StorefrontOpenDownloadableSampleActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml b/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml
index 08f1c2349357d..eb3ad674a0fdf 100644
--- a/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml
+++ b/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml
@@ -63,6 +63,12 @@
URL
https://static.magento.com/sites/all/themes/mag_redesign/images/magento-logo.svg
+
+ downloadableSampleUrl
+ 1
+ url
+ http://example.com
+
Api Downloadable Link
2.00
@@ -72,4 +78,4 @@
0
https://static.magento.com/sites/all/themes/mag_redesign/images/magento-logo.svg
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Downloadable/Test/Mftf/Metadata/downloadable_link_sample-meta.xml b/app/code/Magento/Downloadable/Test/Mftf/Metadata/downloadable_link_sample-meta.xml
new file mode 100644
index 0000000000000..b26bbb7af5a35
--- /dev/null
+++ b/app/code/Magento/Downloadable/Test/Mftf/Metadata/downloadable_link_sample-meta.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+ application/json
+
+ string
+ integer
+ string
+ string
+ sample_file_content
+ string
+
+ boolean
+
+
diff --git a/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontDownloadableLinkPage.xml b/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontDownloadableLinkPage.xml
new file mode 100644
index 0000000000000..7ab6b211d7441
--- /dev/null
+++ b/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontDownloadableLinkPage.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontDownloadableSamplePage.xml b/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontDownloadableSamplePage.xml
new file mode 100644
index 0000000000000..0d588faa777c0
--- /dev/null
+++ b/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontDownloadableSamplePage.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontProductPage.xml b/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontProductPage.xml
new file mode 100644
index 0000000000000..7b9d205d19dc5
--- /dev/null
+++ b/app/code/Magento/Downloadable/Test/Mftf/Page/StorefrontProductPage.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableProductSection.xml b/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableProductSection.xml
index a1db2d4d94941..20b62ef060309 100644
--- a/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableProductSection.xml
+++ b/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableProductSection.xml
@@ -12,5 +12,8 @@
+
+
+
diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml
new file mode 100644
index 0000000000000..8cb0d5fde9863
--- /dev/null
+++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/EditDownloadableProductWithSeparateLinksFromCartTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/EditDownloadableProductWithSeparateLinksFromCartTest.xml
new file mode 100644
index 0000000000000..0b905964fd2d9
--- /dev/null
+++ b/app/code/Magento/Downloadable/Test/Mftf/Test/EditDownloadableProductWithSeparateLinksFromCartTest.xml
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/SelectAllDownloadableLinksDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/SelectAllDownloadableLinksDownloadableProductTest.xml
new file mode 100644
index 0000000000000..94940f0e08195
--- /dev/null
+++ b/app/code/Magento/Downloadable/Test/Mftf/Test/SelectAllDownloadableLinksDownloadableProductTest.xml
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/VerifyDisableDownloadableProductSamplesAreNotAccessibleTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/VerifyDisableDownloadableProductSamplesAreNotAccessibleTest.xml
new file mode 100644
index 0000000000000..f29bbcf925e26
--- /dev/null
+++ b/app/code/Magento/Downloadable/Test/Mftf/Test/VerifyDisableDownloadableProductSamplesAreNotAccessibleTest.xml
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Downloadable/view/frontend/web/js/downloadable.js b/app/code/Magento/Downloadable/view/frontend/web/js/downloadable.js
index 09a5ad1afa9ec..a1e8c785c696a 100644
--- a/app/code/Magento/Downloadable/view/frontend/web/js/downloadable.js
+++ b/app/code/Magento/Downloadable/view/frontend/web/js/downloadable.js
@@ -38,6 +38,8 @@ define([
});
}
});
+
+ this._reloadPrice();
},
/**
diff --git a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php
index 72f4086c1c56b..7af7bf447c45a 100644
--- a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php
+++ b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php
@@ -4,16 +4,14 @@
* See COPYING.txt for license details.
*/
-/**
- * Attribute add/edit form options tab
- *
- * @author Magento Core Team
- */
namespace Magento\Eav\Block\Adminhtml\Attribute\Edit\Options;
use Magento\Store\Model\ResourceModel\Store\Collection;
+use Magento\Eav\Model\Entity\Attribute\AbstractAttribute;
/**
+ * Attribute add/edit form options tab
+ *
* @api
* @since 100.0.2
*/
@@ -61,6 +59,7 @@ public function __construct(
/**
* Is true only for system attributes which use source model
+ *
* Option labels and position for such attributes are kept in source model and thus cannot be overridden
*
* @return bool
@@ -96,12 +95,16 @@ public function getStoresSortedBySortOrder()
{
$stores = $this->getStores();
if (is_array($stores)) {
- usort($stores, function ($storeA, $storeB) {
- if ($storeA->getSortOrder() == $storeB->getSortOrder()) {
- return $storeA->getId() < $storeB->getId() ? -1 : 1;
+ usort(
+ $stores,
+ function ($storeA, $storeB) {
+ if ($storeA->getSortOrder() == $storeB->getSortOrder()) {
+ return $storeA->getId() < $storeB->getId() ? -1 : 1;
+ }
+
+ return ($storeA->getSortOrder() < $storeB->getSortOrder()) ? -1 : 1;
}
- return ($storeA->getSortOrder() < $storeB->getSortOrder()) ? -1 : 1;
- });
+ );
}
return $stores;
}
@@ -130,12 +133,14 @@ public function getOptionValues()
}
/**
- * @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute
+ * Preparing values of attribute options
+ *
+ * @param AbstractAttribute $attribute
* @param array|\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection $optionCollection
* @return array
*/
protected function _prepareOptionValues(
- \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute,
+ AbstractAttribute $attribute,
$optionCollection
) {
$type = $attribute->getFrontendInput();
@@ -149,6 +154,41 @@ protected function _prepareOptionValues(
$values = [];
$isSystemAttribute = is_array($optionCollection);
+ if ($isSystemAttribute) {
+ $values = $this->getPreparedValues($optionCollection, $isSystemAttribute, $inputType, $defaultValues);
+ } else {
+ $optionCollection->setPageSize(200);
+ $pageCount = $optionCollection->getLastPageNumber();
+ $currentPage = 1;
+ while ($currentPage <= $pageCount) {
+ $optionCollection->clear();
+ $optionCollection->setCurPage($currentPage);
+ $values = array_merge(
+ $values,
+ $this->getPreparedValues($optionCollection, $isSystemAttribute, $inputType, $defaultValues)
+ );
+ $currentPage++;
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * Return prepared values of system or user defined attribute options
+ *
+ * @param array|\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection $optionCollection
+ * @param bool $isSystemAttribute
+ * @param string $inputType
+ * @param array $defaultValues
+ */
+ private function getPreparedValues(
+ $optionCollection,
+ bool $isSystemAttribute,
+ string $inputType,
+ array $defaultValues
+ ) {
+ $values = [];
foreach ($optionCollection as $option) {
$bunch = $isSystemAttribute ? $this->_prepareSystemAttributeOptionValues(
$option,
@@ -169,12 +209,13 @@ protected function _prepareOptionValues(
/**
* Retrieve option values collection
+ *
* It is represented by an array in case of system attribute
*
- * @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute
+ * @param AbstractAttribute $attribute
* @return array|\Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection
*/
- protected function _getOptionValuesCollection(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute)
+ protected function _getOptionValuesCollection(AbstractAttribute $attribute)
{
if ($this->canManageOptionDefaultOnly()) {
$options = $this->_universalFactory->create(
@@ -226,7 +267,7 @@ protected function _prepareSystemAttributeOptionValues($option, $inputType, $def
foreach ($this->getStores() as $store) {
$storeId = $store->getId();
$value['store' . $storeId] = $storeId ==
- \Magento\Store\Model\Store::DEFAULT_STORE_ID ? $valuePrefix . $this->escapeHtml($option['label']) : '';
+ \Magento\Store\Model\Store::DEFAULT_STORE_ID ? $valuePrefix . $this->escapeHtml($option['label']) : '';
}
return [$value];
diff --git a/app/code/Magento/Eav/Model/Entity/Attribute.php b/app/code/Magento/Eav/Model/Entity/Attribute.php
index bb2477d4df827..8bd9ca2cc03c8 100644
--- a/app/code/Magento/Eav/Model/Entity/Attribute.php
+++ b/app/code/Magento/Eav/Model/Entity/Attribute.php
@@ -32,12 +32,12 @@ class Attribute extends \Magento\Eav\Model\Entity\Attribute\AbstractAttribute im
const ATTRIBUTE_CODE_MAX_LENGTH = 60;
/**
- * Attribute code min length.
+ * Min accepted length of an attribute code.
*/
const ATTRIBUTE_CODE_MIN_LENGTH = 1;
/**
- * Cache tag
+ * Tag to use for attributes caching.
*/
const CACHE_TAG = 'EAV_ATTRIBUTE';
@@ -311,7 +311,7 @@ public function beforeSave()
}
/**
- * Save additional data
+ * @inheritdoc
*
* @return $this
* @throws LocalizedException
@@ -489,6 +489,9 @@ public function getIdentities()
/**
* @inheritdoc
* @since 100.0.7
+ *
+ * @SuppressWarnings(PHPMD.SerializationAware)
+ * @deprecated Do not use PHP serialization.
*/
public function __sleep()
{
@@ -502,6 +505,9 @@ public function __sleep()
/**
* @inheritdoc
* @since 100.0.7
+ *
+ * @SuppressWarnings(PHPMD.SerializationAware)
+ * @deprecated Do not use PHP serialization.
*/
public function __wakeup()
{
diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php
index 3857118ae67ca..16fe495de18db 100644
--- a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php
+++ b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php
@@ -1405,6 +1405,9 @@ public function setExtensionAttributes(\Magento\Eav\Api\Data\AttributeExtensionI
/**
* @inheritdoc
* @since 100.0.7
+ *
+ * @SuppressWarnings(PHPMD.SerializationAware)
+ * @deprecated Do not use PHP serialization.
*/
public function __sleep()
{
@@ -1430,6 +1433,9 @@ public function __sleep()
/**
* @inheritdoc
* @since 100.0.7
+ *
+ * @SuppressWarnings(PHPMD.SerializationAware)
+ * @deprecated Do not use PHP serialization.
*/
public function __wakeup()
{
diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php
index 0e7a46125d872..c5077733e10ae 100644
--- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php
+++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php
@@ -457,6 +457,7 @@ protected function _updateAttributeOption($object, $optionId, $option)
if (!empty($option['delete'][$optionId])) {
if ($intOptionId) {
$connection->delete($table, ['option_id = ?' => $intOptionId]);
+ $this->clearSelectedOptionInEntities($object, $intOptionId);
}
return false;
}
@@ -475,6 +476,41 @@ protected function _updateAttributeOption($object, $optionId, $option)
return $intOptionId;
}
+ /**
+ * Clear selected option in entities
+ *
+ * @param EntityAttribute|AbstractModel $object
+ * @param int $optionId
+ * @return void
+ */
+ private function clearSelectedOptionInEntities(AbstractModel $object, int $optionId)
+ {
+ $backendTable = $object->getBackendTable();
+ $attributeId = $object->getAttributeId();
+ if (!$backendTable || !$attributeId) {
+ return;
+ }
+
+ $connection = $this->getConnection();
+ $where = $connection->quoteInto('attribute_id = ?', $attributeId);
+ $update = [];
+
+ if ($object->getBackendType() === 'varchar') {
+ $where.= ' AND ' . $connection->prepareSqlCondition('value', ['finset' => $optionId]);
+ $concat = $connection->getConcatSql(["','", 'value', "','"]);
+ $expr = $connection->quoteInto(
+ "TRIM(BOTH ',' FROM REPLACE($concat,',?,',','))",
+ $optionId
+ );
+ $update['value'] = new \Zend_Db_Expr($expr);
+ } else {
+ $where.= $connection->quoteInto(' AND value = ?', $optionId);
+ $update['value'] = null;
+ }
+
+ $connection->update($backendTable, $update, $where);
+ }
+
/**
* Save option values records per store
*
@@ -725,6 +761,9 @@ public function getValidAttributeIds($attributeIds)
*
* @return array
* @since 100.0.7
+ *
+ * @SuppressWarnings(PHPMD.SerializationAware)
+ * @deprecated Do not use PHP serialization.
*/
public function __sleep()
{
@@ -738,6 +777,9 @@ public function __sleep()
*
* @return void
* @since 100.0.7
+ *
+ * @SuppressWarnings(PHPMD.SerializationAware)
+ * @deprecated Do not use PHP serialization.
*/
public function __wakeup()
{
diff --git a/app/code/Magento/Eav/etc/db_schema.xml b/app/code/Magento/Eav/etc/db_schema.xml
index b6c42d725e5e9..ba1c27c703790 100644
--- a/app/code/Magento/Eav/etc/db_schema.xml
+++ b/app/code/Magento/Eav/etc/db_schema.xml
@@ -9,7 +9,7 @@
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
+ comment="Entity Type ID"/>
@@ -21,7 +21,7 @@
+ identity="false" default="0" comment="Default Attribute Set ID"/>
@@ -44,14 +44,14 @@
+ default="0" comment="Entity Type ID"/>
-
+ identity="false" default="0" comment="Attribute Set ID"/>
+
+ default="0" comment="Parent ID"/>
+ default="0" comment="Store ID"/>
+ comment="Value ID"/>
+ default="0" comment="Entity Type ID"/>
+ default="0" comment="Attribute ID"/>
+ default="0" comment="Store ID"/>
@@ -115,13 +115,13 @@
+ comment="Value ID"/>
+ default="0" comment="Entity Type ID"/>
+ default="0" comment="Attribute ID"/>
+ default="0" comment="Store ID"/>
+ comment="Value ID"/>
+ default="0" comment="Entity Type ID"/>
+ default="0" comment="Attribute ID"/>
+ default="0" comment="Store ID"/>
+ comment="Value ID"/>
+ default="0" comment="Entity Type ID"/>
+ default="0" comment="Attribute ID"/>
+ default="0" comment="Store ID"/>
@@ -233,13 +233,13 @@
+ comment="Value ID"/>
+ default="0" comment="Entity Type ID"/>
+ default="0" comment="Attribute ID"/>
+ default="0" comment="Store ID"/>
@@ -273,9 +273,9 @@
+ comment="Attribute ID"/>
+ default="0" comment="Entity Type ID"/>
@@ -308,13 +308,13 @@
+ comment="Entity Store ID"/>
+ default="0" comment="Entity Type ID"/>
+ default="0" comment="Store ID"/>
-
+
@@ -332,9 +332,9 @@
+ comment="Attribute Set ID"/>
+ default="0" comment="Entity Type ID"/>
@@ -355,15 +355,15 @@
+ identity="true" comment="Attribute Group ID"/>
+ identity="false" default="0" comment="Attribute Set ID"/>
+ default="0" comment="Default ID"/>
@@ -388,15 +388,15 @@
+ comment="Entity Attribute ID"/>
+ default="0" comment="Entity Type ID"/>
+ identity="false" default="0" comment="Attribute Set ID"/>
+ identity="false" default="0" comment="Attribute Group ID"/>
+ default="0" comment="Attribute ID"/>
@@ -426,9 +426,9 @@
+ comment="Option ID"/>
+ default="0" comment="Attribute ID"/>
@@ -443,11 +443,11 @@
+ comment="Value ID"/>
+ default="0" comment="Option ID"/>
+ default="0" comment="Store ID"/>
@@ -467,11 +467,11 @@
+ comment="Attribute Label ID"/>
+ default="0" comment="Attribute ID"/>
+ default="0" comment="Store ID"/>
@@ -491,14 +491,14 @@
+ comment="Type ID"/>
+ comment="Store ID"/>
@@ -515,9 +515,9 @@
+ comment="Type ID"/>
+ comment="Entity Type ID"/>
@@ -534,9 +534,9 @@
+ comment="Fieldset ID"/>
+ comment="Type ID"/>
@@ -552,9 +552,9 @@
+ comment="Fieldset ID"/>
+ comment="Store ID"/>
@@ -572,13 +572,13 @@
+ comment="Element ID"/>
+ comment="Type ID"/>
+ comment="Fieldset ID"/>
+ comment="Attribute ID"/>
diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/AttributeOptions.php b/app/code/Magento/EavGraphQl/Model/Resolver/AttributeOptions.php
index e4c27adc60247..7361d52372cd6 100644
--- a/app/code/Magento/EavGraphQl/Model/Resolver/AttributeOptions.php
+++ b/app/code/Magento/EavGraphQl/Model/Resolver/AttributeOptions.php
@@ -57,29 +57,31 @@ public function resolve(
array $args = null
) : Value {
- return $this->valueFactory->create(function () use ($value) {
- $entityType = $this->getEntityType($value);
- $attributeCode = $this->getAttributeCode($value);
+ return $this->valueFactory->create(
+ function () use ($value) {
+ $entityType = $this->getEntityType($value);
+ $attributeCode = $this->getAttributeCode($value);
- $optionsData = $this->getAttributeOptionsData($entityType, $attributeCode);
- return $optionsData;
- });
+ $optionsData = $this->getAttributeOptionsData($entityType, $attributeCode);
+ return $optionsData;
+ }
+ );
}
/**
* Get entity type
*
* @param array $value
- * @return int
+ * @return string
* @throws LocalizedException
*/
- private function getEntityType(array $value): int
+ private function getEntityType(array $value): string
{
if (!isset($value['entity_type'])) {
throw new LocalizedException(__('"Entity type should be specified'));
}
- return (int)$value['entity_type'];
+ return $value['entity_type'];
}
/**
@@ -101,13 +103,13 @@ private function getAttributeCode(array $value): string
/**
* Get attribute options data
*
- * @param int $entityType
+ * @param string $entityType
* @param string $attributeCode
* @return array
* @throws GraphQlInputException
* @throws GraphQlNoSuchEntityException
*/
- private function getAttributeOptionsData(int $entityType, string $attributeCode): array
+ private function getAttributeOptionsData(string $entityType, string $attributeCode): array
{
try {
$optionsData = $this->attributeOptionsDataProvider->getData($entityType, $attributeCode);
diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php b/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php
index 62e3f01836619..85445580bb1fb 100644
--- a/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php
+++ b/app/code/Magento/EavGraphQl/Model/Resolver/CustomAttributeMetadata.php
@@ -9,6 +9,7 @@
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\EavGraphQl\Model\Resolver\Query\Type;
+use Magento\EavGraphQl\Model\Resolver\Query\FrontendType;
use Magento\Framework\Exception\InputException;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\GraphQl\Config\Element\Field;
@@ -26,12 +27,19 @@ class CustomAttributeMetadata implements ResolverInterface
*/
private $type;
+ /**
+ * @var FrontendType
+ */
+ private $frontendType;
+
/**
* @param Type $type
+ * @param FrontendType $frontendType
*/
- public function __construct(Type $type)
+ public function __construct(Type $type, FrontendType $frontendType)
{
$this->type = $type;
+ $this->frontendType = $frontendType;
}
/**
@@ -52,6 +60,7 @@ public function resolve(
continue;
}
try {
+ $frontendType = $this->frontendType->getType($attribute['attribute_code'], $attribute['entity_type']);
$type = $this->type->getType($attribute['attribute_code'], $attribute['entity_type']);
} catch (InputException $exception) {
$attributes['items'][] = new GraphQlNoSuchEntityException(
@@ -78,7 +87,8 @@ public function resolve(
$attributes['items'][] = [
'attribute_code' => $attribute['attribute_code'],
'entity_type' => $attribute['entity_type'],
- 'attribute_type' => ucfirst($type)
+ 'attribute_type' => ucfirst($type),
+ 'input_type' => $frontendType
];
}
diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/DataProvider/AttributeOptions.php b/app/code/Magento/EavGraphQl/Model/Resolver/DataProvider/AttributeOptions.php
index 900a31c1093ed..3371fbe658c9c 100644
--- a/app/code/Magento/EavGraphQl/Model/Resolver/DataProvider/AttributeOptions.php
+++ b/app/code/Magento/EavGraphQl/Model/Resolver/DataProvider/AttributeOptions.php
@@ -29,11 +29,13 @@ public function __construct(
}
/**
- * @param int $entityType
+ * Get attribute options data
+ *
+ * @param string $entityType
* @param string $attributeCode
* @return array
*/
- public function getData(int $entityType, string $attributeCode): array
+ public function getData(string $entityType, string $attributeCode): array
{
$options = $this->optionManager->getItems($entityType, $attributeCode);
diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/Query/FrontendType.php b/app/code/Magento/EavGraphQl/Model/Resolver/Query/FrontendType.php
new file mode 100644
index 0000000000000..c76f19e6dfeb4
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Model/Resolver/Query/FrontendType.php
@@ -0,0 +1,61 @@
+attributeRepository = $attributeRepository;
+ $this->serviceTypeMap = $serviceTypeMap;
+ }
+
+ /**
+ * Return frontend type for attribute
+ *
+ * @param string $attributeCode
+ * @param string $entityType
+ * @return null|string
+ */
+ public function getType(string $attributeCode, string $entityType): ?string
+ {
+ $mappedEntityType = $this->serviceTypeMap->getEntityType($entityType);
+ if ($mappedEntityType) {
+ $entityType = $mappedEntityType;
+ }
+ try {
+ $attribute = $this->attributeRepository->get($entityType, $attributeCode);
+ } catch (NoSuchEntityException $e) {
+ return null;
+ }
+ return $attribute->getFrontendInput();
+ }
+}
diff --git a/app/code/Magento/EavGraphQl/etc/schema.graphqls b/app/code/Magento/EavGraphQl/etc/schema.graphqls
index 0b174fbc4d84d..21aa7001fab2b 100644
--- a/app/code/Magento/EavGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/EavGraphQl/etc/schema.graphqls
@@ -13,6 +13,7 @@ type Attribute @doc(description: "Attribute contains the attribute_type of the s
attribute_code: String @doc(description: "The unique identifier for an attribute code. This value should be in lowercase letters without spaces.")
entity_type: String @doc(description: "The type of entity that defines the attribute")
attribute_type: String @doc(description: "The data type of the attribute")
+ input_type: String @doc(description: "The frontend input type of the attribute")
attribute_options: [AttributeOption] @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\AttributeOptions") @doc(description: "Attribute options list.")
}
diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php b/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php
index 97a76de4b995a..f6d0a6318a5f2 100644
--- a/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php
+++ b/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php
@@ -195,15 +195,11 @@ public function cleanIndex($storeId, $mappedIndexerId)
{
$this->checkIndex($storeId, $mappedIndexerId, true);
$indexName = $this->indexNameResolver->getIndexName($storeId, $mappedIndexerId, $this->preparedIndex);
- if ($this->client->isEmptyIndex($indexName)) {
- // use existing index if empty
- return $this;
- }
// prepare new index name and increase version
$indexPattern = $this->indexNameResolver->getIndexPattern($storeId, $mappedIndexerId);
$version = (int)(str_replace($indexPattern, '', $indexName));
- $newIndexName = $indexPattern . ++$version;
+ $newIndexName = $indexPattern . (++$version);
// remove index if already exists
if ($this->client->indexExists($newIndexName)) {
@@ -354,12 +350,14 @@ protected function prepareIndex($storeId, $indexName, $mappedIndexerId)
{
$this->indexBuilder->setStoreId($storeId);
$settings = $this->indexBuilder->build();
- $allAttributeTypes = $this->fieldMapper->getAllAttributesTypes([
- 'entityType' => $mappedIndexerId,
- // Use store id instead of website id from context for save existing fields mapping.
- // In future websiteId will be eliminated due to index stored per store
- 'websiteId' => $storeId
- ]);
+ $allAttributeTypes = $this->fieldMapper->getAllAttributesTypes(
+ [
+ 'entityType' => $mappedIndexerId,
+ // Use store id instead of website id from context for save existing fields mapping.
+ // In future websiteId will be eliminated due to index stored per store
+ 'websiteId' => $storeId
+ ]
+ );
$settings['index']['mapping']['total_fields']['limit'] = $this->getMappingTotalFieldsLimit($allAttributeTypes);
$this->client->createIndex($indexName, ['settings' => $settings]);
$this->client->addFieldsMapping(
diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php
index 165f7e78eb65f..41a50961ae4bc 100644
--- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php
+++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeAdapter.php
@@ -115,6 +115,18 @@ public function isBooleanType(): bool
&& $this->getAttribute()->getBackendType() !== 'varchar';
}
+ /**
+ * Check if attribute is text type
+ *
+ * @return bool
+ */
+ public function isTextType(): bool
+ {
+ return in_array($this->getAttribute()->getBackendType(), ['varchar', 'static'], true)
+ && in_array($this->getFrontendInput(), ['text'], true)
+ && $this->getAttribute()->getIsVisible();
+ }
+
/**
* Check if attribute has boolean type.
*
diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php
index 6876b23bbb156..0f3020974d08a 100644
--- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php
+++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php
@@ -130,6 +130,15 @@ public function getFields(array $context = []): array
];
}
+ if ($attributeAdapter->isTextType()) {
+ $keywordFieldName = FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD;
+ $allAttributes[$fieldName]['fields'][$keywordFieldName] = [
+ 'type' => $this->fieldTypeConverter->convert(
+ FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD
+ )
+ ];
+ }
+
if ($attributeAdapter->isComplexType()) {
$childFieldName = $this->fieldNameResolver->getFieldName(
$attributeAdapter,
diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php
index 1c8885fec63be..ce88fc290e23c 100644
--- a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php
+++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolver.php
@@ -76,9 +76,6 @@ public function __construct(
*/
public function resolve(): SearchCriteria
{
- if ($this->size !== 0) {
- $this->builder->setPageSize($this->size);
- }
$searchCriteria = $this->builder->create();
$searchCriteria->setRequestName($this->searchRequestName);
$searchCriteria->setSortOrders($this->orders);
diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php
index 3ae2d384782c3..aac396f238358 100644
--- a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php
+++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/SearchResultApplier.php
@@ -25,16 +25,32 @@ class SearchResultApplier implements SearchResultApplierInterface
*/
private $searchResult;
+ /**
+ * @var int
+ */
+ private $size;
+
+ /**
+ * @var int
+ */
+ private $currentPage;
+
/**
* @param Collection $collection
* @param SearchResultInterface $searchResult
+ * @param int $size
+ * @param int $currentPage
*/
public function __construct(
Collection $collection,
- SearchResultInterface $searchResult
+ SearchResultInterface $searchResult,
+ int $size,
+ int $currentPage
) {
$this->collection = $collection;
$this->searchResult = $searchResult;
+ $this->size = $size;
+ $this->currentPage = $currentPage;
}
/**
@@ -46,8 +62,10 @@ public function apply()
$this->collection->getSelect()->where('NULL');
return;
}
+
+ $items = $this->sliceItems($this->searchResult->getItems(), $this->size, $this->currentPage);
$ids = [];
- foreach ($this->searchResult->getItems() as $item) {
+ foreach ($items as $item) {
$ids[] = (int)$item->getId();
}
$this->collection->setPageSize(null);
@@ -56,4 +74,25 @@ public function apply()
$this->collection->getSelect()->reset(\Magento\Framework\DB\Select::ORDER);
$this->collection->getSelect()->order("FIELD(e.entity_id,$orderList)");
}
+
+ /**
+ * Slice current items
+ *
+ * @param array $items
+ * @param int $size
+ * @param int $currentPage
+ * @return array
+ */
+ private function sliceItems(array $items, int $size, int $currentPage): array
+ {
+ if ($size !== 0) {
+ $offset = ($currentPage - 1) * $size;
+ if ($offset < 0) {
+ $offset = 0;
+ }
+ $items = array_slice($items, $offset, $this->size);
+ }
+
+ return $items;
+ }
}
diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php b/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php
index ed8cd049d2915..d88c7e53d813a 100644
--- a/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php
+++ b/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Term.php
@@ -5,10 +5,17 @@
*/
namespace Magento\Elasticsearch\SearchAdapter\Filter\Builder;
+use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Search\Request\Filter\Term as TermFilterRequest;
use Magento\Framework\Search\Request\FilterInterface as RequestFilterInterface;
use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface;
+use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\ConverterInterface
+ as FieldTypeConverterInterface;
+/**
+ * Term filter builder
+ */
class Term implements FilterInterface
{
/**
@@ -16,26 +23,56 @@ class Term implements FilterInterface
*/
protected $fieldMapper;
+ /**
+ * @var AttributeProvider
+ */
+ private $attributeAdapterProvider;
+
+ /**
+ * @var array
+ * @see \Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Resolver\IntegerType::$integerTypeAttributes
+ */
+ private $integerTypeAttributes = ['category_ids'];
+
/**
* @param FieldMapperInterface $fieldMapper
+ * @param AttributeProvider $attributeAdapterProvider
+ * @param array $integerTypeAttributes
*/
- public function __construct(FieldMapperInterface $fieldMapper)
- {
+ public function __construct(
+ FieldMapperInterface $fieldMapper,
+ AttributeProvider $attributeAdapterProvider = null,
+ array $integerTypeAttributes = []
+ ) {
$this->fieldMapper = $fieldMapper;
+ $this->attributeAdapterProvider = $attributeAdapterProvider
+ ?? ObjectManager::getInstance()->get(AttributeProvider::class);
+ $this->integerTypeAttributes = array_merge($this->integerTypeAttributes, $integerTypeAttributes);
}
/**
+ * Build term filter request
+ *
* @param RequestFilterInterface|TermFilterRequest $filter
* @return array
*/
public function buildFilter(RequestFilterInterface $filter)
{
$filterQuery = [];
+
+ $attribute = $this->attributeAdapterProvider->getByAttributeCode($filter->getField());
+ $fieldName = $this->fieldMapper->getFieldName($filter->getField());
+
+ if ($attribute->isTextType() && !in_array($attribute->getAttributeCode(), $this->integerTypeAttributes)) {
+ $suffix = FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD;
+ $fieldName .= '.' . $suffix;
+ }
+
if ($filter->getValue()) {
$operator = is_array($filter->getValue()) ? 'terms' : 'term';
$filterQuery []= [
$operator => [
- $this->fieldMapper->getFieldName($filter->getField()) => $filter->getValue(),
+ $fieldName => $filter->getValue(),
],
];
}
diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php
index ec50ba7b3d5df..326c04aad6165 100644
--- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php
+++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php
@@ -77,6 +77,7 @@ class ElasticsearchTest extends \PHPUnit\Framework\TestCase
* Setup
*
* @return void
+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
protected function setUp()
{
@@ -93,10 +94,12 @@ protected function setUp()
->getMock();
$this->clientConfig = $this->getMockBuilder(\Magento\Elasticsearch\Model\Config::class)
->disableOriginalConstructor()
- ->setMethods([
- 'getIndexPrefix',
- 'getEntityType',
- ])->getMock();
+ ->setMethods(
+ [
+ 'getIndexPrefix',
+ 'getEntityType',
+ ]
+ )->getMock();
$this->indexBuilder = $this->getMockBuilder(\Magento\Elasticsearch\Model\Adapter\Index\BuilderInterface::class)
->disableOriginalConstructor()
->getMock();
@@ -104,44 +107,52 @@ protected function setUp()
->disableOriginalConstructor()
->getMock();
$elasticsearchClientMock = $this->getMockBuilder(\Elasticsearch\Client::class)
- ->setMethods([
- 'indices',
- 'ping',
- 'bulk',
- 'search',
- ])
+ ->setMethods(
+ [
+ 'indices',
+ 'ping',
+ 'bulk',
+ 'search',
+ ]
+ )
->disableOriginalConstructor()
->getMock();
$indicesMock = $this->getMockBuilder(\Elasticsearch\Namespaces\IndicesNamespace::class)
- ->setMethods([
- 'exists',
- 'getSettings',
- 'create',
- 'putMapping',
- 'deleteMapping',
- 'existsAlias',
- 'updateAliases',
- 'stats'
- ])
+ ->setMethods(
+ [
+ 'exists',
+ 'getSettings',
+ 'create',
+ 'putMapping',
+ 'deleteMapping',
+ 'existsAlias',
+ 'updateAliases',
+ 'stats'
+ ]
+ )
->disableOriginalConstructor()
->getMock();
$elasticsearchClientMock->expects($this->any())
->method('indices')
->willReturn($indicesMock);
$this->client = $this->getMockBuilder(\Magento\Elasticsearch\Model\Client\Elasticsearch::class)
- ->setConstructorArgs([
- 'options' => $this->getClientOptions(),
- 'elasticsearchClient' => $elasticsearchClientMock
- ])
+ ->setConstructorArgs(
+ [
+ 'options' => $this->getClientOptions(),
+ 'elasticsearchClient' => $elasticsearchClientMock
+ ]
+ )
->getMock();
$this->connectionManager->expects($this->any())
->method('getConnection')
->willReturn($this->client);
$this->fieldMapper->expects($this->any())
->method('getAllAttributesTypes')
- ->willReturn([
- 'name' => 'string',
- ]);
+ ->willReturn(
+ [
+ 'name' => 'string',
+ ]
+ );
$this->clientConfig->expects($this->any())
->method('getIndexPrefix')
->willReturn('indexName');
@@ -151,12 +162,14 @@ protected function setUp()
$this->indexNameResolver = $this->getMockBuilder(
\Magento\Elasticsearch\Model\Adapter\Index\IndexNameResolver::class
)
- ->setMethods([
- 'getIndexName',
- 'getIndexNamespace',
- 'getIndexFromAlias',
- 'getIndexNameForAlias',
- ])
+ ->setMethods(
+ [
+ 'getIndexName',
+ 'getIndexNamespace',
+ 'getIndexFromAlias',
+ 'getIndexNameForAlias',
+ ]
+ )
->disableOriginalConstructor()
->getMock();
$this->batchDocumentDataMapper = $this->getMockBuilder(
@@ -216,9 +229,11 @@ public function testPrepareDocsPerStore()
{
$this->batchDocumentDataMapper->expects($this->once())
->method('map')
- ->willReturn([
- 'name' => 'Product Name',
- ]);
+ ->willReturn(
+ [
+ 'name' => 'Product Name',
+ ]
+ );
$this->assertInternalType(
'array',
$this->model->prepareDocsPerStore(
@@ -283,10 +298,6 @@ public function testCleanIndex()
->with(1, 'product', [])
->willReturn('indexName_product_1_v');
- $this->client->expects($this->once())
- ->method('isEmptyIndex')
- ->with('indexName_product_1_v')
- ->willReturn(false);
$this->client->expects($this->atLeastOnce())
->method('indexExists')
->willReturn(true);
@@ -299,26 +310,6 @@ public function testCleanIndex()
);
}
- /**
- * Test cleanIndex() method isEmptyIndex is true
- */
- public function testCleanIndexTrue()
- {
- $this->indexNameResolver->expects($this->any())
- ->method('getIndexName')
- ->willReturn('indexName_product_1_v');
-
- $this->client->expects($this->once())
- ->method('isEmptyIndex')
- ->with('indexName_product_1_v')
- ->willReturn(true);
-
- $this->assertSame(
- $this->model,
- $this->model->cleanIndex(1, 'product')
- );
- }
-
/**
* Test deleteDocs() method
*/
@@ -376,9 +367,11 @@ public function testConnectException()
{
$connectionManager = $this->getMockBuilder(\Magento\Elasticsearch\SearchAdapter\ConnectionManager::class)
->disableOriginalConstructor()
- ->setMethods([
- 'getConnection',
- ])
+ ->setMethods(
+ [
+ 'getConnection',
+ ]
+ )
->getMock();
$connectionManager->expects($this->any())
diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php
index de85b8b6602b8..f90c13c9bfb65 100644
--- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php
+++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/StaticFieldTest.php
@@ -139,6 +139,7 @@ public function testGetAllAttributesTypes(
$isComplexType,
$complexType,
$isSortable,
+ $isTextType,
$fieldName,
$compositeFieldName,
$sortFieldName,
@@ -153,29 +154,33 @@ public function testGetAllAttributesTypes(
$this->indexTypeConverter->expects($this->any())
->method('convert')
->with($this->anything())
- ->will($this->returnCallback(
- function ($type) {
- if ($type === 'no_index') {
- return 'no';
- } elseif ($type === 'no_analyze') {
- return 'not_analyzed';
+ ->will(
+ $this->returnCallback(
+ function ($type) {
+ if ($type === 'no_index') {
+ return 'no';
+ } elseif ($type === 'no_analyze') {
+ return 'not_analyzed';
+ }
}
- }
- ));
+ )
+ );
$this->fieldNameResolver->expects($this->any())
->method('getFieldName')
->with($this->anything())
- ->will($this->returnCallback(
- function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortFieldName) {
- if (empty($context)) {
- return $fieldName;
- } elseif ($context['type'] === 'sort') {
- return $sortFieldName;
- } elseif ($context['type'] === 'text') {
- return $compositeFieldName;
+ ->will(
+ $this->returnCallback(
+ function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortFieldName) {
+ if (empty($context)) {
+ return $fieldName;
+ } elseif ($context['type'] === 'sort') {
+ return $sortFieldName;
+ } elseif ($context['type'] === 'text') {
+ return $compositeFieldName;
+ }
}
- }
- ));
+ )
+ );
$productAttributeMock = $this->getMockBuilder(AbstractAttribute::class)
->setMethods(['getAttributeCode'])
@@ -189,7 +194,7 @@ function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortF
$attributeMock = $this->getMockBuilder(AttributeAdapter::class)
->disableOriginalConstructor()
- ->setMethods(['isComplexType', 'getAttributeCode', 'isSortable'])
+ ->setMethods(['isComplexType', 'getAttributeCode', 'isSortable', 'isTextType'])
->getMock();
$attributeMock->expects($this->any())
->method('isComplexType')
@@ -197,6 +202,9 @@ function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortF
$attributeMock->expects($this->any())
->method('isSortable')
->willReturn($isSortable);
+ $attributeMock->expects($this->any())
+ ->method('isTextType')
+ ->willReturn($isTextType);
$attributeMock->expects($this->any())
->method('getAttributeCode')
->willReturn($attributeCode);
@@ -207,22 +215,24 @@ function ($attributeMock, $context) use ($fieldName, $compositeFieldName, $sortF
$this->fieldTypeConverter->expects($this->any())
->method('convert')
->with($this->anything())
- ->will($this->returnCallback(
- function ($type) use ($complexType) {
- static $callCount = [];
- $callCount[$type] = !isset($callCount[$type]) ? 1 : ++$callCount[$type];
+ ->will(
+ $this->returnCallback(
+ function ($type) use ($complexType) {
+ static $callCount = [];
+ $callCount[$type] = !isset($callCount[$type]) ? 1 : ++$callCount[$type];
- if ($type === 'string') {
- return 'string';
- } elseif ($type === 'float') {
- return 'float';
- } elseif ($type === 'keyword') {
- return 'string';
- } else {
- return $complexType;
+ if ($type === 'string') {
+ return 'string';
+ } elseif ($type === 'float') {
+ return 'float';
+ } elseif ($type === 'keyword') {
+ return 'string';
+ } else {
+ return $complexType;
+ }
}
- }
- ));
+ )
+ );
$this->assertEquals(
$expected,
@@ -243,13 +253,19 @@ public function attributeProvider()
true,
'text',
false,
+ true,
'category_ids',
'category_ids_value',
'',
[
'category_ids' => [
'type' => 'select',
- 'index' => true
+ 'index' => true,
+ 'fields' => [
+ 'keyword' => [
+ 'type' => 'string',
+ ]
+ ]
],
'category_ids_value' => [
'type' => 'string'
@@ -267,13 +283,19 @@ public function attributeProvider()
false,
null,
false,
+ true,
'attr_code',
'',
'',
[
'attr_code' => [
'type' => 'text',
- 'index' => 'no'
+ 'index' => 'no',
+ 'fields' => [
+ 'keyword' => [
+ 'type' => 'string',
+ ]
+ ]
],
'store_id' => [
'type' => 'string',
@@ -288,6 +310,7 @@ public function attributeProvider()
false,
null,
false,
+ false,
'attr_code',
'',
'',
@@ -308,6 +331,7 @@ public function attributeProvider()
false,
null,
true,
+ false,
'attr_code',
'',
'sort_attr_code',
diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverTest.php
index fbf63630464f9..b2c0f5e341fc2 100644
--- a/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverTest.php
+++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/Fulltext/Collection/SearchCriteriaResolverTest.php
@@ -106,7 +106,7 @@ public function resolveSortOrderDataProvider()
],
[
['size' => 10, 'orders' => ['test' => 'ASC']],
- ['size' => 10, 'orders' => ['test' => 'ASC']],
+ ['size' => null, 'orders' => ['test' => 'ASC']],
],
];
}
diff --git a/app/code/Magento/GiftMessage/etc/db_schema.xml b/app/code/Magento/GiftMessage/etc/db_schema.xml
index 4ae98799df0c2..5c1e7fb17bc5d 100644
--- a/app/code/Magento/GiftMessage/etc/db_schema.xml
+++ b/app/code/Magento/GiftMessage/etc/db_schema.xml
@@ -9,9 +9,9 @@
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
+ comment="GiftMessage ID"/>
+ default="0" comment="Customer ID"/>
@@ -21,27 +21,27 @@
+ comment="Gift Message ID"/>
+ comment="Gift Message ID"/>
+ comment="Gift Message ID"/>
+ comment="Gift Message ID"/>
+ comment="Gift Message ID"/>
+ comment="Gift Message ID"/>
diff --git a/app/code/Magento/GoogleOptimizer/etc/db_schema.xml b/app/code/Magento/GoogleOptimizer/etc/db_schema.xml
index 76c377544cfb3..ed3dfcecb90f3 100644
--- a/app/code/Magento/GoogleOptimizer/etc/db_schema.xml
+++ b/app/code/Magento/GoogleOptimizer/etc/db_schema.xml
@@ -9,12 +9,12 @@
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
+ comment="Google experiment code ID"/>
+ comment="Optimized entity ID product ID or catalog ID"/>
+ comment="Store ID"/>
diff --git a/app/code/Magento/GraphQl/etc/schema.graphqls b/app/code/Magento/GraphQl/etc/schema.graphqls
index eb6a88a4d487d..77bba5ea3a9d4 100644
--- a/app/code/Magento/GraphQl/etc/schema.graphqls
+++ b/app/code/Magento/GraphQl/etc/schema.graphqls
@@ -65,6 +65,20 @@ input FilterTypeInput @doc(description: "FilterTypeInput specifies which action
nin: [String] @doc(description: "Not in. The value can contain a set of comma-separated values")
}
+input FilterEqualTypeInput @doc(description: "Defines a filter that matches the input exactly.") {
+ in: [String] @doc(description: "An array of values to filter on")
+ eq: String @doc(description: "A string to filter on")
+}
+
+input FilterRangeTypeInput @doc(description: "Defines a filter that matches a range of values, such as prices or dates.") {
+ from: String @doc(description: "The beginning of the range")
+ to: String @doc(description: "The end of the range")
+}
+
+input FilterMatchTypeInput @doc(description: "Defines a filter that performs a fuzzy search.") {
+ match: String @doc(description: "One or more words to filter on")
+}
+
type SearchResultPageInfo @doc(description: "SearchResultPageInfo provides navigation for the query response") {
page_size: Int @doc(description: "Specifies the maximum number of items to return")
current_page: Int @doc(description: "Specifies which page of results to return")
diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAssociateGroupedProductToWebsitesTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAssociateGroupedProductToWebsitesTest.xml
new file mode 100644
index 0000000000000..3827666252478
--- /dev/null
+++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAssociateGroupedProductToWebsitesTest.xml
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ImportExport/Api/Data/ExtendedExportInfoInterface.php b/app/code/Magento/ImportExport/Api/Data/ExtendedExportInfoInterface.php
new file mode 100644
index 0000000000000..aa14d562d9cf7
--- /dev/null
+++ b/app/code/Magento/ImportExport/Api/Data/ExtendedExportInfoInterface.php
@@ -0,0 +1,29 @@
+getRequest()->getParams();
+ if (!array_key_exists('skip_attr', $params)) {
+ $params['skip_attr'] = [];
+ }
+
/** @var ExportInfoFactory $dataObject */
$dataObject = $this->exportInfoFactory->create(
$params['file_format'],
$params['entity'],
- $params['export_filter']
+ $params['export_filter'],
+ $params['skip_attr']
);
$this->messagePublisher->publish('import_export.export', $dataObject);
diff --git a/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfo.php b/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfo.php
index 6dffc1827cfd0..a5d5d63e4f8da 100644
--- a/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfo.php
+++ b/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfo.php
@@ -7,12 +7,12 @@
namespace Magento\ImportExport\Model\Export\Entity;
-use \Magento\ImportExport\Api\Data\ExportInfoInterface;
+use Magento\ImportExport\Api\Data\ExtendedExportInfoInterface;
/**
* Class ExportInfo implementation for ExportInfoInterface.
*/
-class ExportInfo implements ExportInfoInterface
+class ExportInfo implements ExtendedExportInfoInterface
{
/**
* @var string
@@ -39,6 +39,11 @@ class ExportInfo implements ExportInfoInterface
*/
private $exportFilter;
+ /**
+ * @var mixed
+ */
+ private $skipAttr;
+
/**
* @inheritdoc
*/
@@ -118,4 +123,20 @@ public function setExportFilter($exportFilter)
{
$this->exportFilter = $exportFilter;
}
+
+ /**
+ * @inheritdoc
+ */
+ public function getSkipAttr()
+ {
+ return $this->skipAttr;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setSkipAttr($skipAttr)
+ {
+ $this->skipAttr = $skipAttr;
+ }
}
diff --git a/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfoFactory.php b/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfoFactory.php
index e3cbd162aa5af..32c989acb661c 100644
--- a/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfoFactory.php
+++ b/app/code/Magento/ImportExport/Model/Export/Entity/ExportInfoFactory.php
@@ -84,17 +84,25 @@ public function __construct(
* @param string $fileFormat
* @param string $entity
* @param string $exportFilter
+ * @param array $skipAttr
* @return ExportInfoInterface
* @throws \Magento\Framework\Exception\LocalizedException
*/
- public function create($fileFormat, $entity, $exportFilter)
+ public function create($fileFormat, $entity, $exportFilter, $skipAttr)
{
$writer = $this->getWriter($fileFormat);
- $entityAdapter = $this->getEntityAdapter($entity, $fileFormat, $exportFilter, $writer->getContentType());
+ $entityAdapter = $this->getEntityAdapter(
+ $entity,
+ $fileFormat,
+ $exportFilter,
+ $skipAttr,
+ $writer->getContentType()
+ );
$fileName = $this->generateFileName($entity, $entityAdapter, $writer->getFileExtension());
/** @var ExportInfoInterface $exportInfo */
$exportInfo = $this->objectManager->create(ExportInfoInterface::class);
$exportInfo->setExportFilter($this->serializer->serialize($exportFilter));
+ $exportInfo->setSkipAttr($skipAttr);
$exportInfo->setFileName($fileName);
$exportInfo->setEntity($entity);
$exportInfo->setFileFormat($fileFormat);
@@ -130,11 +138,12 @@ private function generateFileName($entity, $entityAdapter, $fileExtensions)
* @param string $entity
* @param string $fileFormat
* @param string $exportFilter
+ * @param array $skipAttr
* @param string $contentType
* @return \Magento\ImportExport\Model\Export\AbstractEntity|AbstractEntity
* @throws \Magento\Framework\Exception\LocalizedException
*/
- private function getEntityAdapter($entity, $fileFormat, $exportFilter, $contentType)
+ private function getEntityAdapter($entity, $fileFormat, $exportFilter, $skipAttr, $contentType)
{
$entities = $this->exportConfig->getEntities();
if (isset($entities[$entity])) {
@@ -166,12 +175,15 @@ private function getEntityAdapter($entity, $fileFormat, $exportFilter, $contentT
} else {
throw new \Magento\Framework\Exception\LocalizedException(__('Please enter a correct entity.'));
}
- $entityAdapter->setParameters([
- 'fileFormat' => $fileFormat,
- 'entity' => $entity,
- 'exportFilter' => $exportFilter,
- 'contentType' => $contentType,
- ]);
+ $entityAdapter->setParameters(
+ [
+ 'fileFormat' => $fileFormat,
+ 'entity' => $entity,
+ 'exportFilter' => $exportFilter,
+ 'skipAttr' => $skipAttr,
+ 'contentType' => $contentType,
+ ]
+ );
return $entityAdapter;
}
diff --git a/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml b/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml
index 2ac790b953ec1..9063916e9f502 100644
--- a/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml
+++ b/app/code/Magento/ImportExport/Test/Mftf/ActionGroup/AdminImportProductsActionGroup.xml
@@ -14,21 +14,50 @@
+
+
-
+
+
+
-
+
-
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ImportExport/Test/Mftf/Page/AdminImportIndexPage.xml b/app/code/Magento/ImportExport/Test/Mftf/Page/AdminImportIndexPage.xml
index 87807eb9b0e85..6262931179599 100644
--- a/app/code/Magento/ImportExport/Test/Mftf/Page/AdminImportIndexPage.xml
+++ b/app/code/Magento/ImportExport/Test/Mftf/Page/AdminImportIndexPage.xml
@@ -11,5 +11,6 @@
+
diff --git a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.xml b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.xml
index 2ce6b1e35777f..ba1deeebbd89a 100644
--- a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.xml
+++ b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportMainSection.xml
@@ -13,5 +13,9 @@
+
+
+
+
diff --git a/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportValidationMessagesSection.xml b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportValidationMessagesSection.xml
new file mode 100644
index 0000000000000..370d9546fa2f7
--- /dev/null
+++ b/app/code/Magento/ImportExport/Test/Mftf/Section/AdminImportValidationMessagesSection.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckDoubleImportOfProductsTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckDoubleImportOfProductsTest.xml
index 0f2dde99b9016..909c6101fe53e 100644
--- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckDoubleImportOfProductsTest.xml
+++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckDoubleImportOfProductsTest.xml
@@ -60,14 +60,14 @@
-
+
-
+
diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml
index ceb4e93e4e9aa..796732d572290 100644
--- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml
+++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml
@@ -72,7 +72,7 @@
-
+
@@ -109,7 +109,7 @@
-
+
diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithErrorEntriesTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithErrorEntriesTest.xml
new file mode 100644
index 0000000000000..94840a4ea6142
--- /dev/null
+++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithErrorEntriesTest.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml
index d63a5546716b1..dc4ede1978de3 100644
--- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml
+++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml
@@ -39,7 +39,7 @@
-
+
diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductImportCSVFileCorrectDifferentFilesTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductImportCSVFileCorrectDifferentFilesTest.xml
new file mode 100644
index 0000000000000..eb84929ec8d93
--- /dev/null
+++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductImportCSVFileCorrectDifferentFilesTest.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ImportExport/etc/communication.xml b/app/code/Magento/ImportExport/etc/communication.xml
index 7794b3e5ab248..3f87eef1ddbd4 100644
--- a/app/code/Magento/ImportExport/etc/communication.xml
+++ b/app/code/Magento/ImportExport/etc/communication.xml
@@ -6,7 +6,7 @@
*/
-->
-
+
diff --git a/app/code/Magento/ImportExport/etc/db_schema.xml b/app/code/Magento/ImportExport/etc/db_schema.xml
index df45131848519..404999cb9e07a 100644
--- a/app/code/Magento/ImportExport/etc/db_schema.xml
+++ b/app/code/Magento/ImportExport/etc/db_schema.xml
@@ -8,7 +8,7 @@
+ comment="History record ID"/>
+
diff --git a/app/code/Magento/Indexer/etc/db_schema.xml b/app/code/Magento/Indexer/etc/db_schema.xml
index d7cb006a2cf45..c9c8e665b3755 100644
--- a/app/code/Magento/Indexer/etc/db_schema.xml
+++ b/app/code/Magento/Indexer/etc/db_schema.xml
@@ -9,8 +9,8 @@
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
-
+ comment="Indexer State ID"/>
+
@@ -24,13 +24,13 @@
-
+ comment="View State ID"/>
+
+ comment="View Version ID"/>
diff --git a/app/code/Magento/Integration/etc/db_schema.xml b/app/code/Magento/Integration/etc/db_schema.xml
index cbf43d79b2cf6..de0cec2e4e20d 100644
--- a/app/code/Magento/Integration/etc/db_schema.xml
+++ b/app/code/Magento/Integration/etc/db_schema.xml
@@ -129,7 +129,7 @@
+ comment="Log ID"/>
Environment Update Time
+ validate-zero-or-greater validate-digits
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml
index 271f6e707cd69..3a58ead3b6dfa 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml
@@ -57,6 +57,7 @@
+
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml
index f425a44130ecb..c9f1856249762 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml
@@ -57,6 +57,7 @@
+
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml
index 8d5a58acc7e18..d52ddb11212aa 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml
@@ -37,6 +37,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -53,13 +63,5 @@
-
-
-
-
-
-
-
-
diff --git a/app/code/Magento/NewRelicReporting/etc/db_schema.xml b/app/code/Magento/NewRelicReporting/etc/db_schema.xml
index c6e61b88f4b1b..e18d7c8077bb9 100644
--- a/app/code/Magento/NewRelicReporting/etc/db_schema.xml
+++ b/app/code/Magento/NewRelicReporting/etc/db_schema.xml
@@ -22,7 +22,7 @@
+ comment="Module ID"/>
diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php
index f9e9d57bf4b40..8ca489d89c1df 100644
--- a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php
+++ b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php
@@ -130,7 +130,7 @@ public function loadByEmail($subscriberEmail)
*/
public function loadByCustomerData(\Magento\Customer\Api\Data\CustomerInterface $customer)
{
- $storeIds = $this->storeManager->getWebsite($customer->getWebsiteId())->getStoreIds();
+ $storeIds = $this->storeManager->getWebsite()->getStoreIds();
if ($customer->getId()) {
$select = $this->connection
diff --git a/app/code/Magento/Newsletter/Model/Subscriber.php b/app/code/Magento/Newsletter/Model/Subscriber.php
index 85d512afaf262..c5eee5e3cf771 100644
--- a/app/code/Magento/Newsletter/Model/Subscriber.php
+++ b/app/code/Magento/Newsletter/Model/Subscriber.php
@@ -353,11 +353,7 @@ public function isStatusChanged()
*/
public function isSubscribed()
{
- if ($this->getId() && $this->getStatus() == self::STATUS_SUBSCRIBED) {
- return true;
- }
-
- return false;
+ return $this->getId() && (int)$this->getStatus() === self::STATUS_SUBSCRIBED;
}
/**
diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml
index 510f3e16e8d8e..0371c0265d149 100644
--- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml
+++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml
@@ -16,9 +16,6 @@
-
-
-
diff --git a/app/code/Magento/Newsletter/etc/db_schema.xml b/app/code/Magento/Newsletter/etc/db_schema.xml
index 5cb572f41b6be..257416d0bc465 100644
--- a/app/code/Magento/Newsletter/etc/db_schema.xml
+++ b/app/code/Magento/Newsletter/etc/db_schema.xml
@@ -9,13 +9,13 @@
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
+ comment="Subscriber ID"/>
+ default="0" comment="Store ID"/>
+ default="0" comment="Customer ID"/>
@@ -69,7 +69,7 @@
+ comment="Queue ID"/>
+ comment="Queue Link ID"/>
+ default="0" comment="Queue ID"/>
+ default="0" comment="Subscriber ID"/>
@@ -123,9 +123,9 @@
+ default="0" comment="Queue ID"/>
+ default="0" comment="Store ID"/>
@@ -142,11 +142,11 @@
+ comment="Problem ID"/>
+ comment="Subscriber ID"/>
+ default="0" comment="Queue ID"/>
diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml
index 5175080add914..20ff63a60a263 100644
--- a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml
+++ b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml
@@ -17,6 +17,7 @@
+ default="0" comment="Website ID"/>
+ default="0" comment="Destination Region ID"/>
diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml
new file mode 100644
index 0000000000000..375211e5f2f51
--- /dev/null
+++ b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/PageCache/etc/adminhtml/system.xml b/app/code/Magento/PageCache/etc/adminhtml/system.xml
index 8013ad40ef5aa..234e3e48a95d8 100644
--- a/app/code/Magento/PageCache/etc/adminhtml/system.xml
+++ b/app/code/Magento/PageCache/etc/adminhtml/system.xml
@@ -74,6 +74,7 @@
TTL for public content
+ validate-zero-or-greater validate-digits
Public content cache lifetime in seconds. If field is empty default value 86400 will be saved.
Magento\PageCache\Model\System\Config\Backend\Ttl
diff --git a/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php b/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php
index 7ad8fe658ec16..895cdb8d4c600 100644
--- a/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php
+++ b/app/code/Magento/Paypal/Controller/Express/AbstractExpress.php
@@ -9,6 +9,8 @@
use Magento\Framework\App\Action\Action as AppAction;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\Action\HttpPostActionInterface;
+use Magento\Framework\App\ObjectManager;
+use Magento\Quote\Api\CartRepositoryInterface;
use Magento\Quote\Api\Data\CartInterface;
/**
@@ -98,6 +100,11 @@ abstract class AbstractExpress extends AppAction implements
*/
protected $_customerUrl;
+ /**
+ * @var CartRepositoryInterface
+ */
+ private $quoteRepository;
+
/**
* @param \Magento\Framework\App\Action\Context $context
* @param \Magento\Customer\Model\Session $customerSession
@@ -107,6 +114,7 @@ abstract class AbstractExpress extends AppAction implements
* @param \Magento\Framework\Session\Generic $paypalSession
* @param \Magento\Framework\Url\Helper\Data $urlHelper
* @param \Magento\Customer\Model\Url $customerUrl
+ * @param CartRepositoryInterface $quoteRepository
*/
public function __construct(
\Magento\Framework\App\Action\Context $context,
@@ -116,7 +124,8 @@ public function __construct(
\Magento\Paypal\Model\Express\Checkout\Factory $checkoutFactory,
\Magento\Framework\Session\Generic $paypalSession,
\Magento\Framework\Url\Helper\Data $urlHelper,
- \Magento\Customer\Model\Url $customerUrl
+ \Magento\Customer\Model\Url $customerUrl,
+ CartRepositoryInterface $quoteRepository = null
) {
$this->_customerSession = $customerSession;
$this->_checkoutSession = $checkoutSession;
@@ -128,6 +137,7 @@ public function __construct(
parent::__construct($context);
$parameters = ['params' => [$this->_configMethod]];
$this->_config = $this->_objectManager->create($this->_configType, $parameters);
+ $this->quoteRepository = $quoteRepository ?: ObjectManager::getInstance()->get(CartRepositoryInterface::class);
}
/**
@@ -233,7 +243,12 @@ protected function _getCheckoutSession()
protected function _getQuote()
{
if (!$this->_quote) {
- $this->_quote = $this->_getCheckoutSession()->getQuote();
+ if ($this->_getSession()->getQuoteId()) {
+ $this->_quote = $this->quoteRepository->get($this->_getSession()->getQuoteId());
+ $this->_getCheckoutSession()->replaceQuote($this->_quote);
+ } else {
+ $this->_quote = $this->_getCheckoutSession()->getQuote();
+ }
}
return $this->_quote;
}
@@ -243,7 +258,7 @@ protected function _getQuote()
*/
public function getCustomerBeforeAuthUrl()
{
- return;
+ return null;
}
/**
diff --git a/app/code/Magento/Paypal/Controller/Express/OnAuthorization.php b/app/code/Magento/Paypal/Controller/Express/OnAuthorization.php
index 62f4c4c4c457a..0d7ec3fc6f32d 100644
--- a/app/code/Magento/Paypal/Controller/Express/OnAuthorization.php
+++ b/app/code/Magento/Paypal/Controller/Express/OnAuthorization.php
@@ -155,6 +155,7 @@ public function execute(): ResultInterface
} else {
$responseContent['redirectUrl'] = $this->urlBuilder->getUrl('paypal/express/review');
$this->_checkoutSession->setQuoteId($quote->getId());
+ $this->_getSession()->setQuoteId($quote->getId());
}
} catch (ApiProcessableException $e) {
$responseContent['success'] = false;
diff --git a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Fieldset/GroupTest.php b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Fieldset/GroupTest.php
deleted file mode 100644
index e4de60cafb8ad..0000000000000
--- a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Fieldset/GroupTest.php
+++ /dev/null
@@ -1,110 +0,0 @@
-_group = $this->createMock(\Magento\Config\Model\Config\Structure\Element\Group::class);
- $this->_element = $this->getMockForAbstractClass(
- \Magento\Framework\Data\Form\Element\AbstractElement::class,
- [],
- '',
- false,
- true,
- true,
- ['getHtmlId', 'getElementHtml', 'getName', 'getElements', 'getId']
- );
- $this->_element->expects($this->any())
- ->method('getHtmlId')
- ->will($this->returnValue('html id'));
- $this->_element->expects($this->any())
- ->method('getElementHtml')
- ->will($this->returnValue('element html'));
- $this->_element->expects($this->any())
- ->method('getName')
- ->will($this->returnValue('name'));
- $this->_element->expects($this->any())
- ->method('getElements')
- ->will($this->returnValue([]));
- $this->_element->expects($this->any())
- ->method('getId')
- ->will($this->returnValue('id'));
- $this->_user = $this->createMock(\Magento\User\Model\User::class);
- $this->_authSession = $this->createMock(\Magento\Backend\Model\Auth\Session::class);
- $this->_authSession->expects($this->any())
- ->method('__call')
- ->with('getUser')
- ->will($this->returnValue($this->_user));
- $this->_model = $helper->getObject(
- \Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Group::class,
- ['authSession' => $this->_authSession]
- );
- $this->_model->setGroup($this->_group);
- }
-
- /**
- * @param mixed $expanded
- * @param int $expected
- * @dataProvider isCollapseStateDataProvider
- */
- public function testIsCollapseState($expanded, $expected)
- {
- $this->_user->setExtra(['configState' => []]);
- $this->_element->setGroup(isset($expanded) ? ['expanded' => $expanded] : []);
- $html = $this->_model->render($this->_element);
- $this->assertContains(
- ' ',
- $html
- );
- }
-
- /**
- * @return array
- */
- public function isCollapseStateDataProvider()
- {
- return [
- [null, 0],
- [false, 0],
- ['', 0],
- [1, 1],
- ['1', 1],
- ];
- }
-}
diff --git a/app/code/Magento/Paypal/etc/db_schema.xml b/app/code/Magento/Paypal/etc/db_schema.xml
index 4dc283fcb48ce..3300f3754e656 100644
--- a/app/code/Magento/Paypal/etc/db_schema.xml
+++ b/app/code/Magento/Paypal/etc/db_schema.xml
@@ -9,17 +9,17 @@
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
+ comment="Agreement ID"/>
+ comment="Customer ID"/>
-
+
+ comment="Store ID"/>
@@ -40,9 +40,9 @@
+ comment="Agreement ID"/>
+ comment="Order ID"/>
@@ -59,9 +59,9 @@
+ comment="Report ID"/>
-
+
@@ -75,15 +75,15 @@
+ comment="Row ID"/>
-
-
+ comment="Report ID"/>
+
+
+ comment="Paypal Reference ID"/>
+ comment="Paypal Reference ID Type"/>
-
+
@@ -117,9 +117,9 @@
+ comment="Cert ID"/>
+ default="0" comment="Website ID"/>
@@ -135,7 +135,7 @@
comment="PayPal Payflow Link Payment Transaction">
-
+
@@ -146,11 +146,11 @@
-
+
+ comment="Paypal Correlation ID"/>
= $block->escapeHtml(__('Place Order')) ?>
-
- = $block->escapeHtml(__('Place Order')) ?>
-
diff --git a/app/code/Magento/Paypal/view/frontend/web/js/order-review.js b/app/code/Magento/Paypal/view/frontend/web/js/order-review.js
index 1deee1bd76593..e3db1010693ee 100644
--- a/app/code/Magento/Paypal/view/frontend/web/js/order-review.js
+++ b/app/code/Magento/Paypal/view/frontend/web/js/order-review.js
@@ -26,7 +26,6 @@ define([
agreementSelector: 'div.checkout-agreements input',
isAjax: false,
updateShippingMethodSubmitSelector: '#update-shipping-method-submit',
- reviewSubmitSelector: '#review-submit',
shippingMethodUpdateUrl: null,
updateOrderSubmitUrl: null,
canEditShippingMethod: false
@@ -57,8 +56,7 @@ define([
this.options.updateContainerSelector
)
).find(this.options.updateOrderSelector).on('click', $.proxy(this._updateOrderHandler, this)).end()
- .find(this.options.updateShippingMethodSubmitSelector).hide().end()
- .find(this.options.reviewSubmitSelector).hide();
+ .find(this.options.updateShippingMethodSubmitSelector).hide().end();
this._shippingTobilling();
if ($(this.options.shippingSubmitFormSelector).length && this.options.canEditShippingMethod) {
diff --git a/app/code/Magento/PaypalGraphQl/etc/schema.graphqls b/app/code/Magento/PaypalGraphQl/etc/schema.graphqls
index 2bacfb18054df..78335e089cdc6 100644
--- a/app/code/Magento/PaypalGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/PaypalGraphQl/etc/schema.graphqls
@@ -8,7 +8,7 @@ type Query {
type Mutation {
createPaypalExpressToken(input: PaypalExpressTokenInput!): PaypalExpressToken @resolver(class: "\\Magento\\PaypalGraphQl\\Model\\Resolver\\PaypalExpressToken") @doc(description:"Initiates an Express Checkout transaction and receives a token. Use this mutation for Express Checkout and Payments Standard payment methods.")
- createPayflowProToken(input: PayflowProTokenInput!): PayflowProToken @resolver(class: "\\Magento\\PaypalGraphQl\\Model\\Resolver\\PayflowProToken") @doc(description: "Initiates a transaction and receives a token. Use this mutation for Payflow Pro and Payments Pro payment methods")
+ createPayflowProToken(input: PayflowProTokenInput!): CreatePayflowProTokenOutput @resolver(class: "\\Magento\\PaypalGraphQl\\Model\\Resolver\\PayflowProToken") @doc(description: "Initiates a transaction and receives a token. Use this mutation for Payflow Pro and Payments Pro payment methods")
handlePayflowProResponse(input: PayflowProResponseInput!): PayflowProResponseOutput @resolver(class: "\\Magento\\PaypalGraphQl\\Model\\Resolver\\PayflowProResponse") @doc(description: "Handles payment response and saves payment in Quote. Use this mutations for Payflow Pro and Payments Pro payment methods.")
}
@@ -112,7 +112,15 @@ input PayflowProUrlInput @doc(description:"A set of relative URLs that PayPal wi
error_url: String! @doc(description:"The relative URL of the transaction error page that PayPal will redirect to upon payment error. If the full URL to this page is https://www.example.com/paypal/action/error.html, the relative URL is paypal/action/error.html.")
}
-type PayflowProToken @doc(description: "Contains the secure information used to authorize transaction. Applies to Payflow Pro and Payments Pro payment methods.") {
+type PayflowProToken @deprecated(reason: "Use CreatePayflowProTokenOutput instead.") @doc(description: "Contains the secure information used to authorize transaction. Applies to Payflow Pro and Payments Pro payment methods.") {
+ secure_token: String!
+ secure_token_id: String!
+ response_message: String!
+ result: Int!
+ result_code: Int!
+}
+
+type CreatePayflowProTokenOutput @doc(description: "Contains the secure information used to authorize transaction. Applies to Payflow Pro and Payments Pro payment methods.") {
secure_token: String!
secure_token_id: String!
response_message: String!
diff --git a/app/code/Magento/Persistent/etc/db_schema.xml b/app/code/Magento/Persistent/etc/db_schema.xml
index 5021d240417b2..e14dedba0ed56 100644
--- a/app/code/Magento/Persistent/etc/db_schema.xml
+++ b/app/code/Magento/Persistent/etc/db_schema.xml
@@ -9,10 +9,10 @@
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
+ comment="Session ID"/>
+ comment="Customer ID"/>
diff --git a/app/code/Magento/Persistent/etc/frontend/sections.xml b/app/code/Magento/Persistent/etc/frontend/sections.xml
index 16b44c502fc47..7466fbe990b02 100644
--- a/app/code/Magento/Persistent/etc/frontend/sections.xml
+++ b/app/code/Magento/Persistent/etc/frontend/sections.xml
@@ -10,4 +10,7 @@
+
+
+
diff --git a/app/code/Magento/Persistent/view/frontend/templates/remember_me.phtml b/app/code/Magento/Persistent/view/frontend/templates/remember_me.phtml
index 0447b3e1b9cef..ba55895453a09 100644
--- a/app/code/Magento/Persistent/view/frontend/templates/remember_me.phtml
+++ b/app/code/Magento/Persistent/view/frontend/templates/remember_me.phtml
@@ -1,4 +1,5 @@
diff --git a/app/code/Magento/Persistent/view/frontend/web/template/remember-me.html b/app/code/Magento/Persistent/view/frontend/web/template/remember-me.html
index f5dd1ffd57d9d..427e6bdcd63ab 100644
--- a/app/code/Magento/Persistent/view/frontend/web/template/remember-me.html
+++ b/app/code/Magento/Persistent/view/frontend/web/template/remember-me.html
@@ -9,8 +9,7 @@
-
-
-
+
+
diff --git a/app/code/Magento/ProductAlert/etc/db_schema.xml b/app/code/Magento/ProductAlert/etc/db_schema.xml
index cb91560f8daa6..17cc76246e5c6 100644
--- a/app/code/Magento/ProductAlert/etc/db_schema.xml
+++ b/app/code/Magento/ProductAlert/etc/db_schema.xml
@@ -9,17 +9,17 @@
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
+ comment="Product alert price ID"/>
+ default="0" comment="Customer ID"/>
+ default="0" comment="Product ID"/>
+ default="0" comment="Website ID"/>
+ default="0" comment="Store ID"/>
+ comment="Product alert stock ID"/>
+ default="0" comment="Customer ID"/>
+ default="0" comment="Product ID"/>
+ default="0" comment="Website ID"/>
+ default="0" comment="Store ID"/>
baseTotalAmounts;
}
-
+
//@codeCoverageIgnoreEnd
/**
diff --git a/app/code/Magento/Quote/Observer/SubmitObserver.php b/app/code/Magento/Quote/Observer/SubmitObserver.php
index 1213636e5966b..0d6613a691390 100644
--- a/app/code/Magento/Quote/Observer/SubmitObserver.php
+++ b/app/code/Magento/Quote/Observer/SubmitObserver.php
@@ -5,13 +5,21 @@
*/
namespace Magento\Quote\Observer;
-use Magento\Sales\Model\Order\Email\Sender\OrderSender;
+use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
+use Magento\Quote\Model\Quote;
+use Magento\Sales\Model\Order;
+use Magento\Sales\Model\Order\Email\Sender\InvoiceSender;
+use Magento\Sales\Model\Order\Email\Sender\OrderSender;
+use Psr\Log\LoggerInterface;
+/**
+ * Class SubmitObserver
+ */
class SubmitObserver implements ObserverInterface
{
/**
- * @var \Psr\Log\LoggerInterface
+ * @var LoggerInterface
*/
private $logger;
@@ -21,27 +29,37 @@ class SubmitObserver implements ObserverInterface
private $orderSender;
/**
- * @param \Psr\Log\LoggerInterface $logger
+ * @var InvoiceSender
+ */
+ private $invoiceSender;
+
+ /**
+ * @param LoggerInterface $logger
* @param OrderSender $orderSender
+ * @param InvoiceSender $invoiceSender
*/
public function __construct(
- \Psr\Log\LoggerInterface $logger,
- OrderSender $orderSender
+ LoggerInterface $logger,
+ OrderSender $orderSender,
+ InvoiceSender $invoiceSender
) {
$this->logger = $logger;
$this->orderSender = $orderSender;
+ $this->invoiceSender = $invoiceSender;
}
/**
- * @param \Magento\Framework\Event\Observer $observer
+ * Sends emails to customer.
+ *
+ * @param Observer $observer
*
* @return void
*/
- public function execute(\Magento\Framework\Event\Observer $observer)
+ public function execute(Observer $observer)
{
- /** @var \Magento\Quote\Model\Quote $quote */
+ /** @var Quote $quote */
$quote = $observer->getEvent()->getQuote();
- /** @var \Magento\Sales\Model\Order $order */
+ /** @var Order $order */
$order = $observer->getEvent()->getOrder();
/**
@@ -51,6 +69,10 @@ public function execute(\Magento\Framework\Event\Observer $observer)
if (!$redirectUrl && $order->getCanSendNewEmailFlag()) {
try {
$this->orderSender->send($order);
+ $invoice = current($order->getInvoiceCollection()->getItems());
+ if ($invoice) {
+ $this->invoiceSender->send($invoice);
+ }
} catch (\Exception $e) {
$this->logger->critical($e);
}
diff --git a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml
index 4ec608a18a686..f7a4fda4f67d8 100644
--- a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml
+++ b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml
@@ -73,6 +73,9 @@
+
+
@@ -116,6 +119,7 @@
+
@@ -143,6 +147,7 @@
+
diff --git a/app/code/Magento/Quote/Test/Unit/Observer/SubmitObserverTest.php b/app/code/Magento/Quote/Test/Unit/Observer/SubmitObserverTest.php
index c19606a7b8f5d..f06f5466df91f 100644
--- a/app/code/Magento/Quote/Test/Unit/Observer/SubmitObserverTest.php
+++ b/app/code/Magento/Quote/Test/Unit/Observer/SubmitObserverTest.php
@@ -5,75 +5,116 @@
*/
namespace Magento\Quote\Test\Unit\Observer;
+use Magento\Framework\Event;
+use Magento\Framework\Event\Observer;
+use Magento\Quote\Model\Quote;
+use Magento\Quote\Model\Quote\Payment;
+use Magento\Quote\Observer\SubmitObserver;
+use Magento\Sales\Model\Order;
+use Magento\Sales\Model\Order\Email\Sender\InvoiceSender;
+use Magento\Sales\Model\Order\Email\Sender\OrderSender;
+use Magento\Sales\Model\Order\Invoice;
+use Magento\Sales\Model\ResourceModel\Order\Invoice\Collection;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Class SubmitObserverTest
+ */
class SubmitObserverTest extends \PHPUnit\Framework\TestCase
{
/**
- * @var \Magento\Quote\Observer\SubmitObserver
+ * @var SubmitObserver
*/
- protected $model;
+ private $model;
/**
- * @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject
+ * @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
- protected $loggerMock;
+ private $loggerMock;
/**
- * @var \Magento\Sales\Model\Order\Email\Sender\OrderSender|\PHPUnit_Framework_MockObject_MockObject
+ * @var OrderSender|\PHPUnit_Framework_MockObject_MockObject
*/
- protected $orderSenderMock;
+ private $orderSenderMock;
/**
- * @var \Magento\Framework\Event\Observer|\PHPUnit_Framework_MockObject_MockObject
+ * @var InvoiceSender|\PHPUnit_Framework_MockObject_MockObject
*/
- protected $observerMock;
+ private $invoiceSender;
/**
- * @var \Magento\Quote\Model\Quote|\PHPUnit_Framework_MockObject_MockObject
+ * @var Observer|\PHPUnit_Framework_MockObject_MockObject
*/
- protected $quoteMock;
+ private $observerMock;
/**
- * @var \Magento\Sales\Model\Order|\PHPUnit_Framework_MockObject_MockObject
+ * @var Quote|\PHPUnit_Framework_MockObject_MockObject
*/
- protected $orderMock;
+ private $quoteMock;
/**
- * @var \Magento\Quote\Model\Quote\Payment|\PHPUnit_Framework_MockObject_MockObject
+ * @var Order|\PHPUnit_Framework_MockObject_MockObject
*/
- protected $paymentMock;
+ private $orderMock;
+
+ /**
+ * @var Payment|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $paymentMock;
protected function setUp()
{
- $this->loggerMock = $this->createMock(\Psr\Log\LoggerInterface::class);
- $this->quoteMock = $this->createMock(\Magento\Quote\Model\Quote::class);
- $this->orderMock = $this->createMock(\Magento\Sales\Model\Order::class);
- $this->paymentMock = $this->createMock(\Magento\Quote\Model\Quote\Payment::class);
- $this->orderSenderMock =
- $this->createMock(\Magento\Sales\Model\Order\Email\Sender\OrderSender::class);
- $eventMock = $this->getMockBuilder(\Magento\Framework\Event::class)
+ $this->loggerMock = $this->createMock(LoggerInterface::class);
+ $this->quoteMock = $this->createMock(Quote::class);
+ $this->orderMock = $this->createMock(Order::class);
+ $this->paymentMock = $this->createMock(Payment::class);
+ $this->orderSenderMock = $this->createMock(OrderSender::class);
+ $this->invoiceSender = $this->createMock(InvoiceSender::class);
+ $eventMock = $this->getMockBuilder(Event::class)
->disableOriginalConstructor()
->setMethods(['getQuote', 'getOrder'])
->getMock();
- $this->observerMock = $this->createPartialMock(\Magento\Framework\Event\Observer::class, ['getEvent']);
+ $this->observerMock = $this->createPartialMock(Observer::class, ['getEvent']);
$this->observerMock->expects($this->any())->method('getEvent')->willReturn($eventMock);
$eventMock->expects($this->once())->method('getQuote')->willReturn($this->quoteMock);
$eventMock->expects($this->once())->method('getOrder')->willReturn($this->orderMock);
$this->quoteMock->expects($this->once())->method('getPayment')->willReturn($this->paymentMock);
- $this->model = new \Magento\Quote\Observer\SubmitObserver(
+ $this->model = new SubmitObserver(
$this->loggerMock,
- $this->orderSenderMock
+ $this->orderSenderMock,
+ $this->invoiceSender
);
}
+ /**
+ * Tests successful email sending.
+ */
public function testSendEmail()
{
- $this->paymentMock->expects($this->once())->method('getOrderPlaceRedirectUrl')->willReturn('');
- $this->orderMock->expects($this->once())->method('getCanSendNewEmailFlag')->willReturn(true);
- $this->orderSenderMock->expects($this->once())->method('send')->willReturn(true);
- $this->loggerMock->expects($this->never())->method('critical');
+ $this->paymentMock->method('getOrderPlaceRedirectUrl')->willReturn('');
+ $invoice = $this->createMock(Invoice::class);
+ $invoiceCollection = $this->createMock(Collection::class);
+ $invoiceCollection->method('getItems')
+ ->willReturn([$invoice]);
+
+ $this->orderMock->method('getInvoiceCollection')
+ ->willReturn($invoiceCollection);
+ $this->orderMock->method('getCanSendNewEmailFlag')->willReturn(true);
+ $this->orderSenderMock->expects($this->once())
+ ->method('send')->willReturn(true);
+ $this->invoiceSender->expects($this->once())
+ ->method('send')
+ ->with($invoice)
+ ->willReturn(true);
+ $this->loggerMock->expects($this->never())
+ ->method('critical');
+
$this->model->execute($this->observerMock);
}
+ /**
+ * Tests failing email sending.
+ */
public function testFailToSendEmail()
{
$this->paymentMock->expects($this->once())->method('getOrderPlaceRedirectUrl')->willReturn('');
@@ -85,6 +126,9 @@ public function testFailToSendEmail()
$this->model->execute($this->observerMock);
}
+ /**
+ * Tests send email when redirect.
+ */
public function testSendEmailWhenRedirectUrlExists()
{
$this->paymentMock->expects($this->once())->method('getOrderPlaceRedirectUrl')->willReturn(false);
diff --git a/app/code/Magento/Quote/etc/db_schema.xml b/app/code/Magento/Quote/etc/db_schema.xml
index b4c75fc1d21d0..d41591c619cde 100644
--- a/app/code/Magento/Quote/etc/db_schema.xml
+++ b/app/code/Magento/Quote/etc/db_schema.xml
@@ -11,7 +11,7 @@
+ default="0" comment="Store ID"/>
@@ -27,7 +27,7 @@
+ default="0" comment="Orig Order ID"/>
+ comment="Customer ID"/>
+ identity="false" comment="Customer Tax Class ID"/>
+ default="0" comment="Customer Group ID"/>
@@ -63,7 +63,7 @@
identity="false" default="0" comment="Customer Is Guest"/>
-
+
+ comment="Address ID"/>
+ default="0" comment="Quote ID"/>
+ comment="Customer ID"/>
+ comment="Customer Address ID"/>
@@ -129,9 +129,9 @@
+ comment="Region ID"/>
-
+
-
+
-
+
@@ -215,19 +215,19 @@
+ comment="Item ID"/>
+ default="0" comment="Quote ID"/>
+ comment="Product ID"/>
+ comment="Store ID"/>
+ comment="Parent Item ID"/>
@@ -315,13 +315,13 @@
+ comment="Address Item ID"/>
+ comment="Parent Item ID"/>
+ default="0" comment="Quote Address ID"/>
+ default="0" comment="Quote Item ID"/>
+ comment="Product ID"/>
+ comment="Super Product ID"/>
+ comment="Parent Product ID"/>
+ comment="Store ID"/>
@@ -413,11 +413,11 @@
+ comment="Option ID"/>
+ comment="Item ID"/>
+ comment="Product ID"/>
@@ -431,9 +431,9 @@
+ comment="Payment ID"/>
+ default="0" comment="Quote ID"/>
+ comment="Rate ID"/>
+ default="0" comment="Address ID"/>
getItems();
+ $discountPerRule = [];
+ foreach ($items as $item) {
+ $discountBreakdown = $item->getExtensionAttributes()->getDiscounts();
+ if ($discountBreakdown) {
+ foreach ($discountBreakdown as $key => $value) {
+ /* @var \Magento\SalesRule\Model\Rule\Action\Discount\Data $discount */
+ $discount = $value['discount'];
+ $rule = $value['rule'];
+ if (isset($discountPerRule[$key])) {
+ $discountPerRule[$key]['discount'] += $discount->getAmount();
+ } else {
+ $discountPerRule[$key]['discount'] = $discount->getAmount();
+ }
+ $discountPerRule[$key]['rule'] = $rule;
+ }
+ }
+ }
+ return $discountPerRule;
+ }
+}
diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/AppliedCoupons.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/AppliedCoupons.php
new file mode 100644
index 0000000000000..fa232f4d9cd6c
--- /dev/null
+++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/AppliedCoupons.php
@@ -0,0 +1,52 @@
+couponManagement = $couponManagement;
+ }
+
+ /**
+ * @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'));
+ }
+ $cart = $value['model'];
+ $cartId = $cart->getId();
+ $appliedCoupons = [];
+ $appliedCoupon = $this->couponManagement->get($cartId);
+ if ($appliedCoupon) {
+ $appliedCoupons[] = [ 'code' => $appliedCoupon ];
+ }
+ return !empty($appliedCoupons) ? $appliedCoupons : null;
+ }
+}
diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php
index a591c74e78db3..b66327ac1dbba 100644
--- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php
+++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php
@@ -55,7 +55,6 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
// But the totals should be calculated even if no address is set
$this->totals = $this->totalsCollector->collectQuoteTotals($cartItem->getQuote());
}
-
$currencyCode = $cartItem->getQuote()->getQuoteCurrencyCode();
return [
@@ -71,6 +70,41 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
'currency' => $currencyCode,
'value' => $cartItem->getRowTotalInclTax(),
],
+ 'total_item_discount' => [
+ 'currency' => $currencyCode,
+ 'value' => $cartItem->getDiscountAmount(),
+ ],
+ 'discounts' => $this->getDiscountValues($cartItem, $currencyCode)
];
}
+
+ /**
+ * Get Discount Values
+ *
+ * @param Item $cartItem
+ * @param string $currencyCode
+ * @return array
+ */
+ private function getDiscountValues($cartItem, $currencyCode)
+ {
+ $itemDiscounts = $cartItem->getExtensionAttributes()->getDiscounts();
+ if ($itemDiscounts) {
+ $discountValues=[];
+ foreach ($itemDiscounts as $value) {
+ $discount = [];
+ $amount = [];
+ /* @var \Magento\SalesRule\Model\Rule\Action\Discount\Data $discountData */
+ $discountData = $value['discount'];
+ /* @var \Magento\SalesRule\Model\Rule $rule */
+ $rule = $value['rule'];
+ $discount['label'] = $rule->getStoreLabel($cartItem->getQuote()->getStore()) ?: __('Discount');
+ $amount['value'] = $discountData->getAmount();
+ $amount['currency'] = $currencyCode;
+ $discount['amount'] = $amount;
+ $discountValues[] = $discount;
+ }
+ return $discountValues;
+ }
+ return null;
+ }
}
diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/Discounts.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/Discounts.php
new file mode 100644
index 0000000000000..2c3f6d0e69f4a
--- /dev/null
+++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/Discounts.php
@@ -0,0 +1,75 @@
+discountAggregator = $discountAggregator;
+ }
+
+ /**
+ * @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'));
+ }
+ $quote = $value['model'];
+
+ return $this->getDiscountValues($quote);
+ }
+
+ /**
+ * Get Discount Values
+ *
+ * @param Quote $quote
+ * @return array
+ */
+ private function getDiscountValues(Quote $quote)
+ {
+ $discountValues=[];
+ $totalDiscounts = $this->discountAggregator->aggregateDiscountPerRule($quote);
+ if ($totalDiscounts) {
+ foreach ($totalDiscounts as $value) {
+ $discount = [];
+ $amount = [];
+ /* @var \Magento\SalesRule\Model\Rule $rule*/
+ $rule = $value['rule'];
+ $discount['label'] = $rule->getStoreLabel($quote->getStore()) ?: __('Discount');
+ $amount['value'] = $value['discount'];
+ $amount['currency'] = $quote->getQuoteCurrencyCode();
+ $discount['amount'] = $amount;
+ $discountValues[] = $discount;
+ }
+ return $discountValues;
+ }
+ return null;
+ }
+}
diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php
index c6f25dd78823b..429fda816efd3 100644
--- a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php
+++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php
@@ -31,36 +31,37 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
/** @var Address $address */
$address = $value['model'];
$rates = $address->getAllShippingRates();
- $carrierTitle = null;
- $methodTitle = null;
+ $carrierTitle = '';
+ $methodTitle = '';
- if (count($rates) > 0 && !empty($address->getShippingMethod())) {
- list($carrierCode, $methodCode) = explode('_', $address->getShippingMethod(), 2);
+ if (!count($rates) || empty($address->getShippingMethod())) {
+ return null;
+ }
- /** @var Rate $rate */
- foreach ($rates as $rate) {
- if ($rate->getCode() == $address->getShippingMethod()) {
- $carrierTitle = $rate->getCarrierTitle();
- $methodTitle = $rate->getMethodTitle();
- break;
- }
- }
+ list($carrierCode, $methodCode) = explode('_', $address->getShippingMethod(), 2);
- $data = [
- 'carrier_code' => $carrierCode,
- 'method_code' => $methodCode,
- 'carrier_title' => $carrierTitle,
- 'method_title' => $methodTitle,
- 'amount' => [
- 'value' => $address->getShippingAmount(),
- 'currency' => $address->getQuote()->getQuoteCurrencyCode(),
- ],
- /** @deprecated The field should not be used on the storefront */
- 'base_amount' => null,
- ];
- } else {
- $data = null;
+ /** @var Rate $rate */
+ foreach ($rates as $rate) {
+ if ($rate->getCode() == $address->getShippingMethod()) {
+ $carrierTitle = $rate->getCarrierTitle();
+ $methodTitle = $rate->getMethodTitle();
+ break;
+ }
}
+
+ $data = [
+ 'carrier_code' => $carrierCode,
+ 'method_code' => $methodCode,
+ 'carrier_title' => $carrierTitle,
+ 'method_title' => $methodTitle,
+ 'amount' => [
+ 'value' => $address->getShippingAmount(),
+ 'currency' => $address->getQuote()->getQuoteCurrencyCode(),
+ ],
+ /** @deprecated The field should not be used on the storefront */
+ 'base_amount' => null,
+ ];
+
return $data;
}
}
diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls
index 075d2f8147340..2edb3f1e196ab 100644
--- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls
@@ -150,9 +150,10 @@ type CartPrices {
grand_total: Money
subtotal_including_tax: Money
subtotal_excluding_tax: Money
- discount: CartDiscount
+ discount: CartDiscount @deprecated(reason: "Use discounts instead ")
subtotal_with_discount_excluding_tax: Money
applied_taxes: [CartTaxItem]
+ discounts: [Discount] @doc(description:"An array of applied discounts") @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\Discounts")
}
type CartTaxItem {
@@ -191,7 +192,8 @@ type PlaceOrderOutput {
type Cart {
items: [CartItemInterface] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartItems")
- applied_coupon: AppliedCoupon @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\AppliedCoupon")
+ applied_coupon: AppliedCoupon @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\AppliedCoupon") @doc(description:"An array of coupons that have been applied to the cart") @deprecated(reason: "Use applied_coupons instead ")
+ applied_coupons: [AppliedCoupon] @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\AppliedCoupons") @doc(description:"An array of `AppliedCoupon` objects. Each object contains the `code` text attribute, which specifies the coupon code")
email: String @resolver (class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CartEmail")
shipping_addresses: [ShippingCartAddress]! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\ShippingAddresses")
billing_address: BillingCartAddress! @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\BillingAddress")
@@ -242,11 +244,11 @@ type CartAddressCountry {
}
type SelectedShippingMethod {
- carrier_code: String
- method_code: String
- carrier_title: String
- method_title: String
- amount: Money
+ carrier_code: String!
+ method_code: String!
+ carrier_title: String!
+ method_title: String!
+ amount: Money!
base_amount: Money @deprecated(reason: "The field should not be used on the storefront")
}
@@ -321,10 +323,17 @@ interface CartItemInterface @typeResolver(class: "Magento\\QuoteGraphQl\\Model\\
product: ProductInterface!
}
+type Discount @doc(description:"Defines an individual discount. A discount can be applied to the cart as a whole or to an item.") {
+ amount: Money! @doc(description:"The amount of the discount")
+ label: String! @doc(description:"A description of the discount")
+}
+
type CartItemPrices {
price: Money!
row_total: Money!
row_total_including_tax: Money!
+ discounts: [Discount] @doc(description:"An array of discounts to be applied to the cart item")
+ total_item_discount: Money @doc(description:"The total of all discounts applied to the item")
}
type SelectedCustomizableOption {
diff --git a/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php b/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php
index 813c5f28bf4d9..a7f619863af56 100644
--- a/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php
+++ b/app/code/Magento/ReleaseNotification/Test/Unit/Model/Condition/CanViewNotificationTest.php
@@ -12,6 +12,7 @@
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
use Magento\Backend\Model\Auth\Session;
use Magento\Framework\App\CacheInterface;
+use Magento\User\Model\User;
/**
* Class CanViewNotificationTest
@@ -36,6 +37,11 @@ class CanViewNotificationTest extends \PHPUnit\Framework\TestCase
/** @var $cacheStorageMock \PHPUnit_Framework_MockObject_MockObject|CacheInterface */
private $cacheStorageMock;
+ /**
+ * @var User|\PHPUnit_Framework_MockObject_MockObject
+ */
+ private $userMock;
+
public function setUp()
{
$this->cacheStorageMock = $this->getMockBuilder(CacheInterface::class)
@@ -44,7 +50,6 @@ public function setUp()
->getMock();
$this->sessionMock = $this->getMockBuilder(Session::class)
->disableOriginalConstructor()
- ->setMethods(['getUser', 'getId'])
->getMock();
$this->viewerLoggerMock = $this->getMockBuilder(Logger::class)
->disableOriginalConstructor()
@@ -52,6 +57,7 @@ public function setUp()
$this->productMetadataMock = $this->getMockBuilder(ProductMetadataInterface::class)
->disableOriginalConstructor()
->getMock();
+ $this->userMock = $this->createMock(User::class);
$objectManager = new ObjectManager($this);
$this->canViewNotification = $objectManager->getObject(
CanViewNotification::class,
@@ -68,8 +74,8 @@ public function testIsVisibleLoadDataFromCache()
{
$this->sessionMock->expects($this->once())
->method('getUser')
- ->willReturn($this->sessionMock);
- $this->sessionMock->expects($this->once())
+ ->willReturn($this->userMock);
+ $this->userMock->expects($this->once())
->method('getId')
->willReturn(1);
$this->cacheStorageMock->expects($this->once())
@@ -93,8 +99,8 @@ public function testIsVisible($expected, $version, $lastViewVersion)
->willReturn(false);
$this->sessionMock->expects($this->once())
->method('getUser')
- ->willReturn($this->sessionMock);
- $this->sessionMock->expects($this->once())
+ ->willReturn($this->userMock);
+ $this->userMock->expects($this->once())
->method('getId')
->willReturn(1);
$this->productMetadataMock->expects($this->once())
diff --git a/app/code/Magento/Reports/etc/db_schema.xml b/app/code/Magento/Reports/etc/db_schema.xml
index 1321ebba4d3d6..30accf36a053e 100644
--- a/app/code/Magento/Reports/etc/db_schema.xml
+++ b/app/code/Magento/Reports/etc/db_schema.xml
@@ -10,15 +10,15 @@
+ comment="Index ID"/>
+ comment="Visitor ID"/>
+ comment="Customer ID"/>
+ comment="Product ID"/>
+ comment="Store ID"/>
@@ -54,15 +54,15 @@
+ comment="Index ID"/>
+ comment="Visitor ID"/>
+ comment="Customer ID"/>
+ comment="Product ID"/>
+ comment="Store ID"/>
@@ -97,7 +97,7 @@
+ comment="Event Type ID"/>
@@ -107,19 +107,19 @@
+ comment="Event ID"/>
+ default="0" comment="Event Type ID"/>
+ default="0" comment="Object ID"/>
+ default="0" comment="Subject ID"/>
+ comment="Store ID"/>
@@ -146,12 +146,12 @@
-
+
+ comment="Store ID"/>
+ comment="Product ID"/>
@@ -182,12 +182,12 @@
-
+
+ comment="Store ID"/>
+ comment="Product ID"/>
@@ -218,12 +218,12 @@
-
+
+ comment="Store ID"/>
+ comment="Product ID"/>
diff --git a/app/code/Magento/Review/Model/Rss.php b/app/code/Magento/Review/Model/Rss.php
index df8a5dbb96841..f5abdbb4d3c9e 100644
--- a/app/code/Magento/Review/Model/Rss.php
+++ b/app/code/Magento/Review/Model/Rss.php
@@ -3,11 +3,17 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+
+declare(strict_types=1);
+
namespace Magento\Review\Model;
+use Magento\Framework\App\ObjectManager;
+
/**
- * Class Rss
- * @package Magento\Catalog\Model\Rss\Product
+ * Model Rss
+ *
+ * Class \Magento\Catalog\Model\Rss\Product\Rss
*/
class Rss extends \Magento\Framework\Model\AbstractModel
{
@@ -24,18 +30,35 @@ class Rss extends \Magento\Framework\Model\AbstractModel
protected $eventManager;
/**
+ * Rss constructor.
+ *
* @param \Magento\Framework\Event\ManagerInterface $eventManager
* @param ReviewFactory $reviewFactory
+ * @param \Magento\Framework\Model\Context|null $context
+ * @param \Magento\Framework\Registry|null $registry
+ * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource
+ * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection
+ * @param array $data
*/
public function __construct(
\Magento\Framework\Event\ManagerInterface $eventManager,
- \Magento\Review\Model\ReviewFactory $reviewFactory
+ \Magento\Review\Model\ReviewFactory $reviewFactory,
+ \Magento\Framework\Model\Context $context = null,
+ \Magento\Framework\Registry $registry = null,
+ \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
+ \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
+ array $data = []
) {
$this->reviewFactory = $reviewFactory;
$this->eventManager = $eventManager;
+ $context = $context ?? ObjectManager::getInstance()->get(\Magento\Framework\Model\Context::class);
+ $registry = $registry ?? ObjectManager::getInstance()->get(\Magento\Framework\Registry::class);
+ parent::__construct($context, $registry, $resource, $resourceCollection, $data);
}
/**
+ * Get Product Collection
+ *
* @return $this|\Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection
*/
public function getProductCollection()
diff --git a/app/code/Magento/Review/etc/db_schema.xml b/app/code/Magento/Review/etc/db_schema.xml
index d1090d413384b..7a451dbbbcf98 100644
--- a/app/code/Magento/Review/etc/db_schema.xml
+++ b/app/code/Magento/Review/etc/db_schema.xml
@@ -9,7 +9,7 @@
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
+ comment="Review entity ID"/>
@@ -17,7 +17,7 @@
+ comment="Status ID"/>
@@ -25,13 +25,13 @@
+ comment="Review ID"/>
+ default="0" comment="Entity ID"/>
+ default="0" comment="Product ID"/>
@@ -53,16 +53,16 @@
+ comment="Review detail ID"/>
+ default="0" comment="Review ID"/>
+ default="0" comment="Store ID"/>
+ comment="Customer ID"/>
@@ -85,11 +85,11 @@
+ comment="Summary review entity ID"/>
+ default="0" comment="Product ID"/>
+ default="0" comment="Entity type ID"/>
+ comment="Rating Option ID"/>
+ default="0" comment="Rating ID"/>
@@ -177,20 +177,20 @@
+ comment="Vote ID"/>
+ default="0" comment="Vote option ID"/>
+ default="0" comment="Customer ID"/>
+ default="0" comment="Product ID"/>
+ default="0" comment="Rating ID"/>
+ comment="Review ID"/>
+ comment="Vote aggregation ID"/>
+ default="0" comment="Rating ID"/>
+ default="0" comment="Product ID"/>
+ default="0" comment="Store ID"/>
@@ -242,9 +242,9 @@
+ default="0" comment="Rating ID"/>
+ default="0" comment="Store ID"/>
@@ -259,9 +259,9 @@
+ default="0" comment="Rating ID"/>
+ default="0" comment="Store ID"/>
diff --git a/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php b/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php
index 021e7b66cd13f..a5c7f71df66c5 100644
--- a/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php
+++ b/app/code/Magento/Sales/Cron/CleanExpiredQuotes.php
@@ -57,7 +57,6 @@ public function execute()
$quotes->addFieldToFilter('store_id', $storeId);
$quotes->addFieldToFilter('updated_at', ['to' => date("Y-m-d", time() - $lifetime)]);
- $quotes->addFieldToFilter('is_active', 0);
foreach ($this->getExpireQuotesAdditionalFilterFields() as $field => $condition) {
$quotes->addFieldToFilter($field, $condition);
diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertProductInShoppingCartSectionActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertProductInShoppingCartSectionActionGroup.xml
new file mode 100644
index 0000000000000..4fd992418887a
--- /dev/null
+++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertProductInShoppingCartSectionActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Assert product in Shopping cart section in Customer's Activities block on Create Order Page.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminMoveProductToItemsOrderedFromShoppingCartActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminMoveProductToItemsOrderedFromShoppingCartActionGroup.xml
new file mode 100644
index 0000000000000..8512387d45e8a
--- /dev/null
+++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminMoveProductToItemsOrderedFromShoppingCartActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Move product to the "Items Ordered" section from shopping cart.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontCustomerReorderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontCustomerReorderActionGroup.xml
new file mode 100644
index 0000000000000..03a14ca514091
--- /dev/null
+++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontCustomerReorderActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Navigate to customer dashboard -> orders. Press 'reorder' button for specified order id. Notice: customer should be logged in.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml
index 6f62ce199ecbb..f57b1f65eb94e 100644
--- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml
+++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrderFormTotalSection.xml
@@ -11,6 +11,6 @@
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml
index 415bac7fd051d..c0deb9ab55d2b 100644
--- a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml
+++ b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontCustomerOrdersGridSection.xml
@@ -10,5 +10,6 @@
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminProductInTheShoppingCartCouldBeReachedByAdminDuringOrderCreationWithMultiWebsiteConfigTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminProductInTheShoppingCartCouldBeReachedByAdminDuringOrderCreationWithMultiWebsiteConfigTest.xml
new file mode 100644
index 0000000000000..b40e9d041a10e
--- /dev/null
+++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminProductInTheShoppingCartCouldBeReachedByAdminDuringOrderCreationWithMultiWebsiteConfigTest.xml
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml
index b8772f24a2a42..0e58bb84988a2 100644
--- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml
+++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml
@@ -129,7 +129,7 @@
-
+
diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml
index 9909fca44fe2c..3ff8a7791d88b 100644
--- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml
+++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml
@@ -125,7 +125,7 @@
-
+
diff --git a/app/code/Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php b/app/code/Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php
index e424cae85f223..ad6a3e03ba679 100644
--- a/app/code/Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php
+++ b/app/code/Magento/Sales/Test/Unit/Cron/CleanExpiredQuotesTest.php
@@ -59,7 +59,7 @@ public function testExecute($lifetimes, $additionalFilterFields)
$this->quoteFactoryMock->expects($this->exactly(count($lifetimes)))
->method('create')
->will($this->returnValue($quotesMock));
- $quotesMock->expects($this->exactly((3 + count($additionalFilterFields)) * count($lifetimes)))
+ $quotesMock->expects($this->exactly((2 + count($additionalFilterFields)) * count($lifetimes)))
->method('addFieldToFilter');
if (!empty($lifetimes)) {
$quotesMock->expects($this->exactly(count($lifetimes)))
diff --git a/app/code/Magento/Sales/etc/db_schema.xml b/app/code/Magento/Sales/etc/db_schema.xml
index 1f781604491bf..eb508af8daf25 100644
--- a/app/code/Magento/Sales/etc/db_schema.xml
+++ b/app/code/Magento/Sales/etc/db_schema.xml
@@ -9,7 +9,7 @@
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
+ comment="Entity ID"/>
@@ -19,9 +19,9 @@
+ comment="Store ID"/>
+ comment="Customer ID"/>
+ comment="Billing Address ID"/>
@@ -158,11 +158,11 @@
+ comment="Quote Address ID"/>
+ comment="Quote ID"/>
+ comment="Shipping Address ID"/>
-
+
@@ -201,21 +201,21 @@
-
-
+
+
-
+ comment="Original Increment ID"/>
+
-
+ comment="Relation Child Real ID"/>
+
+ comment="Relation Parent Real ID"/>
@@ -297,13 +297,13 @@
+ comment="Entity ID"/>
+ comment="Store ID"/>
+ comment="Customer ID"/>
-
+
@@ -386,17 +386,17 @@
+ comment="Entity ID"/>
+ comment="Parent ID"/>
+ comment="Customer Address ID"/>
+ comment="Quote Address ID"/>
+ comment="Region ID"/>
+ comment="Customer ID"/>
@@ -405,17 +405,17 @@
-
+
-
+
-
+
@@ -431,9 +431,9 @@
+ comment="Entity ID"/>
+ comment="Parent ID"/>
+ comment="Item ID"/>
+ default="0" comment="Order ID"/>
+ comment="Parent Item ID"/>
+ comment="Quote Item ID"/>
+ comment="Store ID"/>
+ comment="Product ID"/>
-
+
+ comment="Entity ID"/>
+ comment="Parent ID"/>
+ comment="Quote Payment ID"/>
@@ -663,7 +663,7 @@
-
+
@@ -681,7 +681,7 @@
comment="Echeck Account Name"/>
-
+
@@ -696,9 +696,9 @@
+ comment="Entity ID"/>
+ comment="Store ID"/>
+ comment="Order ID"/>
+ comment="Customer ID"/>
+ comment="Shipping Address ID"/>
+ comment="Billing Address ID"/>
-
+
-
+ comment="Entity ID"/>
+
-
+ comment="Store ID"/>
+
+ comment="Order ID"/>
+ default="CURRENT_TIMESTAMP" comment="Order Increment ID"/>
@@ -837,9 +837,9 @@
+ comment="Entity ID"/>
+ comment="Parent ID"/>
+ comment="Product ID"/>
+ comment="Order Item ID"/>
@@ -867,14 +867,14 @@
+ comment="Entity ID"/>
+ comment="Parent ID"/>
+ comment="Order ID"/>
@@ -901,9 +901,9 @@
+ comment="Entity ID"/>
+ comment="Parent ID"/>
+ comment="Entity ID"/>
+ comment="Store ID"/>
+ comment="Billing Address ID"/>
+ comment="Order ID"/>
+ comment="Shipping Address ID"/>
-
+
-
+
-
+ comment="Entity ID"/>
+
+ comment="Store ID"/>
-
+ comment="Order ID"/>
+
@@ -1136,9 +1136,9 @@
+ comment="Entity ID"/>
+ comment="Parent ID"/>
+ comment="Product ID"/>
+ comment="Order Item ID"/>
@@ -1192,9 +1192,9 @@
+ comment="Entity ID"/>
+ comment="Parent ID"/>
+ comment="Entity ID"/>
+ comment="Store ID"/>
+ comment="Order ID"/>
+ comment="Shipping Address ID"/>
+ comment="Billing Address ID"/>
+ comment="Invoice ID"/>
-
-
+
+
-
+ comment="Entity ID"/>
+
-
+ comment="Order ID"/>
+
@@ -1366,13 +1366,13 @@
comment="Base Grand Total"/>
+ comment="Store ID"/>
+ identity="false" comment="Customer Group ID"/>
@@ -1440,9 +1440,9 @@
+ comment="Entity ID"/>
+ comment="Parent ID"/>
+ comment="Product ID"/>
+ comment="Order Item ID"/>
@@ -1496,9 +1496,9 @@
+ comment="Entity ID"/>
+ comment="Parent ID"/>
-
+
+ comment="Store ID"/>
@@ -1552,10 +1552,10 @@
-
+
+ comment="Store ID"/>
@@ -1584,10 +1584,10 @@
-
+
+ comment="Store ID"/>
@@ -1638,10 +1638,10 @@
-
+
+ comment="Store ID"/>
@@ -1692,15 +1692,15 @@
+ comment="Transaction ID"/>
+ comment="Parent ID"/>
+ default="0" comment="Order ID"/>
-
-
+ default="0" comment="Payment ID"/>
+
+
@@ -1732,10 +1732,10 @@
-
+
+ comment="Store ID"/>
@@ -1762,10 +1762,10 @@
-
+
+ comment="Store ID"/>
@@ -1791,10 +1791,10 @@
-
+
+ comment="Store ID"/>
@@ -1822,10 +1822,10 @@
-
+
+ comment="Store ID"/>
@@ -1853,12 +1853,12 @@
-
+
+ comment="Store ID"/>
+ comment="Product ID"/>
@@ -1886,12 +1886,12 @@
-
+
+ comment="Store ID"/>
+ comment="Product ID"/>
@@ -1919,12 +1919,12 @@
-
+
+ comment="Store ID"/>
+ comment="Product ID"/>
@@ -1952,9 +1952,9 @@
+ comment="Tax ID"/>
+ comment="Order ID"/>
+ comment="Tax Item ID"/>
+ comment="Tax ID"/>
+ comment="Item ID"/>
+ comment="Store ID"/>
diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php
index 4fd06e88878b4..8d81afeab4c90 100644
--- a/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php
+++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php
@@ -56,6 +56,7 @@ public function resolve(
$items[] = [
'id' => $order->getId(),
'increment_id' => $order->getIncrementId(),
+ 'order_number' => $order->getIncrementId(),
'created_at' => $order->getCreatedAt(),
'grand_total' => $order->getGrandTotal(),
'status' => $order->getStatus(),
diff --git a/app/code/Magento/SalesGraphQl/etc/schema.graphqls b/app/code/Magento/SalesGraphQl/etc/schema.graphqls
index 06146f805c644..a7c30f582e752 100644
--- a/app/code/Magento/SalesGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/SalesGraphQl/etc/schema.graphqls
@@ -7,7 +7,8 @@ type Query {
type CustomerOrder @doc(description: "Order mapping fields") {
id: Int
- increment_id: String
+ increment_id: String @deprecated(reason: "Use the order_number instaed.")
+ order_number: String! @doc(description: "The order number")
created_at: String
grand_total: Float
status: String
diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php
index 89a0d6e579727..56c08864c90c4 100644
--- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php
+++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/NewActionHtml.php
@@ -1,12 +1,19 @@
getRequest()->getParam('id');
- $formName = $this->getRequest()->getParam('form');
+ $id = $this->getRequest()
+ ->getParam('id');
+ $formName = $this->getRequest()
+ ->getParam('form_namespace');
$typeArr = explode('|', str_replace('-', '/', $this->getRequest()->getParam('type')));
$type = $typeArr[0];
@@ -27,7 +36,7 @@ public function execute()
)->setType(
$type
)->setRule(
- $this->_objectManager->create(\Magento\SalesRule\Model\Rule::class)
+ $this->_objectManager->create(Rule::class)
)->setPrefix(
'actions'
);
@@ -35,12 +44,14 @@ public function execute()
$model->setAttribute($typeArr[1]);
}
- if ($model instanceof \Magento\Rule\Model\Condition\AbstractCondition) {
+ if ($model instanceof AbstractCondition) {
$model->setJsFormObject($formName);
+ $model->setFormName($formName);
$html = $model->asHtmlRecursive();
} else {
$html = '';
}
- $this->getResponse()->setBody($html);
+ $this->getResponse()
+ ->setBody($html);
}
}
diff --git a/app/code/Magento/SalesRule/Model/Rule/Action/SimpleActionOptionsProvider.php b/app/code/Magento/SalesRule/Model/Rule/Action/SimpleActionOptionsProvider.php
new file mode 100644
index 0000000000000..a0fd4bf576f61
--- /dev/null
+++ b/app/code/Magento/SalesRule/Model/Rule/Action/SimpleActionOptionsProvider.php
@@ -0,0 +1,30 @@
+ __('Percent of product price discount'), 'value' => Rule::BY_PERCENT_ACTION],
+ ['label' => __('Fixed amount discount'), 'value' => Rule::BY_FIXED_ACTION],
+ ['label' => __('Fixed amount discount for whole cart'), 'value' => Rule::CART_FIXED_ACTION],
+ ['label' => __('Buy X get Y free (discount amount is Y)'), 'value' => Rule::BUY_X_GET_Y_ACTION]
+ ];
+ }
+}
diff --git a/app/code/Magento/SalesRule/Model/Rule/Metadata/ValueProvider.php b/app/code/Magento/SalesRule/Model/Rule/Metadata/ValueProvider.php
index fdd6c2b169a7d..e4aaaec98dc79 100644
--- a/app/code/Magento/SalesRule/Model/Rule/Metadata/ValueProvider.php
+++ b/app/code/Magento/SalesRule/Model/Rule/Metadata/ValueProvider.php
@@ -5,11 +5,14 @@
*/
namespace Magento\SalesRule\Model\Rule\Metadata;
-use Magento\SalesRule\Model\Rule;
-use Magento\Store\Model\System\Store;
use Magento\Customer\Api\GroupRepositoryInterface;
use Magento\Framework\Api\SearchCriteriaBuilder;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Convert\DataObject;
+use Magento\SalesRule\Model\Rule;
+use Magento\SalesRule\Model\Rule\Action\SimpleActionOptionsProvider;
+use Magento\SalesRule\Model\RuleFactory;
+use Magento\Store\Model\System\Store;
/**
* Metadata provider for sales rule edit form.
@@ -37,10 +40,15 @@ class ValueProvider
protected $objectConverter;
/**
- * @var \Magento\SalesRule\Model\RuleFactory
+ * @var RuleFactory
*/
protected $salesRuleFactory;
+ /**
+ * @var SimpleActionOptionsProvider
+ */
+ private $simpleActionOptionsProvider;
+
/**
* Initialize dependencies.
*
@@ -48,20 +56,24 @@ class ValueProvider
* @param GroupRepositoryInterface $groupRepository
* @param SearchCriteriaBuilder $searchCriteriaBuilder
* @param DataObject $objectConverter
- * @param \Magento\SalesRule\Model\RuleFactory $salesRuleFactory
+ * @param RuleFactory $salesRuleFactory
+ * @param SimpleActionOptionsProvider|null $simpleActionOptionsProvider
*/
public function __construct(
Store $store,
GroupRepositoryInterface $groupRepository,
SearchCriteriaBuilder $searchCriteriaBuilder,
DataObject $objectConverter,
- \Magento\SalesRule\Model\RuleFactory $salesRuleFactory
+ RuleFactory $salesRuleFactory,
+ SimpleActionOptionsProvider $simpleActionOptionsProvider = null
) {
$this->store = $store;
$this->groupRepository = $groupRepository;
$this->searchCriteriaBuilder = $searchCriteriaBuilder;
$this->objectConverter = $objectConverter;
$this->salesRuleFactory = $salesRuleFactory;
+ $this->simpleActionOptionsProvider = $simpleActionOptionsProvider ?:
+ ObjectManager::getInstance()->get(SimpleActionOptionsProvider::class);
}
/**
@@ -71,15 +83,10 @@ public function __construct(
* @return array
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
- public function getMetadataValues(\Magento\SalesRule\Model\Rule $rule)
+ public function getMetadataValues(Rule $rule)
{
$customerGroups = $this->groupRepository->getList($this->searchCriteriaBuilder->create())->getItems();
- $applyOptions = [
- ['label' => __('Percent of product price discount'), 'value' => Rule::BY_PERCENT_ACTION],
- ['label' => __('Fixed amount discount'), 'value' => Rule::BY_FIXED_ACTION],
- ['label' => __('Fixed amount discount for whole cart'), 'value' => Rule::CART_FIXED_ACTION],
- ['label' => __('Buy X get Y free (discount amount is Y)'), 'value' => Rule::BUY_X_GET_Y_ACTION]
- ];
+ $applyOptions = $this->simpleActionOptionsProvider->toOptionArray();
$couponTypesOptions = [];
$couponTypes = $this->salesRuleFactory->create()->getCouponTypes();
diff --git a/app/code/Magento/SalesRule/Model/RulesApplier.php b/app/code/Magento/SalesRule/Model/RulesApplier.php
index f771a4f1e3892..1214e6642b440 100644
--- a/app/code/Magento/SalesRule/Model/RulesApplier.php
+++ b/app/code/Magento/SalesRule/Model/RulesApplier.php
@@ -6,12 +6,16 @@
namespace Magento\SalesRule\Model;
use Magento\Quote\Model\Quote\Address;
+use Magento\Quote\Model\Quote\Item\AbstractItem;
use Magento\SalesRule\Model\Quote\ChildrenValidationLocator;
use Magento\Framework\App\ObjectManager;
+use Magento\SalesRule\Model\ResourceModel\Rule\Collection;
use Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory;
+use Magento\SalesRule\Model\Rule\Action\Discount\DataFactory;
/**
* Class RulesApplier
+ *
* @package Magento\SalesRule\Model\Validator
*/
class RulesApplier
@@ -39,29 +43,37 @@ class RulesApplier
private $calculatorFactory;
/**
- * @param \Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory $calculatorFactory
+ * @var \Magento\SalesRule\Model\Rule\Action\Discount\DataFactory
+ */
+ protected $discountFactory;
+
+ /**
+ * @param CalculatorFactory $calculatorFactory
* @param \Magento\Framework\Event\ManagerInterface $eventManager
- * @param \Magento\SalesRule\Model\Utility $utility
+ * @param Utility $utility
* @param ChildrenValidationLocator|null $childrenValidationLocator
+ * @param DataFactory $discountDataFactory
*/
public function __construct(
\Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory $calculatorFactory,
\Magento\Framework\Event\ManagerInterface $eventManager,
\Magento\SalesRule\Model\Utility $utility,
- ChildrenValidationLocator $childrenValidationLocator = null
+ ChildrenValidationLocator $childrenValidationLocator = null,
+ DataFactory $discountDataFactory = null
) {
$this->calculatorFactory = $calculatorFactory;
$this->validatorUtility = $utility;
$this->_eventManager = $eventManager;
$this->childrenValidationLocator = $childrenValidationLocator
?: ObjectManager::getInstance()->get(ChildrenValidationLocator::class);
+ $this->discountFactory = $discountDataFactory ?: ObjectManager::getInstance()->get(DataFactory::class);
}
/**
* Apply rules to current order item
*
- * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item
- * @param \Magento\SalesRule\Model\ResourceModel\Rule\Collection $rules
+ * @param AbstractItem $item
+ * @param Collection $rules
* @param bool $skipValidation
* @param mixed $couponCode
* @return array
@@ -71,7 +83,7 @@ public function applyRules($item, $rules, $skipValidation, $couponCode)
{
$address = $item->getAddress();
$appliedRuleIds = [];
- /* @var $rule \Magento\SalesRule\Model\Rule */
+ /* @var $rule Rule */
foreach ($rules as $rule) {
if (!$this->validatorUtility->canProcessRule($rule, $address)) {
continue;
@@ -79,7 +91,7 @@ public function applyRules($item, $rules, $skipValidation, $couponCode)
if (!$skipValidation && !$rule->getActions()->validate($item)) {
if (!$this->childrenValidationLocator->isChildrenValidationRequired($item)) {
- continue;
+ continue;
}
$childItems = $item->getChildren();
$isContinue = true;
@@ -110,7 +122,7 @@ public function applyRules($item, $rules, $skipValidation, $couponCode)
* Add rule discount description label to address object
*
* @param Address $address
- * @param \Magento\SalesRule\Model\Rule $rule
+ * @param Rule $rule
* @return $this
*/
public function addDiscountDescription($address, $rule)
@@ -123,6 +135,10 @@ public function addDiscountDescription($address, $rule)
} else {
if (strlen($address->getCouponCode())) {
$label = $address->getCouponCode();
+
+ if ($rule->getDescription()) {
+ $label = $rule->getDescription();
+ }
}
}
@@ -136,8 +152,10 @@ public function addDiscountDescription($address, $rule)
}
/**
- * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item
- * @param \Magento\SalesRule\Model\Rule $rule
+ * Apply Rule
+ *
+ * @param AbstractItem $item
+ * @param Rule $rule
* @param \Magento\Quote\Model\Quote\Address $address
* @param mixed $couponCode
* @return $this
@@ -154,8 +172,10 @@ protected function applyRule($item, $rule, $address, $couponCode)
}
/**
- * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item
- * @param \Magento\SalesRule\Model\Rule $rule
+ * Get discount Data
+ *
+ * @param AbstractItem $item
+ * @param Rule $rule
* @return \Magento\SalesRule\Model\Rule\Action\Discount\Data
*/
protected function getDiscountData($item, $rule)
@@ -165,9 +185,9 @@ protected function getDiscountData($item, $rule)
$discountCalculator = $this->calculatorFactory->create($rule->getSimpleAction());
$qty = $discountCalculator->fixQuantity($qty, $rule);
$discountData = $discountCalculator->calculate($rule, $item, $qty);
-
$this->eventFix($discountData, $item, $rule, $qty);
$this->validatorUtility->deltaRoundingFix($discountData, $item);
+ $this->setDiscountBreakdown($discountData, $item, $rule);
/**
* We can't use row total here because row total not include tax
@@ -180,8 +200,35 @@ protected function getDiscountData($item, $rule)
}
/**
+ * Set Discount Breakdown
+ *
* @param \Magento\SalesRule\Model\Rule\Action\Discount\Data $discountData
* @param \Magento\Quote\Model\Quote\Item\AbstractItem $item
+ * @param \Magento\SalesRule\Model\Rule $rule
+ * @return $this
+ */
+ private function setDiscountBreakdown($discountData, $item, $rule)
+ {
+ if ($discountData->getAmount() > 0) {
+ /** @var \Magento\SalesRule\Model\Rule\Action\Discount\Data $discount */
+ $discount = $this->discountFactory->create();
+ $discount->setBaseOriginalAmount($discountData->getBaseOriginalAmount());
+ $discount->setAmount($discountData->getAmount());
+ $discount->setBaseAmount($discountData->getBaseAmount());
+ $discount->setOriginalAmount($discountData->getOriginalAmount());
+ $discountBreakdown = $item->getExtensionAttributes()->getDiscounts() ?? [];
+ $discountBreakdown[$rule->getId()]['discount'] = $discount;
+ $discountBreakdown[$rule->getId()]['rule'] = $rule;
+ $item->getExtensionAttributes()->setDiscounts($discountBreakdown);
+ }
+ return $this;
+ }
+
+ /**
+ * Set Discount data
+ *
+ * @param \Magento\SalesRule\Model\Rule\Action\Discount\Data $discountData
+ * @param AbstractItem $item
* @return $this
*/
protected function setDiscountData($discountData, $item)
@@ -198,7 +245,7 @@ protected function setDiscountData($discountData, $item)
* Set coupon code to address if $rule contains validated coupon
*
* @param Address $address
- * @param \Magento\SalesRule\Model\Rule $rule
+ * @param Rule $rule
* @param mixed $couponCode
* @return $this
*/
@@ -208,7 +255,7 @@ public function maintainAddressCouponCode($address, $rule, $couponCode)
Rule is a part of rules collection, which includes only rules with 'No Coupon' type or with validated coupon.
As a result, if rule uses coupon code(s) ('Specific' or 'Auto' Coupon Type), it always contains validated coupon
*/
- if ($rule->getCouponType() != \Magento\SalesRule\Model\Rule::COUPON_TYPE_NO_COUPON) {
+ if ($rule->getCouponType() != Rule::COUPON_TYPE_NO_COUPON) {
$address->setCouponCode($couponCode);
}
@@ -219,15 +266,15 @@ public function maintainAddressCouponCode($address, $rule, $couponCode)
* Fire event to allow overwriting of discount amounts
*
* @param \Magento\SalesRule\Model\Rule\Action\Discount\Data $discountData
- * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item
- * @param \Magento\SalesRule\Model\Rule $rule
+ * @param AbstractItem $item
+ * @param Rule $rule
* @param float $qty
* @return $this
*/
protected function eventFix(
\Magento\SalesRule\Model\Rule\Action\Discount\Data $discountData,
- \Magento\Quote\Model\Quote\Item\AbstractItem $item,
- \Magento\SalesRule\Model\Rule $rule,
+ AbstractItem $item,
+ Rule $rule,
$qty
) {
$quote = $item->getQuote();
@@ -249,11 +296,13 @@ protected function eventFix(
}
/**
- * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item
+ * Set Applied Rule Ids
+ *
+ * @param AbstractItem $item
* @param int[] $appliedRuleIds
* @return $this
*/
- public function setAppliedRuleIds(\Magento\Quote\Model\Quote\Item\AbstractItem $item, array $appliedRuleIds)
+ public function setAppliedRuleIds(AbstractItem $item, array $appliedRuleIds)
{
$address = $item->getAddress();
$quote = $item->getQuote();
diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/SimpleActionOptionsProviderTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/SimpleActionOptionsProviderTest.php
new file mode 100644
index 0000000000000..f1653dd043b50
--- /dev/null
+++ b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Action/SimpleActionOptionsProviderTest.php
@@ -0,0 +1,43 @@
+model = $objectManager->getObject(SimpleActionOptionsProvider::class);
+ }
+
+ public function testToOptionArray()
+ {
+ $expected = [
+ ['label' => __('Percent of product price discount'), 'value' => Rule::BY_PERCENT_ACTION],
+ ['label' => __('Fixed amount discount'), 'value' => Rule::BY_FIXED_ACTION],
+ ['label' => __('Fixed amount discount for whole cart'), 'value' => Rule::CART_FIXED_ACTION],
+ ['label' => __('Buy X get Y free (discount amount is Y)'), 'value' => Rule::BUY_X_GET_Y_ACTION]
+ ];
+
+ $this->assertEquals($expected, $this->model->toOptionArray());
+ }
+}
diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Metadata/ValueProviderTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Metadata/ValueProviderTest.php
index 0864b4a5e1480..d63ba150f4822 100644
--- a/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Metadata/ValueProviderTest.php
+++ b/app/code/Magento/SalesRule/Test/Unit/Model/Rule/Metadata/ValueProviderTest.php
@@ -5,52 +5,72 @@
*/
namespace Magento\SalesRule\Test\Unit\Model\Rule\Metadata;
+use Magento\Customer\Api\Data\GroupInterface;
+use Magento\Customer\Api\Data\GroupSearchResultsInterface;
+use Magento\Customer\Api\GroupRepositoryInterface;
+use Magento\Framework\Api\SearchCriteriaBuilder;
+use Magento\Framework\Api\SearchCriteriaInterface;
+use Magento\Framework\Convert\DataObject;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
+use Magento\SalesRule\Model\Rule;
+use Magento\SalesRule\Model\Rule\Action\SimpleActionOptionsProvider;
+use Magento\SalesRule\Model\Rule\Metadata\ValueProvider;
+use Magento\SalesRule\Model\RuleFactory;
+use Magento\Store\Model\System\Store;
+use PHPUnit\Framework\TestCase;
+use PHPUnit\Framework\MockObject\MockObject;
/**
* @covers Magento\SalesRule\Model\Rule\Metadata\ValueProvider
*/
-class ValueProviderTest extends \PHPUnit\Framework\TestCase
+class ValueProviderTest extends TestCase
{
/**
- * @var \Magento\SalesRule\Model\Rule\Metadata\ValueProvider
+ * @var ValueProvider
*/
protected $model;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var Store|MockObject
*/
protected $storeMock;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var GroupRepositoryInterface|MockObject
*/
protected $groupRepositoryMock;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var SearchCriteriaBuilder|MockObject
*/
protected $searchCriteriaBuilderMock;
/**
- * @var \PHPUnit_Framework_MockObject_MockObject
+ * @var DataObject|MockObject
*/
protected $dataObjectMock;
/**
- * @var \Magento\SalesRule\Model\RuleFactory|\PHPUnit_Framework_MockObject_MockObject
+ * @var RuleFactory|MockObject
*/
protected $ruleFactoryMock;
+ /**
+ * @var SimpleActionOptionsProvider|MockObject
+ */
+ private $simpleActionOptionsProviderMock;
+
protected function setUp()
{
- $this->searchCriteriaBuilderMock = $this->createMock(\Magento\Framework\Api\SearchCriteriaBuilder::class);
- $this->storeMock = $this->createMock(\Magento\Store\Model\System\Store::class);
- $this->groupRepositoryMock = $this->createMock(\Magento\Customer\Api\GroupRepositoryInterface::class);
- $this->dataObjectMock = $this->createMock(\Magento\Framework\Convert\DataObject::class);
- $searchCriteriaMock = $this->createMock(\Magento\Framework\Api\SearchCriteriaInterface::class);
- $groupSearchResultsMock = $this->createMock(\Magento\Customer\Api\Data\GroupSearchResultsInterface::class);
- $groupsMock = $this->createMock(\Magento\Customer\Api\Data\GroupInterface::class);
+ $expectedData = include __DIR__ . '/_files/MetaData.php';
+ $this->searchCriteriaBuilderMock = $this->createMock(SearchCriteriaBuilder::class);
+ $this->storeMock = $this->createMock(Store::class);
+ $this->groupRepositoryMock = $this->createMock(GroupRepositoryInterface::class);
+ $this->dataObjectMock = $this->createMock(DataObject::class);
+ $this->simpleActionOptionsProviderMock = $this->createMock(SimpleActionOptionsProvider::class);
+ $searchCriteriaMock = $this->createMock(SearchCriteriaInterface::class);
+ $groupSearchResultsMock = $this->createMock(GroupSearchResultsInterface::class);
+ $groupsMock = $this->createMock(GroupInterface::class);
$this->searchCriteriaBuilderMock->expects($this->once())->method('create')->willReturn($searchCriteriaMock);
$this->groupRepositoryMock->expects($this->once())->method('getList')->with($searchCriteriaMock)
@@ -59,15 +79,19 @@ protected function setUp()
$this->storeMock->expects($this->once())->method('getWebsiteValuesForForm')->willReturn([]);
$this->dataObjectMock->expects($this->once())->method('toOptionArray')->with([$groupsMock], 'id', 'code')
->willReturn([]);
- $this->ruleFactoryMock = $this->createPartialMock(\Magento\SalesRule\Model\RuleFactory::class, ['create']);
+ $this->ruleFactoryMock = $this->createPartialMock(RuleFactory::class, ['create']);
+ $this->simpleActionOptionsProviderMock->method('toOptionArray')->willReturn(
+ $expectedData['actions']['children']['simple_action']['arguments']['data']['config']['options']
+ );
$this->model = (new ObjectManager($this))->getObject(
- \Magento\SalesRule\Model\Rule\Metadata\ValueProvider::class,
+ ValueProvider::class,
[
'store' => $this->storeMock,
'groupRepository' => $this->groupRepositoryMock,
'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock,
'objectConverter' => $this->dataObjectMock,
'salesRuleFactory' => $this->ruleFactoryMock,
+ 'simpleActionOptionsProvider' => $this->simpleActionOptionsProviderMock
]
);
}
@@ -76,8 +100,8 @@ public function testGetMetadataValues()
{
$expectedData = include __DIR__ . '/_files/MetaData.php';
- /** @var \Magento\SalesRule\Model\Rule|\PHPUnit_Framework_MockObject_MockObject $ruleMock */
- $ruleMock = $this->createMock(\Magento\SalesRule\Model\Rule::class);
+ /** @var Rule|MockObject $ruleMock */
+ $ruleMock = $this->createMock(Rule::class);
$this->ruleFactoryMock->expects($this->once())
->method('create')
->willReturn($ruleMock);
diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php
index 217a8dba273c4..4260e6b415091 100644
--- a/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php
+++ b/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php
@@ -6,55 +6,81 @@
namespace Magento\SalesRule\Test\Unit\Model;
+use Magento\Framework\Event\Manager;
+use Magento\Quote\Model\Quote;
+use Magento\Quote\Model\Quote\Address;
+use Magento\Quote\Model\Quote\Item;
+use Magento\Quote\Model\Quote\Item\AbstractItem;
+use Magento\Rule\Model\Action\Collection;
+use Magento\SalesRule\Model\Quote\ChildrenValidationLocator;
+use Magento\SalesRule\Model\Rule;
+use Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory;
+use Magento\SalesRule\Model\Rule\Action\Discount\Data;
+use Magento\SalesRule\Model\Rule\Action\Discount\DiscountInterface;
+use Magento\SalesRule\Model\RulesApplier;
+use Magento\SalesRule\Model\Utility;
+use PHPUnit\Framework\TestCase;
+use PHPUnit_Framework_MockObject_MockObject;
+
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
-class RulesApplierTest extends \PHPUnit\Framework\TestCase
+class RulesApplierTest extends TestCase
{
/**
- * @var \Magento\SalesRule\Model\RulesApplier
+ * @var RulesApplier
*/
protected $rulesApplier;
/**
- * @var \Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory|\PHPUnit_Framework_MockObject_MockObject
+ * @var CalculatorFactory|PHPUnit_Framework_MockObject_MockObject
*/
protected $calculatorFactory;
/**
- * @var \Magento\Framework\Event\Manager|\PHPUnit_Framework_MockObject_MockObject
+ * @var \Magento\SalesRule\Model\Rule\Action\Discount\DataFactory|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $discountFactory;
+
+ /**
+ * @var Manager|PHPUnit_Framework_MockObject_MockObject
*/
protected $eventManager;
/**
- * @var \Magento\SalesRule\Model\Utility|\PHPUnit_Framework_MockObject_MockObject
+ * @var Utility|PHPUnit_Framework_MockObject_MockObject
*/
protected $validatorUtility;
/**
- * @var \Magento\SalesRule\Model\Quote\ChildrenValidationLocator|\PHPUnit_Framework_MockObject_MockObject
+ * @var ChildrenValidationLocator|PHPUnit_Framework_MockObject_MockObject
*/
protected $childrenValidationLocator;
protected function setUp()
{
$this->calculatorFactory = $this->createMock(
- \Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory::class
+ CalculatorFactory::class
+ );
+ $this->discountFactory = $this->createPartialMock(
+ \Magento\SalesRule\Model\Rule\Action\Discount\DataFactory::class,
+ ['create']
);
$this->eventManager = $this->createPartialMock(\Magento\Framework\Event\Manager::class, ['dispatch']);
$this->validatorUtility = $this->createPartialMock(
- \Magento\SalesRule\Model\Utility::class,
+ Utility::class,
['canProcessRule', 'minFix', 'deltaRoundingFix', 'getItemQty']
);
$this->childrenValidationLocator = $this->createPartialMock(
- \Magento\SalesRule\Model\Quote\ChildrenValidationLocator::class,
+ ChildrenValidationLocator::class,
['isChildrenValidationRequired']
);
- $this->rulesApplier = new \Magento\SalesRule\Model\RulesApplier(
+ $this->rulesApplier = new RulesApplier(
$this->calculatorFactory,
$this->eventManager,
$this->validatorUtility,
- $this->childrenValidationLocator
+ $this->childrenValidationLocator,
+ $this->discountFactory
);
}
@@ -73,21 +99,36 @@ public function testApplyRulesWhenRuleWithStopRulesProcessingIsUsed($isChildren,
$ruleId = 1;
$appliedRuleIds = [$ruleId => $ruleId];
-
+ $discountData = $this->getMockBuilder(\Magento\SalesRule\Model\Rule\Action\Discount\Data::class)
+ ->setConstructorArgs(
+ [
+ 'amount' => 0,
+ 'baseAmount' => 0,
+ 'originalAmount' => 0,
+ 'baseOriginalAmount' => 0
+ ]
+ )
+ ->getMock();
+ $this->discountFactory->expects($this->any())
+ ->method('create')
+ ->with($this->anything())
+ ->will($this->returnValue($discountData));
/**
- * @var \Magento\SalesRule\Model\Rule|\PHPUnit_Framework_MockObject_MockObject $ruleWithStopFurtherProcessing
+ * @var Rule|PHPUnit_Framework_MockObject_MockObject $ruleWithStopFurtherProcessing
*/
$ruleWithStopFurtherProcessing = $this->createPartialMock(
- \Magento\SalesRule\Model\Rule::class,
+ Rule::class,
['getStoreLabel', 'getCouponType', 'getRuleId', '__wakeup', 'getActions']
);
- /** @var \Magento\SalesRule\Model\Rule|\PHPUnit_Framework_MockObject_MockObject $ruleThatShouldNotBeRun */
+ /**
+ * @var Rule|PHPUnit_Framework_MockObject_MockObject $ruleThatShouldNotBeRun
+ */
$ruleThatShouldNotBeRun = $this->createPartialMock(
- \Magento\SalesRule\Model\Rule::class,
+ Rule::class,
['getStopRulesProcessing', '__wakeup']
);
- $actionMock = $this->createPartialMock(\Magento\Rule\Model\Action\Collection::class, ['validate']);
+ $actionMock = $this->createPartialMock(Collection::class, ['validate']);
$ruleWithStopFurtherProcessing->setName('ruleWithStopFurtherProcessing');
$ruleThatShouldNotBeRun->setName('ruleThatShouldNotBeRun');
@@ -140,6 +181,40 @@ public function testApplyRulesWhenRuleWithStopRulesProcessingIsUsed($isChildren,
$this->assertEquals($appliedRuleIds, $result);
}
+ public function testAddCouponDescriptionWithRuleDescriptionIsUsed()
+ {
+ $ruleId = 1;
+ $ruleDescription = 'Rule description';
+
+ /**
+ * @var Rule|PHPUnit_Framework_MockObject_MockObject $rule
+ */
+ $rule = $this->createPartialMock(
+ Rule::class,
+ ['getStoreLabel', 'getCouponType', 'getRuleId', '__wakeup', 'getActions']
+ );
+
+ $rule->setDescription($ruleDescription);
+
+ /**
+ * @var Address|PHPUnit_Framework_MockObject_MockObject $address
+ */
+ $address = $this->createPartialMock(
+ Address::class,
+ [
+ 'getQuote',
+ 'setCouponCode',
+ 'setAppliedRuleIds',
+ '__wakeup'
+ ]
+ );
+ $description = $address->getDiscountDescriptionArray();
+ $description[$ruleId] = $rule->getDescription();
+ $address->setDiscountDescriptionArray($description[$ruleId]);
+
+ $this->assertEquals($address->getDiscountDescriptionArray(), $description[$ruleId]);
+ }
+
/**
* @return array
*/
@@ -152,29 +227,48 @@ public function dataProviderChildren()
}
/**
- * @return \Magento\Quote\Model\Quote\Item\AbstractItem|\PHPUnit_Framework_MockObject_MockObject
+ * @return AbstractItem|PHPUnit_Framework_MockObject_MockObject
*/
protected function getPreparedItem()
{
- /** @var \Magento\Quote\Model\Quote\Address|\PHPUnit_Framework_MockObject_MockObject $address */
- $address = $this->createPartialMock(\Magento\Quote\Model\Quote\Address::class, [
+ /**
+ * @var Address|PHPUnit_Framework_MockObject_MockObject $address
+ */
+ $address = $this->createPartialMock(
+ Address::class,
+ [
'getQuote',
'setCouponCode',
'setAppliedRuleIds',
'__wakeup'
- ]);
- /** @var \Magento\Quote\Model\Quote\Item\AbstractItem|\PHPUnit_Framework_MockObject_MockObject $item */
- $item = $this->createPartialMock(\Magento\Quote\Model\Quote\Item::class, [
+ ]
+ );
+ /**
+ * @var AbstractItem|PHPUnit_Framework_MockObject_MockObject $item
+ */
+ $item = $this->createPartialMock(
+ Item::class,
+ [
'setDiscountAmount',
'setBaseDiscountAmount',
'setDiscountPercent',
'getAddress',
'setAppliedRuleIds',
'__wakeup',
- 'getChildren'
- ]);
+ 'getChildren',
+ 'getExtensionAttributes'
+ ]
+ );
+ $itemExtension = $this->getMockBuilder(
+ \Magento\Framework\Api\ExtensionAttributesInterface::class
+ )->setMethods(['setDiscounts', 'getDiscounts'])->getMock();
+ $itemExtension->method('getDiscounts')->willReturn([]);
+ $itemExtension->expects($this->any())
+ ->method('setDiscounts')
+ ->willReturn([]);
$quote = $this->createPartialMock(\Magento\Quote\Model\Quote::class, ['getStore', '__wakeUp']);
$item->expects($this->any())->method('getAddress')->will($this->returnValue($address));
+ $item->expects($this->any())->method('getExtensionAttributes')->will($this->returnValue($itemExtension));
$address->expects($this->any())
->method('getQuote')
->will($this->returnValue($quote));
@@ -190,10 +284,10 @@ protected function applyRule($item, $rule)
{
$qty = 2;
$discountCalc = $this->createPartialMock(
- \Magento\SalesRule\Model\Rule\Action\Discount\DiscountInterface::class,
+ DiscountInterface::class,
['fixQuantity', 'calculate']
);
- $discountData = $this->getMockBuilder(\Magento\SalesRule\Model\Rule\Action\Discount\Data::class)
+ $discountData = $this->getMockBuilder(Data::class)
->setConstructorArgs(
[
'amount' => 30,
diff --git a/app/code/Magento/SalesRule/etc/db_schema.xml b/app/code/Magento/SalesRule/etc/db_schema.xml
index 5a4877bbf825e..e100121bea345 100644
--- a/app/code/Magento/SalesRule/etc/db_schema.xml
+++ b/app/code/Magento/SalesRule/etc/db_schema.xml
@@ -58,7 +58,7 @@
+ comment="Coupon ID"/>
@@ -94,9 +94,9 @@
+ comment="Coupon ID"/>
+ comment="Customer ID"/>
@@ -115,11 +115,11 @@
+ comment="Rule Customer ID"/>
+ default="0" comment="Customer ID"/>
@@ -141,11 +141,11 @@
+ comment="Label ID"/>
+ comment="Store ID"/>
@@ -166,11 +166,11 @@
+ comment="Website ID"/>
+ comment="Customer Group ID"/>
+ comment="Attribute ID"/>
@@ -200,10 +200,10 @@
-
+
+ comment="Store ID"/>
-
+
+ comment="Store ID"/>
-
+
+ comment="Store ID"/>
+ comment="Website ID"/>
@@ -341,7 +341,7 @@
+ comment="Customer Group ID"/>
diff --git a/app/code/Magento/SalesRule/etc/extension_attributes.xml b/app/code/Magento/SalesRule/etc/extension_attributes.xml
new file mode 100644
index 0000000000000..202ced4204f73
--- /dev/null
+++ b/app/code/Magento/SalesRule/etc/extension_attributes.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/code/Magento/SalesSequence/etc/db_schema.xml b/app/code/Magento/SalesSequence/etc/db_schema.xml
index 7ad48badf7b80..5ae72319c5a69 100644
--- a/app/code/Magento/SalesSequence/etc/db_schema.xml
+++ b/app/code/Magento/SalesSequence/etc/db_schema.xml
@@ -9,7 +9,7 @@
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
+ comment="ID"/>
@@ -37,10 +37,10 @@
+ comment="ID"/>
+ comment="Store ID"/>
diff --git a/app/code/Magento/Search/Model/QueryFactory.php b/app/code/Magento/Search/Model/QueryFactory.php
index 2122451742402..4186b5c3055f4 100644
--- a/app/code/Magento/Search/Model/QueryFactory.php
+++ b/app/code/Magento/Search/Model/QueryFactory.php
@@ -5,13 +5,15 @@
*/
namespace Magento\Search\Model;
-use Magento\Search\Helper\Data;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\Helper\Context;
use Magento\Framework\ObjectManagerInterface;
use Magento\Framework\Stdlib\StringUtils as StdlibString;
+use Magento\Search\Helper\Data;
/**
+ * Search Query Factory
+ *
* @api
* @since 100.0.2
*/
@@ -72,7 +74,7 @@ public function __construct(
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function get()
{
@@ -82,9 +84,7 @@ public function get()
$rawQueryText = $this->getRawQueryText();
$preparedQueryText = $this->getPreparedQueryText($rawQueryText, $maxQueryLength);
$query = $this->create()->loadByQueryText($preparedQueryText);
- if (!$query->getId()) {
- $query->setQueryText($preparedQueryText);
- }
+ $query->setQueryText($preparedQueryText);
$query->setIsQueryTextExceeded($this->isQueryTooLong($rawQueryText, $maxQueryLength));
$query->setIsQueryTextShort($this->isQueryTooShort($rawQueryText, $minQueryLength));
$this->query = $query;
@@ -117,6 +117,8 @@ private function getRawQueryText()
}
/**
+ * Prepare query text
+ *
* @param string $queryText
* @param int|string $maxQueryLength
* @return string
@@ -130,6 +132,8 @@ private function getPreparedQueryText($queryText, $maxQueryLength)
}
/**
+ * Check if the provided text exceeds the provided length
+ *
* @param string $queryText
* @param int|string $maxQueryLength
* @return bool
@@ -140,6 +144,8 @@ private function isQueryTooLong($queryText, $maxQueryLength)
}
/**
+ * Check if the provided text is shorter than the provided length
+ *
* @param string $queryText
* @param int|string $minQueryLength
* @return bool
diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml
index 0ec33c48f259e..61a89b4610d6a 100644
--- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml
+++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml
@@ -28,6 +28,7 @@
+
diff --git a/app/code/Magento/Search/Test/Unit/Model/QueryFactoryTest.php b/app/code/Magento/Search/Test/Unit/Model/QueryFactoryTest.php
index 3df457b0d4497..f66c1c7dd9e3f 100644
--- a/app/code/Magento/Search/Test/Unit/Model/QueryFactoryTest.php
+++ b/app/code/Magento/Search/Test/Unit/Model/QueryFactoryTest.php
@@ -5,14 +5,14 @@
*/
namespace Magento\Search\Test\Unit\Model;
-use Magento\Search\Helper\Data;
use Magento\Framework\App\Helper\Context;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\ObjectManagerInterface;
-use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
-use Magento\Search\Model\QueryFactory;
use Magento\Framework\Stdlib\StringUtils;
+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
+use Magento\Search\Helper\Data;
use Magento\Search\Model\Query;
+use Magento\Search\Model\QueryFactory;
/**
* Class QueryFactoryTest tests Magento\Search\Model\QueryFactory
@@ -67,7 +67,7 @@ protected function setUp()
->disableOriginalConstructor()
->getMock();
$this->query = $this->getMockBuilder(Query::class)
- ->setMethods(['setIsQueryTextExceeded', 'setIsQueryTextShort', 'loadByQueryText', 'getId', 'setQueryText'])
+ ->setMethods(['setIsQueryTextExceeded', 'setIsQueryTextShort', 'loadByQueryText', 'getId'])
->disableOriginalConstructor()
->getMock();
@@ -124,7 +124,6 @@ public function testGetNewQuery()
$isQueryTextExceeded = false;
$isQueryTextShort = false;
- $this->mockSetQueryTextNeverExecute($cleanedRawText);
$this->mockString($cleanedRawText);
$this->mockQueryLengths($maxQueryLength, $minQueryLength);
$this->mockGetRawQueryText($rawQueryText);
@@ -135,6 +134,7 @@ public function testGetNewQuery()
$result = $this->model->get();
$this->assertSame($this->query, $result);
+ $this->assertSearchQuery($cleanedRawText);
}
/**
@@ -150,7 +150,6 @@ public function testGetQueryTwice()
$isQueryTextExceeded = false;
$isQueryTextShort = false;
- $this->mockSetQueryTextNeverExecute($cleanedRawText);
$this->mockString($cleanedRawText);
$this->mockQueryLengths($maxQueryLength, $minQueryLength);
$this->mockGetRawQueryText($rawQueryText);
@@ -163,6 +162,7 @@ public function testGetQueryTwice()
$result = $this->model->get();
$this->assertSame($this->query, $result, 'After second execution queries are not same');
+ $this->assertSearchQuery($cleanedRawText);
}
/**
@@ -184,7 +184,6 @@ public function testGetTooLongQuery()
->withConsecutive([$cleanedRawText, 0, $maxQueryLength])
->willReturn($subRawText);
- $this->mockSetQueryTextNeverExecute($cleanedRawText);
$this->mockString($cleanedRawText);
$this->mockQueryLengths($maxQueryLength, $minQueryLength);
$this->mockGetRawQueryText($rawQueryText);
@@ -194,6 +193,7 @@ public function testGetTooLongQuery()
$result = $this->model->get();
$this->assertSame($this->query, $result);
+ $this->assertSearchQuery($subRawText);
}
/**
@@ -209,7 +209,6 @@ public function testGetTooShortQuery()
$isQueryTextExceeded = false;
$isQueryTextShort = true;
- $this->mockSetQueryTextNeverExecute($cleanedRawText);
$this->mockString($cleanedRawText);
$this->mockQueryLengths($maxQueryLength, $minQueryLength);
$this->mockGetRawQueryText($rawQueryText);
@@ -219,6 +218,7 @@ public function testGetTooShortQuery()
$result = $this->model->get();
$this->assertSame($this->query, $result);
+ $this->assertSearchQuery($cleanedRawText);
}
/**
@@ -234,7 +234,6 @@ public function testGetQueryWithoutId()
$isQueryTextExceeded = false;
$isQueryTextShort = false;
- $this->mockSetQueryTextOnceExecute($cleanedRawText);
$this->mockString($cleanedRawText);
$this->mockQueryLengths($maxQueryLength, $minQueryLength);
$this->mockGetRawQueryText($rawQueryText);
@@ -244,6 +243,35 @@ public function testGetQueryWithoutId()
$result = $this->model->get();
$this->assertSame($this->query, $result);
+ $this->assertSearchQuery($cleanedRawText);
+ }
+
+ /**
+ * Test for inaccurate match of search query in query_text table
+ *
+ * Because of inaccurate string comparison of utf8_general_ci,
+ * the search_query result text may be different from the original text (e.g organos, Organos, Órganos)
+ */
+ public function testInaccurateQueryTextMatch()
+ {
+ $queryId = 1;
+ $maxQueryLength = 100;
+ $minQueryLength = 3;
+ $rawQueryText = 'Órganos';
+ $cleanedRawText = 'Órganos';
+ $isQueryTextExceeded = false;
+ $isQueryTextShort = false;
+
+ $this->mockString($cleanedRawText);
+ $this->mockQueryLengths($maxQueryLength, $minQueryLength);
+ $this->mockGetRawQueryText($rawQueryText);
+ $this->mockSimpleQuery($cleanedRawText, $queryId, $isQueryTextExceeded, $isQueryTextShort, 'Organos');
+
+ $this->mockCreateQuery();
+
+ $result = $this->model->get();
+ $this->assertSame($this->query, $result);
+ $this->assertSearchQuery($cleanedRawText);
}
/**
@@ -305,15 +333,25 @@ private function mockCreateQuery()
* @param int $queryId
* @param bool $isQueryTextExceeded
* @param bool $isQueryTextShort
+ * @param string $matchedQueryText
* @return void
*/
- private function mockSimpleQuery($cleanedRawText, $queryId, $isQueryTextExceeded, $isQueryTextShort)
- {
+ private function mockSimpleQuery(
+ string $cleanedRawText,
+ ?int $queryId,
+ bool $isQueryTextExceeded,
+ bool $isQueryTextShort,
+ string $matchedQueryText = null
+ ) {
+ if (null === $matchedQueryText) {
+ $matchedQueryText = $cleanedRawText;
+ }
$this->query->expects($this->once())
->method('loadByQueryText')
->withConsecutive([$cleanedRawText])
->willReturnSelf();
- $this->query->expects($this->once())
+ $this->query->setData(['query_text' => $matchedQueryText]);
+ $this->query->expects($this->any())
->method('getId')
->willReturn($queryId);
$this->query->expects($this->once())
@@ -328,23 +366,8 @@ private function mockSimpleQuery($cleanedRawText, $queryId, $isQueryTextExceeded
* @param string $cleanedRawText
* @return void
*/
- private function mockSetQueryTextNeverExecute($cleanedRawText)
+ private function assertSearchQuery($cleanedRawText)
{
- $this->query->expects($this->never())
- ->method('setQueryText')
- ->withConsecutive([$cleanedRawText])
- ->willReturnSelf();
- }
-
- /**
- * @param string $cleanedRawText
- * @return void
- */
- private function mockSetQueryTextOnceExecute($cleanedRawText)
- {
- $this->query->expects($this->once())
- ->method('setQueryText')
- ->withConsecutive([$cleanedRawText])
- ->willReturnSelf();
+ $this->assertEquals($cleanedRawText, $this->query->getQueryText());
}
}
diff --git a/app/code/Magento/Search/etc/db_schema.xml b/app/code/Magento/Search/etc/db_schema.xml
index 754af7d246d6d..ab4b54298c2a3 100644
--- a/app/code/Magento/Search/etc/db_schema.xml
+++ b/app/code/Magento/Search/etc/db_schema.xml
@@ -49,12 +49,12 @@
+ comment="Synonyms Group ID"/>
+ default="0" comment="Store ID - identifies the store view these synonyms belong to"/>
+ default="0" comment="Website ID - identifies the website ID these synonyms belong to"/>
diff --git a/app/code/Magento/Security/etc/db_schema.xml b/app/code/Magento/Security/etc/db_schema.xml
index ce7143582ce69..5052f5642cb53 100644
--- a/app/code/Magento/Security/etc/db_schema.xml
+++ b/app/code/Magento/Security/etc/db_schema.xml
@@ -10,7 +10,7 @@
-
+
5.00
- F
+ 0
This shipping method is not available. To use this shipping method, please contact us.
diff --git a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php
index 8eeeb5bf6bc12..5cfc7349888f3 100644
--- a/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php
+++ b/app/code/Magento/Sitemap/Controller/Adminhtml/Sitemap/Generate.php
@@ -49,7 +49,13 @@ public function execute()
// if sitemap record exists
if ($sitemap->getId()) {
try {
+ $this->appEmulation->startEnvironmentEmulation(
+ $sitemap->getStoreId(),
+ \Magento\Framework\App\Area::AREA_FRONTEND,
+ true
+ );
$sitemap->generateXml();
+ $this->appEmulation->stopEnvironmentEmulation();
$this->messageManager->addSuccessMessage(
__('The sitemap "%1" has been generated.', $sitemap->getSitemapFilename())
);
diff --git a/app/code/Magento/Sitemap/etc/db_schema.xml b/app/code/Magento/Sitemap/etc/db_schema.xml
index b3c028b626b73..adf1f11124f52 100644
--- a/app/code/Magento/Sitemap/etc/db_schema.xml
+++ b/app/code/Magento/Sitemap/etc/db_schema.xml
@@ -9,13 +9,13 @@
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
+ comment="Sitemap ID"/>
+ default="0" comment="Store ID"/>
diff --git a/app/code/Magento/Store/Model/Store.php b/app/code/Magento/Store/Model/Store.php
index 0bc371da0aab9..faa26b24a5505 100644
--- a/app/code/Magento/Store/Model/Store.php
+++ b/app/code/Magento/Store/Model/Store.php
@@ -53,7 +53,7 @@ class Store extends AbstractExtensibleModel implements
const ENTITY = 'store';
/**
- * Custom entry point param
+ * Parameter used to determine app context.
*/
const CUSTOM_ENTRY_POINT_PARAM = 'custom_entry_point';
@@ -104,7 +104,7 @@ class Store extends AbstractExtensibleModel implements
const ADMIN_CODE = 'admin';
/**
- * Cache tag
+ * Tag to use to cache stores.
*/
const CACHE_TAG = 'store';
@@ -423,6 +423,9 @@ public function __construct(
/**
* @inheritdoc
+ *
+ * @SuppressWarnings(PHPMD.SerializationAware)
+ * @deprecated Do not use PHP serialization.
*/
public function __sleep()
{
@@ -435,6 +438,9 @@ public function __sleep()
* Init not serializable fields
*
* @return void
+ *
+ * @SuppressWarnings(PHPMD.SerializationAware)
+ * @deprecated Do not use PHP serialization.
*/
public function __wakeup()
{
diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml
index 94856bb083da8..63dc4b0ded4f9 100644
--- a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml
+++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminDeleteStoreViewActionGroup.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Store/etc/db_schema.xml b/app/code/Magento/Store/etc/db_schema.xml
index 6eea94b8deec7..5b2e178ff24b8 100644
--- a/app/code/Magento/Store/etc/db_schema.xml
+++ b/app/code/Magento/Store/etc/db_schema.xml
@@ -9,13 +9,13 @@
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
+ comment="Website ID"/>
+ identity="false" default="0" comment="Default Group ID"/>
@@ -33,14 +33,14 @@
+ comment="Group ID"/>
+ default="0" comment="Website ID"/>
+ default="0" comment="Root Category ID"/>
+ identity="false" default="0" comment="Default Store ID"/>
@@ -59,12 +59,12 @@
+ comment="Store ID"/>
+ default="0" comment="Website ID"/>
+ default="0" comment="Group ID"/>
diff --git a/app/code/Magento/StoreGraphQl/etc/graphql/di.xml b/app/code/Magento/StoreGraphQl/etc/graphql/di.xml
index 3a0143821d8b9..f3771b704c3e9 100644
--- a/app/code/Magento/StoreGraphQl/etc/graphql/di.xml
+++ b/app/code/Magento/StoreGraphQl/etc/graphql/di.xml
@@ -23,4 +23,11 @@
+
+
+
+ - store/information/name
+
+
+
diff --git a/app/code/Magento/StoreGraphQl/etc/schema.graphqls b/app/code/Magento/StoreGraphQl/etc/schema.graphqls
index 376635e5c8f75..aaef3aa13dbaf 100644
--- a/app/code/Magento/StoreGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/StoreGraphQl/etc/schema.graphqls
@@ -30,4 +30,5 @@ type StoreConfig @doc(description: "The type contains information about a store
secure_base_link_url : String @doc(description: "Secure base link URL for the store")
secure_base_static_url : String @doc(description: "Secure base static URL for the store")
secure_base_media_url : String @doc(description: "Secure base media URL for the store")
+ store_name : String @doc(description: "Name of the store")
}
diff --git a/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php b/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php
index 0848f566f67bb..a2cae7f7b5a20 100644
--- a/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php
+++ b/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php
@@ -3,6 +3,7 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types = 1);
namespace Magento\Swatches\Block\Product\Renderer;
use Magento\Catalog\Block\Product\Context;
@@ -57,6 +58,11 @@ class Configurable extends \Magento\ConfigurableProduct\Block\Product\View\Type\
*/
const SWATCH_THUMBNAIL_NAME = 'swatchThumb';
+ /**
+ * Config path which contains number of swatches per product
+ */
+ private const XML_PATH_SWATCHES_PER_PRODUCT = 'catalog/frontend/swatches_per_product';
+
/**
* @var Product
*/
@@ -200,7 +206,7 @@ public function getJsonSwatchConfig()
public function getNumberSwatchesPerProduct()
{
return $this->_scopeConfig->getValue(
- 'catalog/frontend/swatches_per_product',
+ self::XML_PATH_SWATCHES_PER_PRODUCT,
ScopeInterface::SCOPE_STORE
);
}
diff --git a/app/code/Magento/Swatches/Model/Plugin/EavAttribute.php b/app/code/Magento/Swatches/Model/Plugin/EavAttribute.php
index ebbb1775aa7f8..0acd7ef315700 100644
--- a/app/code/Magento/Swatches/Model/Plugin/EavAttribute.php
+++ b/app/code/Magento/Swatches/Model/Plugin/EavAttribute.php
@@ -14,6 +14,8 @@
/**
* Plugin model for Catalog Resource Attribute
+ *
+ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
*/
class EavAttribute
{
@@ -29,6 +31,11 @@ class EavAttribute
*/
const BASE_OPTION_TITLE = 'option';
+ /**
+ * Prefix added to option value added through API
+ */
+ private const API_OPTION_PREFIX = 'id_';
+
/**
* @var \Magento\Swatches\Model\ResourceModel\Swatch\CollectionFactory
*/
@@ -189,7 +196,9 @@ protected function processSwatchOptions(Attribute $attribute)
if (!empty($optionsArray) && is_array($optionsArray)) {
$optionsArray = $this->prepareOptionIds($optionsArray);
- $attributeSavedOptions = $attribute->getSource()->getAllOptions();
+ $adminStoreAttribute = clone $attribute;
+ $adminStoreAttribute->setStoreId(self::DEFAULT_STORE_ID);
+ $attributeSavedOptions = $adminStoreAttribute->getSource()->getAllOptions();
$this->prepareOptionLinks($optionsArray, $attributeSavedOptions);
}
@@ -227,10 +236,9 @@ protected function prepareOptionLinks(array $optionsArray, array $attributeSaved
{
$dependencyArray = [];
if (is_array($optionsArray['value'])) {
- $optionCounter = 1;
- foreach (array_keys($optionsArray['value']) as $baseOptionId) {
- $dependencyArray[$baseOptionId] = $attributeSavedOptions[$optionCounter]['value'];
- $optionCounter++;
+ $options = array_column($attributeSavedOptions, 'value', 'label');
+ foreach ($optionsArray['value'] as $id => $labels) {
+ $dependencyArray[$id] = $options[$labels[self::DEFAULT_STORE_ID]];
}
}
@@ -285,7 +293,7 @@ protected function processVisualSwatch(Attribute $attribute)
* Clean swatch option values after switching to the dropdown type.
*
* @param array $attributeOptions
- * @param int|null $swatchType
+ * @param int|null $swatchType
* @throws \Magento\Framework\Exception\LocalizedException
*/
private function cleanEavAttributeOptionSwatchValues(array $attributeOptions, int $swatchType = null)
@@ -309,6 +317,8 @@ private function cleanTextSwatchValuesAfterSwitch(array $attributeOptions)
}
/**
+ * Get the visual swatch type based on its value
+ *
* @param string $value
* @return int
*/
@@ -368,7 +378,7 @@ protected function processTextualSwatch(Attribute $attribute)
*/
protected function getAttributeOptionId($optionId)
{
- if (substr($optionId, 0, 6) == self::BASE_OPTION_TITLE) {
+ if (substr($optionId, 0, 6) == self::BASE_OPTION_TITLE || substr($optionId, 0, 3) == self::API_OPTION_PREFIX) {
$optionId = isset($this->dependencyArray[$optionId]) ? $this->dependencyArray[$optionId] : null;
}
return $optionId;
@@ -447,13 +457,10 @@ protected function saveDefaultSwatchOptionValue(Attribute $attribute)
if (!empty($defaultValue)) {
/** @var \Magento\Swatches\Model\Swatch $swatch */
$swatch = $this->swatchFactory->create();
- // created and removed on frontend option not exists in dependency array
- if (substr($defaultValue, 0, 6) == self::BASE_OPTION_TITLE &&
- isset($this->dependencyArray[$defaultValue])
- ) {
- $defaultValue = $this->dependencyArray[$defaultValue];
- }
- $swatch->getResource()->saveDefaultSwatchOption($attribute->getId(), $defaultValue);
+ $swatch->getResource()->saveDefaultSwatchOption(
+ $attribute->getId(),
+ $this->getAttributeOptionId($defaultValue)
+ );
}
}
@@ -503,6 +510,10 @@ protected function isOptionsValid(array $options, Attribute $attribute)
}
/**
+ * Modifies Attribute::usesSource() response
+ *
+ * Returns true if attribute type is swatch
+ *
* @param Attribute $attribute
* @param bool $result
* @return bool
diff --git a/app/code/Magento/Swatches/Plugin/Eav/Model/Entity/Attribute/OptionManagement.php b/app/code/Magento/Swatches/Plugin/Eav/Model/Entity/Attribute/OptionManagement.php
new file mode 100644
index 0000000000000..795c48f12ebcc
--- /dev/null
+++ b/app/code/Magento/Swatches/Plugin/Eav/Model/Entity/Attribute/OptionManagement.php
@@ -0,0 +1,92 @@
+attributeRepository = $attributeRepository;
+ $this->swatchHelper = $swatchHelper;
+ }
+
+ /**
+ * Add swatch value to the attribute option
+ *
+ * @param \Magento\Catalog\Model\Product\Attribute\OptionManagement $subject
+ * @param string $attributeCode
+ * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function beforeAdd(
+ \Magento\Catalog\Model\Product\Attribute\OptionManagement $subject,
+ ?string $attributeCode,
+ \Magento\Eav\Api\Data\AttributeOptionInterface $option
+ ) {
+ if (empty($attributeCode)) {
+ return;
+ }
+ $attribute = $this->attributeRepository->get(
+ ProductAttributeInterface::ENTITY_TYPE_CODE,
+ $attributeCode
+ );
+ if (!$attribute || !$this->swatchHelper->isSwatchAttribute($attribute)) {
+ return;
+ }
+ $optionId = $this->getOptionId($option);
+ $optionsValue = $option->getValue();
+ if ($this->swatchHelper->isVisualSwatch($attribute)) {
+ $attribute->setData('swatchvisual', ['value' => [$optionId => $optionsValue]]);
+ } else {
+ $options = [];
+ $options['value'][$optionId][Store::DEFAULT_STORE_ID] = $optionsValue;
+ if (is_array($option->getStoreLabels())) {
+ foreach ($option->getStoreLabels() as $label) {
+ if (!isset($options['value'][$optionId][$label->getStoreId()])) {
+ $options['value'][$optionId][$label->getStoreId()] = null;
+ }
+ }
+ }
+ $attribute->setData('swatchtext', $options);
+ }
+ }
+
+ /**
+ * Returns option id
+ *
+ * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option
+ * @return string
+ */
+ private function getOptionId(\Magento\Eav\Api\Data\AttributeOptionInterface $option) : string
+ {
+ return 'id_' . ($option->getValue() ?: 'new_option');
+ }
+}
diff --git a/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml
index 6fdf2276f39d9..5b714f01fd46f 100644
--- a/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml
+++ b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml
@@ -16,5 +16,6 @@
+
diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml
new file mode 100644
index 0000000000000..3d69895b0c895
--- /dev/null
+++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml
@@ -0,0 +1,160 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminSaveConfigurableProductWithAttributesImagesAndSwatchesTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminSaveConfigurableProductWithAttributesImagesAndSwatchesTest.xml
new file mode 100644
index 0000000000000..f94314fe94806
--- /dev/null
+++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminSaveConfigurableProductWithAttributesImagesAndSwatchesTest.xml
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php b/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php
index 317ea77107222..6bdb83c3a8129 100644
--- a/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php
+++ b/app/code/Magento/Swatches/Test/Unit/Model/Plugin/EavAttributeTest.php
@@ -6,87 +6,153 @@
namespace Magento\Swatches\Test\Unit\Model\Plugin;
+use Magento\Catalog\Model\ResourceModel\Eav\Attribute;
+use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource;
+use Magento\Framework\Serialize\Serializer\Json;
+use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
+use Magento\Swatches\Helper\Data;
use Magento\Swatches\Model\Plugin\EavAttribute;
+use Magento\Swatches\Model\ResourceModel\Swatch\Collection;
+use Magento\Swatches\Model\ResourceModel\Swatch\CollectionFactory;
use Magento\Swatches\Model\Swatch;
+use Magento\Swatches\Model\SwatchAttributeType;
+use Magento\Swatches\Model\SwatchFactory;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
-class EavAttributeTest extends \PHPUnit\Framework\TestCase
+/**
+ * Test plugin model for Catalog Resource Attribute
+ *
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * @SuppressWarnings(PHPMD.TooManyPublicMethods)
+ * @SuppressWarnings(PHPMD.ExcessiveClassLength)
+ */
+class EavAttributeTest extends TestCase
{
- const ATTRIBUTE_ID = 123;
- const OPTION_ID = 'option 12';
- const STORE_ID = 'option 89';
- const ATTRIBUTE_DEFAULT_VALUE = 1;
- const ATTRIBUTE_OPTION_VALUE = 2;
- const ATTRIBUTE_SWATCH_VALUE = 3;
+ private const ATTRIBUTE_ID = 123;
+ private const OPTION_1_ID = 1;
+ private const OPTION_2_ID = 2;
+ private const ADMIN_STORE_ID = 0;
+ private const DEFAULT_STORE_ID = 1;
+ private const NEW_OPTION_KEY = 'option_2';
+ private const ATTRIBUTE_DEFAULT_VALUE = [
+ 0 => self::NEW_OPTION_KEY
+ ];
+ private const VISUAL_ATTRIBUTE_OPTIONS = [
+ 'value' => [
+ self::OPTION_1_ID => [
+ self::ADMIN_STORE_ID => 'Black',
+ self::DEFAULT_STORE_ID => 'Black',
+ ],
+ self::NEW_OPTION_KEY => [
+ self::ADMIN_STORE_ID => 'White',
+ self::DEFAULT_STORE_ID => 'White',
+ ],
+ ]
+ ];
+ private const VISUAL_SWATCH_OPTIONS = [
+ 'value' => [
+ self::OPTION_1_ID => '#000000',
+ self::NEW_OPTION_KEY => '#ffffff',
+ ]
+ ];
+ private const VISUAL_SAVED_OPTIONS = [
+ [
+ 'value' => self::OPTION_1_ID,
+ 'label' => 'Black',
+ ],
+ [
+ 'value' => self::OPTION_2_ID,
+ 'label' => 'White',
+ ]
+ ];
+ private const TEXT_ATTRIBUTE_OPTIONS = [
+ 'value' => [
+ self::OPTION_1_ID => [
+ self::ADMIN_STORE_ID => 'Small',
+ self::DEFAULT_STORE_ID => 'Small',
+ ],
+ self::NEW_OPTION_KEY => [
+ self::ADMIN_STORE_ID => 'Medium',
+ self::DEFAULT_STORE_ID => 'Medium',
+ ],
+ ]
+ ];
+ private const TEXT_SWATCH_OPTIONS = [
+ 'value' => [
+ self::OPTION_1_ID => [
+ self::ADMIN_STORE_ID => 'S',
+ self::DEFAULT_STORE_ID => 'S',
+ ],
+ self::NEW_OPTION_KEY => [
+ self::ADMIN_STORE_ID => 'M',
+ self::DEFAULT_STORE_ID => 'M',
+ ],
+ ]
+ ];
+ private const TEXT_SAVED_OPTIONS = [
+ [
+ 'value' => self::OPTION_1_ID,
+ 'label' => 'Small',
+ ],
+ [
+ 'value' => self::OPTION_2_ID,
+ 'label' => 'Medium',
+ ]
+ ];
/** @var EavAttribute */
private $eavAttribute;
- /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute|\PHPUnit_Framework_MockObject_MockObject */
+ /** @var Attribute|MockObject */
private $attribute;
- /** @var \Magento\Swatches\Model\SwatchFactory|\PHPUnit_Framework_MockObject_MockObject */
+ /** @var SwatchFactory|MockObject */
private $swatchFactory;
- /** @var \Magento\Swatches\Model\ResourceModel\Swatch\CollectionFactory|\PHPUnit_Framework_MockObject_MockObject */
+ /** @var CollectionFactory|MockObject */
private $collectionFactory;
- /** @var \Magento\Swatches\Helper\Data|\PHPUnit_Framework_MockObject_MockObject */
+ /** @var Data|MockObject */
private $swatchHelper;
- /** @var \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource|\PHPUnit_Framework_MockObject_MockObject */
+ /** @var AbstractSource|MockObject */
private $abstractSource;
- /** @var \Magento\Swatches\Model\Swatch|\PHPUnit_Framework_MockObject_MockObject */
- private $swatch;
-
- /** @var \Magento\Swatches\Model\ResourceModel\Swatch|\PHPUnit_Framework_MockObject_MockObject */
+ /** @var \Magento\Swatches\Model\ResourceModel\Swatch|MockObject */
private $resource;
- /** @var \Magento\Swatches\Model\ResourceModel\Swatch\Collection|\PHPUnit_Framework_MockObject_MockObject */
+ /** @var Collection|MockObject */
private $collection;
- /** @var array */
- private $optionIds = [];
-
- /** @var array */
- private $allOptions = [];
-
- /** @var array */
- private $dependencyArray = [];
-
+ /**
+ * {@inheritDoc}
+ */
protected function setUp()
{
- $this->attribute = $this->createMock(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class);
- $this->swatchFactory = $this->createPartialMock(\Magento\Swatches\Model\SwatchFactory::class, ['create']);
- $this->swatchHelper = $this->createMock(\Magento\Swatches\Helper\Data::class);
- $this->swatch = $this->createMock(\Magento\Swatches\Model\Swatch::class);
- $this->resource = $this->createMock(\Magento\Swatches\Model\ResourceModel\Swatch::class);
- $this->collection =
- $this->createMock(\Magento\Swatches\Model\ResourceModel\Swatch\Collection::class);
- $this->collectionFactory = $this->createPartialMock(
- \Magento\Swatches\Model\ResourceModel\Swatch\CollectionFactory::class,
+ $objectManager = new ObjectManager($this);
+ $this->abstractSource = $this->createMock(AbstractSource::class);
+ $this->attribute = $this->createPartialMock(
+ Attribute::class,
+ ['getSource']
+ );
+ $this->attribute->setId(self::ATTRIBUTE_ID);
+ $this->swatchFactory = $this->createPartialMock(
+ SwatchFactory::class,
['create']
);
- $this->abstractSource = $this->createMock(\Magento\Eav\Model\Entity\Attribute\Source\AbstractSource::class);
-
- $serializer = $this->createPartialMock(
- \Magento\Framework\Serialize\Serializer\Json::class,
- ['serialize', 'unserialize']
+ $this->swatchHelper = $objectManager->getObject(
+ Data::class,
+ [
+ 'swatchTypeChecker' => $objectManager->getObject(SwatchAttributeType::class)
+ ]
);
-
- $serializer->expects($this->any())
- ->method('serialize')->willReturnCallback(function ($parameter) {
- return json_encode($parameter);
- });
-
- $serializer->expects($this->any())
- ->method('unserialize')->willReturnCallback(function ($parameter) {
- return json_decode($parameter, true);
- });
-
- $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
+ $this->resource = $this->createMock(\Magento\Swatches\Model\ResourceModel\Swatch::class);
+ $this->collection = $this->createMock(Collection::class);
+ $this->collectionFactory = $this->createPartialMock(CollectionFactory::class, ['create']);
+ $serializer = $objectManager->getObject(Json::class);
$this->eavAttribute = $objectManager->getObject(
- \Magento\Swatches\Model\Plugin\EavAttribute::class,
+ EavAttribute::class,
[
'collectionFactory' => $this->collectionFactory,
'swatchFactory' => $this->swatchFactory,
@@ -94,220 +160,128 @@ protected function setUp()
'serializer' => $serializer,
]
);
-
- $this->optionIds = [
- 'value' => ['option 89' => 'test 1', 'option 114' => 'test 2', 'option 170' => 'test 3'],
- 'delete' => ['option 89' => 0, 'option 114' => 1, 'option 170' => 0],
- ];
- $this->allOptions = [null, ['value' => 'option 12'], ['value' => 'option 154']];
- $this->dependencyArray = ['option 89', 'option 170'];
+ $this->attribute->expects($this->any())
+ ->method('getSource')
+ ->willReturn($this->abstractSource);
+ $swatch = $this->createMock(Swatch::class);
+ $swatch->expects($this->any())
+ ->method('getResource')
+ ->willReturn($this->resource);
+ $this->swatchFactory->expects($this->any())
+ ->method('create')
+ ->willReturn($swatch);
}
+ /**
+ * Test beforeSave plugin for visual swatch
+ */
public function testBeforeSaveVisualSwatch()
{
- $option = [
- 'value' => [
- 0 => 'option value',
+ $this->attribute->setData(
+ [
+ 'defaultvisual' => self::ATTRIBUTE_DEFAULT_VALUE,
+ 'optionvisual' => self::VISUAL_ATTRIBUTE_OPTIONS,
+ 'swatchvisual' => self::VISUAL_SWATCH_OPTIONS,
]
- ];
- $this->attribute->expects($this->exactly(6))->method('getData')->withConsecutive(
- ['defaultvisual'],
- ['optionvisual'],
- ['swatchvisual'],
- ['optionvisual'],
- ['option/delete/0']
- )->will($this->onConsecutiveCalls(
- self::ATTRIBUTE_DEFAULT_VALUE,
- self::ATTRIBUTE_OPTION_VALUE,
- self::ATTRIBUTE_SWATCH_VALUE,
- $option,
- false
- ));
-
- $this->attribute->expects($this->exactly(3))->method('setData')
- ->withConsecutive(
- ['option', self::ATTRIBUTE_OPTION_VALUE],
- ['default', self::ATTRIBUTE_DEFAULT_VALUE],
- ['swatch', self::ATTRIBUTE_SWATCH_VALUE]
- );
-
- $this->swatchHelper->expects($this->once())->method('assembleAdditionalDataEavAttribute')
- ->with($this->attribute);
- $this->swatchHelper->expects($this->atLeastOnce())->method('isVisualSwatch')
- ->with($this->attribute)
- ->willReturn(true);
- $this->swatchHelper->expects($this->once())->method('isSwatchAttribute')
- ->with($this->attribute)
- ->willReturn(true);
- $this->swatchHelper->expects($this->never())->method('isTextSwatch');
+ );
+ $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_VISUAL);
$this->eavAttribute->beforeBeforeSave($this->attribute);
+ $this->assertEquals(self::ATTRIBUTE_DEFAULT_VALUE, $this->attribute->getData('default'));
+ $this->assertEquals(self::VISUAL_ATTRIBUTE_OPTIONS, $this->attribute->getData('option'));
+ $this->assertEquals(self::VISUAL_SWATCH_OPTIONS, $this->attribute->getData('swatch'));
}
+ /**
+ * Test beforeSave plugin for text swatch
+ */
public function testBeforeSaveTextSwatch()
{
- $option = [
- 'value' => [
- 0 => 'option value',
+ $this->attribute->setData(
+ [
+ 'defaulttext' => self::ATTRIBUTE_DEFAULT_VALUE,
+ 'optiontext' => self::TEXT_ATTRIBUTE_OPTIONS,
+ 'swatchtext' => self::TEXT_SWATCH_OPTIONS,
]
- ];
- $this->attribute->expects($this->exactly(6))->method('getData')->withConsecutive(
- ['defaulttext'],
- ['optiontext'],
- ['swatchtext'],
- ['optiontext'],
- ['option/delete/0']
- )->will(
- $this->onConsecutiveCalls(
- self::ATTRIBUTE_DEFAULT_VALUE,
- self::ATTRIBUTE_OPTION_VALUE,
- self::ATTRIBUTE_SWATCH_VALUE,
- $option,
- false
- )
);
- $this->attribute->expects($this->exactly(3))->method('setData')
- ->withConsecutive(
- ['option', self::ATTRIBUTE_OPTION_VALUE],
- ['default', self::ATTRIBUTE_DEFAULT_VALUE],
- ['swatch', self::ATTRIBUTE_SWATCH_VALUE]
- );
-
- $this->swatchHelper->expects($this->once())->method('assembleAdditionalDataEavAttribute')
- ->with($this->attribute);
- $this->swatchHelper->expects($this->atLeastOnce())->method('isVisualSwatch')
- ->with($this->attribute)
- ->willReturn(false);
- $this->swatchHelper->expects($this->atLeastOnce())->method('isTextSwatch')
- ->with($this->attribute)
- ->willReturn(true);
- $this->swatchHelper->expects($this->once())->method('isSwatchAttribute')
- ->with($this->attribute)
- ->willReturn(true);
-
+ $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_TEXT);
$this->eavAttribute->beforeBeforeSave($this->attribute);
+ $this->assertEquals(self::ATTRIBUTE_DEFAULT_VALUE, $this->attribute->getData('default'));
+ $this->assertEquals(self::TEXT_ATTRIBUTE_OPTIONS, $this->attribute->getData('option'));
+ $this->assertEquals(self::TEXT_SWATCH_OPTIONS, $this->attribute->getData('swatch'));
}
/**
+ * Test beforeSave plugin on empty label
+ *
* @expectedException \Magento\Framework\Exception\InputException
* @expectedExceptionMessage Admin is a required field in each row
*/
public function testBeforeSaveWithFailedValidation()
{
- $optionText = [
- 'value' => [
- 0 => '',
+ $options = self::VISUAL_ATTRIBUTE_OPTIONS;
+ $options['value'][self::NEW_OPTION_KEY][self::ADMIN_STORE_ID] = '';
+ $this->attribute->setData(
+ [
+ 'defaultvisual' => self::ATTRIBUTE_DEFAULT_VALUE,
+ 'optionvisual' => $options,
+ 'swatchvisual' => self::VISUAL_SWATCH_OPTIONS,
]
- ];
- $this->swatchHelper->expects($this->once())
- ->method('isSwatchAttribute')
- ->with($this->attribute)
- ->willReturn(true);
-
- $this->swatchHelper->expects($this->atLeastOnce())
- ->method('isVisualSwatch')
- ->willReturn(true);
- $this->attribute->expects($this->exactly(5))
- ->method('getData')
- ->withConsecutive(
- ['defaultvisual'],
- ['optionvisual'],
- ['swatchvisual'],
- ['optionvisual'],
- ['option/delete/0']
- )
- ->will(
- $this->onConsecutiveCalls(
- self::ATTRIBUTE_DEFAULT_VALUE,
- self::ATTRIBUTE_OPTION_VALUE,
- self::ATTRIBUTE_SWATCH_VALUE,
- $optionText,
- false
- )
- );
+ );
+ $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_VISUAL);
$this->eavAttribute->beforeBeforeSave($this->attribute);
}
/**
- * @covers \Magento\Swatches\Model\Plugin\EavAttribute::beforeBeforeSave()
+ * Test beforeSave plugin on empty label of option being deleted
*/
- public function testBeforeSaveWithDeletedOption()
+ public function testValidationIsSkippedForDeletedOption()
{
- $optionText = [
- 'value' => [
- 0 => '',
+ $options = self::VISUAL_ATTRIBUTE_OPTIONS;
+ $options['value'][self::NEW_OPTION_KEY][self::ADMIN_STORE_ID] = '';
+ $options['delete'][self::NEW_OPTION_KEY] = '1';
+ $this->attribute->setData(
+ [
+ 'defaultvisual' => self::ATTRIBUTE_DEFAULT_VALUE,
+ 'optionvisual' => $options,
+ 'swatchvisual' => self::VISUAL_SWATCH_OPTIONS,
]
- ];
-
- $this->swatchHelper->expects($this->once())
- ->method('isSwatchAttribute')
- ->with($this->attribute)
- ->willReturn(true);
+ );
- $this->swatchHelper->expects($this->atLeastOnce())
- ->method('isVisualSwatch')
- ->willReturn(true);
- $this->attribute->expects($this->exactly(6))
- ->method('getData')
- ->withConsecutive(
- ['defaultvisual'],
- ['optionvisual'],
- ['swatchvisual'],
- ['optionvisual'],
- ['option/delete/0'],
- ['swatch_input_type']
- )
- ->will(
- $this->onConsecutiveCalls(
- self::ATTRIBUTE_DEFAULT_VALUE,
- self::ATTRIBUTE_OPTION_VALUE,
- self::ATTRIBUTE_SWATCH_VALUE,
- $optionText,
- true,
- false
- )
- );
+ $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_VISUAL);
$this->eavAttribute->beforeBeforeSave($this->attribute);
+ $this->assertEquals(self::ATTRIBUTE_DEFAULT_VALUE, $this->attribute->getData('default'));
+ $this->assertEquals($options, $this->attribute->getData('option'));
+ $this->assertEquals(self::VISUAL_SWATCH_OPTIONS, $this->attribute->getData('swatch'));
}
+ /**
+ * Test beforeSave plugin for non a swatch attribute
+ */
public function testBeforeSaveNotSwatch()
{
$additionalData = [
- 'swatch_input_type' => 'visual',
- 'update_product_preview_image' => 1,
- 'use_product_image_for_swatch' => 0
- ];
-
- $shortAdditionalData = [
+ Swatch::SWATCH_INPUT_TYPE_KEY => Swatch::SWATCH_INPUT_TYPE_VISUAL,
'update_product_preview_image' => 1,
'use_product_image_for_swatch' => 0
];
- $this->attribute->expects($this->exactly(2))->method('getData')->withConsecutive(
- [Swatch::SWATCH_INPUT_TYPE_KEY],
- ['additional_data']
- )->willReturnOnConsecutiveCalls(
- Swatch::SWATCH_INPUT_TYPE_DROPDOWN,
- json_encode($additionalData)
+ $this->attribute->setData(
+ [
+ Swatch::SWATCH_INPUT_TYPE_KEY => Swatch::SWATCH_INPUT_TYPE_DROPDOWN,
+ 'additional_data' => json_encode($additionalData),
+ ]
);
- $this->attribute
- ->expects($this->once())
- ->method('setData')
- ->with('additional_data', json_encode($shortAdditionalData))
- ->will($this->returnSelf());
+ $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_DROPDOWN);
- $this->swatchHelper->expects($this->never())->method('assembleAdditionalDataEavAttribute');
- $this->swatchHelper->expects($this->never())->method('isVisualSwatch');
- $this->swatchHelper->expects($this->never())->method('isTextSwatch');
+ $this->eavAttribute->beforeBeforeSave($this->attribute);
- $this->swatchHelper->expects($this->once())->method('isSwatchAttribute')
- ->with($this->attribute)
- ->willReturn(false);
+ unset($additionalData[Swatch::SWATCH_INPUT_TYPE_KEY]);
- $this->eavAttribute->beforeBeforeSave($this->attribute);
+ $this->assertEquals(json_encode($additionalData), $this->attribute->getData('additional_data'));
}
/**
@@ -316,390 +290,383 @@ public function testBeforeSaveNotSwatch()
public function visualSwatchProvider()
{
return [
- [Swatch::SWATCH_TYPE_EMPTY, null],
- [Swatch::SWATCH_TYPE_VISUAL_COLOR, '#hex'],
- [Swatch::SWATCH_TYPE_VISUAL_IMAGE, '/path'],
+ [Swatch::SWATCH_TYPE_EMPTY, 'black', 'white'],
+ [Swatch::SWATCH_TYPE_VISUAL_COLOR, '#000000', '#ffffff'],
+ [Swatch::SWATCH_TYPE_VISUAL_IMAGE, '/path/black.png', '/path/white.png'],
];
}
/**
- * @dataProvider visualSwatchProvider
+ * Test afterSave plugin for visual swatch
+ *
+ * @param string $swatchType
+ * @param string $swatch1
+ * @param string $swatch2
*
- * @param $swatchType
- * @param $swatchValue
+ * @dataProvider visualSwatchProvider
*/
- public function testAfterAfterSaveVisualSwatch($swatchType, $swatchValue)
+ public function testAfterAfterSaveVisualSwatch(string $swatchType, string $swatch1, string $swatch2)
{
- $this->abstractSource->expects($this->once())->method('getAllOptions')
- ->willReturn($this->allOptions);
- $this->resource->expects($this->once())->method('saveDefaultSwatchOption')
- ->with(self::ATTRIBUTE_ID, self::OPTION_ID);
+ $options = self::VISUAL_SWATCH_OPTIONS;
+ $options['value'][self::OPTION_1_ID] = $swatch1;
+ $options['value'][self::NEW_OPTION_KEY] = $swatch2;
+ $this->attribute->addData(
+ [
+ 'defaultvisual' => self::ATTRIBUTE_DEFAULT_VALUE,
+ 'optionvisual' => self::VISUAL_ATTRIBUTE_OPTIONS,
+ 'swatchvisual' => $options,
+ ]
+ );
- $this->swatch->expects($this->once())->method('getResource')
- ->willReturn($this->resource);
- $this->swatch->expects($this->once())->method('getId')
- ->willReturn(EavAttribute::DEFAULT_STORE_ID);
- $this->swatch->expects($this->once())->method('save');
- $this->swatch->expects($this->exactly(4))->method('setData')
- ->withConsecutive(
- ['option_id', self::OPTION_ID],
- ['store_id', EavAttribute::DEFAULT_STORE_ID],
- ['type', $swatchType],
- ['value', $swatchValue]
- );
+ $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_VISUAL);
+ $this->eavAttribute->beforeBeforeSave($this->attribute);
+ $this->abstractSource->expects($this->once())
+ ->method('getAllOptions')
+ ->willReturn(self::VISUAL_SAVED_OPTIONS);
+
+ $this->resource->expects($this->once())
+ ->method('saveDefaultSwatchOption')
+ ->with(self::ATTRIBUTE_ID, self::OPTION_2_ID);
- $this->collection->expects($this->exactly(2))->method('addFieldToFilter')
+ $this->collection->expects($this->exactly(4))
+ ->method('addFieldToFilter')
->withConsecutive(
- ['option_id', self::OPTION_ID],
- ['store_id', EavAttribute::DEFAULT_STORE_ID]
- )->willReturnSelf();
+ ['option_id', self::OPTION_1_ID],
+ ['store_id', self::ADMIN_STORE_ID],
+ ['option_id', self::OPTION_2_ID],
+ ['store_id', self::ADMIN_STORE_ID]
+ )
+ ->willReturnSelf();
- $this->collection->expects($this->once())->method('getFirstItem')
- ->willReturn($this->swatch);
- $this->collectionFactory->expects($this->once())->method('create')
+ $this->collection->expects($this->exactly(2))
+ ->method('getFirstItem')
+ ->willReturnOnConsecutiveCalls(
+ $this->createSwatchMock(
+ $swatchType,
+ $swatch1,
+ 1
+ ),
+ $this->createSwatchMock(
+ $swatchType,
+ $swatch2,
+ null,
+ self::OPTION_2_ID,
+ self::ADMIN_STORE_ID
+ )
+ );
+ $this->collectionFactory->expects($this->exactly(2))
+ ->method('create')
->willReturn($this->collection);
- $this->attribute->expects($this->at(0))->method('getData')
- ->willReturn($this->optionIds);
- $this->attribute->expects($this->at(1))->method('getSource')
- ->willReturn($this->abstractSource);
- $this->attribute->expects($this->at(2))->method('getData')
- ->with('default/0')
- ->willReturn($this->dependencyArray[0]);
- $this->attribute->expects($this->at(3))->method('getId')
- ->willReturn(self::ATTRIBUTE_ID);
- $this->attribute->expects($this->at(4))->method('getData')
- ->with('swatch/value')
- ->willReturn([self::STORE_ID => $swatchValue]);
- $this->attribute->expects($this->at(5))->method('getData')
- ->with('option/delete/' . self::OPTION_ID)
- ->willReturn(false);
-
- $this->swatchFactory->expects($this->exactly(1))->method('create')
- ->willReturn($this->swatch);
- $this->swatchHelper->expects($this->exactly(2))->method('isSwatchAttribute')
- ->with($this->attribute)
- ->willReturn(true);
- $this->swatchHelper->expects($this->once())->method('isVisualSwatch')
- ->with($this->attribute)
- ->willReturn(true);
- $this->swatchHelper->expects($this->never())->method('isTextSwatch');
-
$this->eavAttribute->afterAfterSave($this->attribute);
}
- public function testDefaultTextualSwatchAfterSave()
+ /**
+ * Test afterSave plugin for text swatch
+ */
+ public function testAfterAfterSaveTextualSwatch()
{
- $this->abstractSource->expects($this->once())->method('getAllOptions')
- ->willReturn($this->allOptions);
+ $this->attribute->addData(
+ [
+ 'defaulttext' => self::ATTRIBUTE_DEFAULT_VALUE,
+ 'optiontext' => self::TEXT_ATTRIBUTE_OPTIONS,
+ 'swatchtext' => self::TEXT_SWATCH_OPTIONS,
+ ]
+ );
- $this->swatch->expects($this->any())->method('getId')
- ->willReturn(EavAttribute::DEFAULT_STORE_ID);
- $this->swatch->expects($this->any())->method('save');
- $this->swatch->expects($this->any())->method('isDeleted')
- ->with(false);
+ $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_TEXT);
+ $this->eavAttribute->beforeBeforeSave($this->attribute);
- $this->collection->expects($this->any())->method('addFieldToFilter')
- ->willReturnSelf();
- $this->collection->expects($this->any())->method('getFirstItem')
- ->willReturn($this->swatch);
- $this->collectionFactory->expects($this->any())->method('create')
- ->willReturn($this->collection);
+ $this->abstractSource->expects($this->once())
+ ->method('getAllOptions')
+ ->willReturn(self::TEXT_SAVED_OPTIONS);
- $this->attribute->expects($this->at(0))->method('getData')
- ->willReturn($this->optionIds);
- $this->attribute->expects($this->at(1))->method('getSource')
- ->willReturn($this->abstractSource);
- $this->attribute->expects($this->at(2))->method('getData')
- ->with('default/0')
- ->willReturn(null);
-
- $this->attribute->expects($this->at(3))->method('getData')
- ->with('swatch/value')
- ->willReturn(
- [
- self::STORE_ID => [
- 1 => "test",
- 2 => false,
- 3 => null,
- 4 => "",
- ]
- ]
- );
+ $this->resource->expects($this->once())
+ ->method('saveDefaultSwatchOption')
+ ->with(self::ATTRIBUTE_ID, self::OPTION_2_ID);
- $this->swatchHelper->expects($this->exactly(2))->method('isSwatchAttribute')
- ->with($this->attribute)
- ->willReturn(true);
- $this->swatchHelper->expects($this->once())->method('isVisualSwatch')
- ->with($this->attribute)
- ->willReturn(false);
- $this->swatchHelper->expects($this->once())->method('isTextSwatch')
- ->with($this->attribute)
- ->willReturn(true);
-
- $this->swatch->expects($this->any())->method('setData')
+ $this->collection->expects($this->exactly(8))
+ ->method('addFieldToFilter')
->withConsecutive(
- ['option_id', self::OPTION_ID],
- ['store_id', 1],
- ['type', Swatch::SWATCH_TYPE_TEXTUAL],
- ['value', "test"]
- );
-
- $this->eavAttribute->afterAfterSave($this->attribute);
- }
-
- public function testAfterAfterSaveTextualSwatch()
- {
- $this->abstractSource->expects($this->once())->method('getAllOptions')
- ->willReturn($this->allOptions);
- $this->resource->expects($this->once())->method('saveDefaultSwatchOption')
- ->with(self::ATTRIBUTE_ID, self::OPTION_ID);
+ ['option_id', self::OPTION_1_ID],
+ ['store_id', self::ADMIN_STORE_ID],
+ ['option_id', self::OPTION_1_ID],
+ ['store_id', self::DEFAULT_STORE_ID],
+ ['option_id', self::OPTION_2_ID],
+ ['store_id', self::ADMIN_STORE_ID],
+ ['option_id', self::OPTION_2_ID],
+ ['store_id', self::DEFAULT_STORE_ID]
+ )
+ ->willReturnSelf();
- $this->swatch->expects($this->once())->method('getResource')
- ->willReturn($this->resource);
- $this->swatch->expects($this->once())->method('getId')
- ->willReturn(EavAttribute::DEFAULT_STORE_ID);
- $this->swatch->expects($this->once())->method('save');
- $this->swatch->expects($this->once())->method('isDeleted')
- ->with(false);
- $this->swatch->expects($this->exactly(4))->method('setData')
- ->withConsecutive(
- ['option_id', self::OPTION_ID],
- ['store_id', self::OPTION_ID],
- ['type', Swatch::SWATCH_TYPE_TEXTUAL],
- ['value', null]
+ $this->collection->expects($this->exactly(4))
+ ->method('getFirstItem')
+ ->willReturnOnConsecutiveCalls(
+ $this->createSwatchMock(
+ Swatch::SWATCH_TYPE_TEXTUAL,
+ self::TEXT_SWATCH_OPTIONS['value'][self::OPTION_1_ID][self::ADMIN_STORE_ID],
+ 1
+ ),
+ $this->createSwatchMock(
+ Swatch::SWATCH_TYPE_TEXTUAL,
+ self::TEXT_SWATCH_OPTIONS['value'][self::OPTION_1_ID][self::DEFAULT_STORE_ID],
+ 1
+ ),
+ $this->createSwatchMock(
+ Swatch::SWATCH_TYPE_TEXTUAL,
+ self::TEXT_SWATCH_OPTIONS['value'][self::NEW_OPTION_KEY][self::ADMIN_STORE_ID],
+ null,
+ self::OPTION_2_ID,
+ self::ADMIN_STORE_ID
+ ),
+ $this->createSwatchMock(
+ Swatch::SWATCH_TYPE_TEXTUAL,
+ self::TEXT_SWATCH_OPTIONS['value'][self::NEW_OPTION_KEY][self::DEFAULT_STORE_ID],
+ null,
+ self::OPTION_2_ID,
+ self::DEFAULT_STORE_ID
+ )
);
-
- $this->collection->expects($this->exactly(2))->method('addFieldToFilter')
- ->withConsecutive(
- ['option_id', self::OPTION_ID],
- ['store_id', self::OPTION_ID]
- )->willReturnSelf();
- $this->collection->expects($this->once())->method('getFirstItem')
- ->willReturn($this->swatch);
- $this->collectionFactory->expects($this->once())->method('create')
+ $this->collectionFactory->expects($this->exactly(4))
+ ->method('create')
->willReturn($this->collection);
- $this->attribute->expects($this->at(0))->method('getData')
- ->willReturn($this->optionIds);
- $this->attribute->expects($this->at(1))->method('getSource')
- ->willReturn($this->abstractSource);
- $this->attribute->expects($this->at(2))->method('getData')
- ->with('default/0')
- ->willReturn($this->dependencyArray[0]);
- $this->attribute->expects($this->at(3))->method('getId')
- ->willReturn(self::ATTRIBUTE_ID);
- $this->attribute->expects($this->at(4))->method('getData')
- ->with('swatch/value')
- ->willReturn([self::STORE_ID => [self::OPTION_ID => null]]);
- $this->attribute->expects($this->at(5))->method('getData')
- ->with('option/delete/' . self::OPTION_ID)
- ->willReturn(false);
-
- $this->swatchFactory->expects($this->exactly(1))->method('create')
- ->willReturn($this->swatch);
- $this->swatchHelper->expects($this->exactly(2))->method('isSwatchAttribute')
- ->with($this->attribute)
- ->willReturn(true);
- $this->swatchHelper->expects($this->once())->method('isVisualSwatch')
- ->with($this->attribute)
- ->willReturn(false);
- $this->swatchHelper->expects($this->once())->method('isTextSwatch')
- ->with($this->attribute)
- ->willReturn(true);
-
$this->eavAttribute->afterAfterSave($this->attribute);
}
+ /**
+ * Test afterSave plugin for deleted visual swatch option
+ */
public function testAfterAfterSaveVisualSwatchIsDelete()
{
- $this->abstractSource->expects($this->once())->method('getAllOptions')
- ->willReturn($this->allOptions);
- $this->resource->expects($this->once())->method('saveDefaultSwatchOption')
- ->with(self::ATTRIBUTE_ID, self::OPTION_ID);
+ $options = self::VISUAL_ATTRIBUTE_OPTIONS;
+ $options['delete'][self::OPTION_1_ID] = '1';
+ $this->attribute->addData(
+ [
+ 'defaultvisual' => self::ATTRIBUTE_DEFAULT_VALUE,
+ 'optionvisual' => $options,
+ 'swatchvisual' => self::VISUAL_SWATCH_OPTIONS,
+ ]
+ );
- $this->swatch->expects($this->once())->method('getResource')
- ->willReturn($this->resource);
+ $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_VISUAL);
+ $this->eavAttribute->beforeBeforeSave($this->attribute);
+ $this->abstractSource->expects($this->once())
+ ->method('getAllOptions')
+ ->willReturn(self::VISUAL_SAVED_OPTIONS);
- $this->attribute->expects($this->at(0))->method('getData')
- ->willReturn($this->optionIds);
- $this->attribute->expects($this->at(1))->method('getSource')
- ->willReturn($this->abstractSource);
- $this->attribute->expects($this->at(2))->method('getData')
- ->with('default/0')
- ->willReturn($this->dependencyArray[0]);
- $this->attribute->expects($this->at(3))->method('getId')
- ->willReturn(self::ATTRIBUTE_ID);
- $this->attribute->expects($this->at(4))->method('getData')
- ->with('swatch/value')
- ->willReturn([self::STORE_ID => null]);
- $this->attribute->expects($this->at(5))->method('getData')
- ->with('option/delete/' . self::OPTION_ID)
- ->willReturn(true);
-
- $this->swatchFactory->expects($this->once())->method('create')
- ->willReturn($this->swatch);
- $this->swatchHelper->expects($this->exactly(2))->method('isSwatchAttribute')
- ->with($this->attribute)
- ->willReturn(true);
- $this->swatchHelper->expects($this->once())->method('isVisualSwatch')
- ->with($this->attribute)
- ->willReturn(true);
- $this->swatchHelper->expects($this->never())->method('isTextSwatch');
+ $this->resource->expects($this->once())
+ ->method('saveDefaultSwatchOption')
+ ->with(self::ATTRIBUTE_ID, self::OPTION_2_ID);
+
+ $this->collection->expects($this->exactly(2))
+ ->method('addFieldToFilter')
+ ->withConsecutive(
+ ['option_id', self::OPTION_2_ID],
+ ['store_id', self::ADMIN_STORE_ID]
+ )
+ ->willReturnSelf();
+
+ $this->collection->expects($this->exactly(1))
+ ->method('getFirstItem')
+ ->willReturnOnConsecutiveCalls(
+ $this->createSwatchMock(
+ Swatch::SWATCH_TYPE_VISUAL_COLOR,
+ self::VISUAL_SWATCH_OPTIONS['value'][self::NEW_OPTION_KEY],
+ null,
+ self::OPTION_2_ID,
+ self::ADMIN_STORE_ID
+ )
+ );
+ $this->collectionFactory->expects($this->exactly(1))
+ ->method('create')
+ ->willReturn($this->collection);
$this->eavAttribute->afterAfterSave($this->attribute);
}
+ /**
+ * Test afterSave plugin for deleted text swatch option
+ */
public function testAfterAfterSaveTextualSwatchIsDelete()
{
- $this->abstractSource->expects($this->once())->method('getAllOptions')
- ->willReturn($this->allOptions);
- $this->resource->expects($this->once())->method('saveDefaultSwatchOption')
- ->with(self::ATTRIBUTE_ID, self::OPTION_ID);
-
- $this->swatch->expects($this->once())->method('getResource')
- ->willReturn($this->resource);
+ $options = self::TEXT_ATTRIBUTE_OPTIONS;
+ $options['delete'][self::OPTION_1_ID] = '1';
+ $this->attribute->addData(
+ [
+ 'defaulttext' => self::ATTRIBUTE_DEFAULT_VALUE,
+ 'optiontext' => $options,
+ 'swatchtext' => self::TEXT_SWATCH_OPTIONS,
+ ]
+ );
- $this->attribute->expects($this->at(0))->method('getData')
- ->willReturn($this->optionIds);
- $this->attribute->expects($this->at(1))->method('getSource')
- ->willReturn($this->abstractSource);
- $this->attribute->expects($this->at(2))->method('getData')
- ->with('default/0')
- ->willReturn($this->dependencyArray[0]);
- $this->attribute->expects($this->at(3))->method('getId')
- ->willReturn(self::ATTRIBUTE_ID);
- $this->attribute->expects($this->at(4))->method('getData')
- ->with('swatch/value')
- ->willReturn([self::STORE_ID => [self::OPTION_ID => null]]);
- $this->attribute->expects($this->at(5))->method('getData')
- ->with('option/delete/' . self::OPTION_ID)
- ->willReturn(true);
-
- $this->swatchFactory->expects($this->once())->method('create')
- ->willReturn($this->swatch);
- $this->swatchHelper->expects($this->exactly(2))->method('isSwatchAttribute')
- ->with($this->attribute)
- ->willReturn(true);
- $this->swatchHelper->expects($this->once())->method('isVisualSwatch')
- ->with($this->attribute)
- ->willReturn(false);
- $this->swatchHelper->expects($this->once())->method('isTextSwatch')
- ->with($this->attribute)
- ->willReturn(true);
+ $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_TEXT);
+ $this->eavAttribute->beforeBeforeSave($this->attribute);
- $this->eavAttribute->afterAfterSave($this->attribute);
- }
+ $this->abstractSource->expects($this->once())
+ ->method('getAllOptions')
+ ->willReturn(self::TEXT_SAVED_OPTIONS);
- public function testAfterAfterSaveIsSwatchExists()
- {
- $this->abstractSource->expects($this->once())->method('getAllOptions')
- ->willReturn($this->allOptions);
- $this->resource->expects($this->once())->method('saveDefaultSwatchOption')
- ->with(self::ATTRIBUTE_ID, self::OPTION_ID);
+ $this->resource->expects($this->once())
+ ->method('saveDefaultSwatchOption')
+ ->with(self::ATTRIBUTE_ID, self::OPTION_2_ID);
- $this->swatch->expects($this->once())->method('getResource')
- ->willReturn($this->resource);
- $this->swatch->expects($this->once())->method('getId')
- ->willReturn(1);
- $this->swatch->expects($this->once())->method('save');
- $this->swatch->expects($this->once())->method('isDeleted')
- ->with(false);
- $this->swatch->expects($this->exactly(2))->method('setData')
+ $this->collection->expects($this->exactly(4))
+ ->method('addFieldToFilter')
->withConsecutive(
- ['type', Swatch::SWATCH_TYPE_TEXTUAL],
- ['value', null]
- );
+ ['option_id', self::OPTION_2_ID],
+ ['store_id', self::ADMIN_STORE_ID],
+ ['option_id', self::OPTION_2_ID],
+ ['store_id', self::DEFAULT_STORE_ID]
+ )
+ ->willReturnSelf();
- $this->collection->expects($this->exactly(2))->method('addFieldToFilter')
- ->withConsecutive(
- ['option_id', self::OPTION_ID],
- ['store_id', self::OPTION_ID]
- )->willReturnSelf();
- $this->collection->expects($this->once())->method('getFirstItem')
- ->willReturn($this->swatch);
- $this->collectionFactory->expects($this->once())->method('create')
+ $this->collection->expects($this->exactly(2))
+ ->method('getFirstItem')
+ ->willReturnOnConsecutiveCalls(
+ $this->createSwatchMock(
+ Swatch::SWATCH_TYPE_TEXTUAL,
+ self::TEXT_SWATCH_OPTIONS['value'][self::NEW_OPTION_KEY][self::ADMIN_STORE_ID],
+ null,
+ self::OPTION_2_ID,
+ self::ADMIN_STORE_ID
+ ),
+ $this->createSwatchMock(
+ Swatch::SWATCH_TYPE_TEXTUAL,
+ self::TEXT_SWATCH_OPTIONS['value'][self::NEW_OPTION_KEY][self::DEFAULT_STORE_ID],
+ null,
+ self::OPTION_2_ID,
+ self::DEFAULT_STORE_ID
+ )
+ );
+ $this->collectionFactory->expects($this->exactly(2))
+ ->method('create')
->willReturn($this->collection);
- $this->attribute->expects($this->at(0))->method('getData')
- ->willReturn($this->optionIds);
- $this->attribute->expects($this->at(1))->method('getSource')
- ->willReturn($this->abstractSource);
- $this->attribute->expects($this->at(2))->method('getData')
- ->with('default/0')
- ->willReturn($this->dependencyArray[0]);
- $this->attribute->expects($this->at(3))->method('getId')
- ->willReturn(self::ATTRIBUTE_ID);
- $this->attribute->expects($this->at(4))->method('getData')
- ->with('swatch/value')
- ->willReturn([self::STORE_ID => [self::OPTION_ID => null]]);
- $this->attribute->expects($this->at(5))->method('getData')
- ->with('option/delete/' . self::OPTION_ID)
- ->willReturn(false);
-
- $this->swatchFactory->expects($this->exactly(1))->method('create')
- ->willReturn($this->swatch);
- $this->swatchHelper->expects($this->exactly(2))->method('isSwatchAttribute')
- ->with($this->attribute)
- ->willReturn(true);
- $this->swatchHelper->expects($this->once())->method('isVisualSwatch')
- ->with($this->attribute)
- ->willReturn(false);
- $this->swatchHelper->expects($this->once())->method('isTextSwatch')
- ->with($this->attribute)
- ->willReturn(true);
-
$this->eavAttribute->afterAfterSave($this->attribute);
}
+ /**
+ * Test afterSave plugin on empty swatch value
+ */
public function testAfterAfterSaveNotSwatchAttribute()
{
- $this->abstractSource->expects($this->once())->method('getAllOptions')
- ->willReturn($this->allOptions);
-
- $this->swatch->expects($this->once())->method('getId')
- ->willReturn(1);
- $this->swatch->expects($this->once())->method('save');
- $this->swatch->expects($this->once())->method('isDeleted')
- ->with(false);
- $this->swatch->expects($this->exactly(2))->method('setData')
- ->withConsecutive(
- ['type', Swatch::SWATCH_TYPE_TEXTUAL],
- ['value', null]
- );
+ $options = self::TEXT_SWATCH_OPTIONS;
+ $options['value'][self::OPTION_1_ID][self::ADMIN_STORE_ID] = null;
+ $options['value'][self::OPTION_1_ID][self::DEFAULT_STORE_ID] = null;
+ $options['value'][self::NEW_OPTION_KEY][self::ADMIN_STORE_ID] = null;
+ $options['value'][self::NEW_OPTION_KEY][self::DEFAULT_STORE_ID] = null;
+ $this->attribute->addData(
+ [
+ 'defaulttext' => self::ATTRIBUTE_DEFAULT_VALUE,
+ 'optiontext' => self::TEXT_ATTRIBUTE_OPTIONS,
+ 'swatchtext' => $options,
+ ]
+ );
+
+ $this->attribute->setData(Swatch::SWATCH_INPUT_TYPE_KEY, Swatch::SWATCH_INPUT_TYPE_TEXT);
+ $this->eavAttribute->beforeBeforeSave($this->attribute);
- $this->collection->expects($this->exactly(2))->method('addFieldToFilter')
+ $this->abstractSource->expects($this->once())
+ ->method('getAllOptions')
+ ->willReturn(self::TEXT_SAVED_OPTIONS);
+
+ $this->resource->expects($this->once())
+ ->method('saveDefaultSwatchOption')
+ ->with(self::ATTRIBUTE_ID, self::OPTION_2_ID);
+
+ $this->collection->expects($this->exactly(8))
+ ->method('addFieldToFilter')
->withConsecutive(
- ['option_id', self::OPTION_ID],
- ['store_id', self::OPTION_ID]
- )->willReturnSelf();
- $this->collection->expects($this->once())->method('getFirstItem')
- ->willReturn($this->swatch);
- $this->collectionFactory->expects($this->once())->method('create')
- ->willReturn($this->collection);
+ ['option_id', self::OPTION_1_ID],
+ ['store_id', self::ADMIN_STORE_ID],
+ ['option_id', self::OPTION_1_ID],
+ ['store_id', self::DEFAULT_STORE_ID],
+ ['option_id', self::OPTION_2_ID],
+ ['store_id', self::ADMIN_STORE_ID],
+ ['option_id', self::OPTION_2_ID],
+ ['store_id', self::DEFAULT_STORE_ID]
+ )
+ ->willReturnSelf();
- $this->attribute->expects($this->at(0))->method('getData')
- ->with('option')
- ->willReturn($this->optionIds);
- $this->attribute->expects($this->at(1))->method('getSource')
- ->willReturn($this->abstractSource);
- $this->attribute->expects($this->at(2))->method('getData')
- ->with('swatch/value')
- ->willReturn([self::STORE_ID => [self::OPTION_ID => null]]);
- $this->attribute->expects($this->at(3))->method('getData')
- ->with('option/delete/' . self::OPTION_ID)
- ->willReturn(false);
-
- $this->swatchHelper->expects($this->exactly(2))->method('isSwatchAttribute')
- ->with($this->attribute)
- ->will($this->onConsecutiveCalls(true, false));
- $this->swatchHelper->expects($this->once())->method('isVisualSwatch')
- ->with($this->attribute)
- ->willReturn(false);
- $this->swatchHelper->expects($this->once())->method('isTextSwatch')
- ->with($this->attribute)
- ->willReturn(true);
+ $this->collection->expects($this->exactly(4))
+ ->method('getFirstItem')
+ ->willReturnOnConsecutiveCalls(
+ $this->createSwatchMock(
+ Swatch::SWATCH_TYPE_TEXTUAL,
+ null,
+ 1
+ ),
+ $this->createSwatchMock(
+ Swatch::SWATCH_TYPE_TEXTUAL,
+ null,
+ 1
+ ),
+ $this->createSwatchMock(
+ Swatch::SWATCH_TYPE_TEXTUAL,
+ null,
+ null,
+ self::OPTION_2_ID,
+ self::ADMIN_STORE_ID
+ ),
+ $this->createSwatchMock(
+ Swatch::SWATCH_TYPE_TEXTUAL,
+ null,
+ null,
+ self::OPTION_2_ID,
+ self::DEFAULT_STORE_ID
+ )
+ );
+ $this->collectionFactory->expects($this->exactly(4))
+ ->method('create')
+ ->willReturn($this->collection);
$this->eavAttribute->afterAfterSave($this->attribute);
}
+
+ /**
+ * Create configured mock for swatch model
+ *
+ * @param string $type
+ * @param string|null $value
+ * @param int|null $id
+ * @param int|null $optionId
+ * @param int|null $storeId
+ * @return MockObject
+ */
+ private function createSwatchMock(
+ string $type,
+ ?string $value,
+ ?int $id = null,
+ ?int $optionId = null,
+ ?int $storeId = null
+ ) {
+ $swatch = $this->createMock(Swatch::class);
+ $swatch->expects($this->any())
+ ->method('getId')
+ ->willReturn($id);
+ $swatch->expects($this->any())
+ ->method('getResource')
+ ->willReturn($this->resource);
+ $swatch->expects($this->once())
+ ->method('save');
+ if ($id) {
+ $swatch->expects($this->exactly(2))
+ ->method('setData')
+ ->withConsecutive(
+ ['type', $type],
+ ['value', $value]
+ );
+ } else {
+ $swatch->expects($this->exactly(4))
+ ->method('setData')
+ ->withConsecutive(
+ ['option_id', $optionId],
+ ['store_id', $storeId],
+ ['type', $type],
+ ['value', $value]
+ );
+ }
+ return $swatch;
+ }
}
diff --git a/app/code/Magento/Swatches/ViewModel/Product/Renderer/Configurable.php b/app/code/Magento/Swatches/ViewModel/Product/Renderer/Configurable.php
new file mode 100644
index 0000000000000..849d79cc58d92
--- /dev/null
+++ b/app/code/Magento/Swatches/ViewModel/Product/Renderer/Configurable.php
@@ -0,0 +1,50 @@
+scopeConfig = $scopeConfig;
+ }
+
+ /**
+ * Get config if swatch tooltips should be rendered.
+ *
+ * @return string
+ */
+ public function getShowSwatchTooltip()
+ {
+ return $this->scopeConfig->getValue(
+ self::XML_PATH_SHOW_SWATCH_TOOLTIP,
+ ScopeInterface::SCOPE_STORE
+ );
+ }
+}
diff --git a/app/code/Magento/Swatches/etc/adminhtml/system.xml b/app/code/Magento/Swatches/etc/adminhtml/system.xml
index 2cf40ae83cc3b..6fbf110fadcd3 100644
--- a/app/code/Magento/Swatches/etc/adminhtml/system.xml
+++ b/app/code/Magento/Swatches/etc/adminhtml/system.xml
@@ -17,6 +17,10 @@
Show Swatches in Product List
Magento\Config\Model\Config\Source\Yesno
+
+ Show Swatch Tooltip
+ Magento\Config\Model\Config\Source\Yesno
+
diff --git a/app/code/Magento/Swatches/etc/config.xml b/app/code/Magento/Swatches/etc/config.xml
index 65b36558c2796..4140acc4974d6 100644
--- a/app/code/Magento/Swatches/etc/config.xml
+++ b/app/code/Magento/Swatches/etc/config.xml
@@ -11,6 +11,7 @@
16
1
+ 1
diff --git a/app/code/Magento/Swatches/etc/di.xml b/app/code/Magento/Swatches/etc/di.xml
index 5292bfafb6a0f..585cef924e928 100644
--- a/app/code/Magento/Swatches/etc/di.xml
+++ b/app/code/Magento/Swatches/etc/di.xml
@@ -81,4 +81,7 @@
+
+
+
diff --git a/app/code/Magento/Swatches/view/frontend/layout/catalog_category_view.xml b/app/code/Magento/Swatches/view/frontend/layout/catalog_category_view.xml
index c2dc36e83950c..86031a189d798 100644
--- a/app/code/Magento/Swatches/view/frontend/layout/catalog_category_view.xml
+++ b/app/code/Magento/Swatches/view/frontend/layout/catalog_category_view.xml
@@ -5,10 +5,19 @@
* See COPYING.txt for license details.
*/
-->
-
+
-
+
+
+ Magento\Swatches\ViewModel\Product\Renderer\Configurable
+
+
diff --git a/app/code/Magento/Swatches/view/frontend/layout/catalog_product_view_type_configurable.xml b/app/code/Magento/Swatches/view/frontend/layout/catalog_product_view_type_configurable.xml
index 6188f3957a11d..98346d6ae7e67 100644
--- a/app/code/Magento/Swatches/view/frontend/layout/catalog_product_view_type_configurable.xml
+++ b/app/code/Magento/Swatches/view/frontend/layout/catalog_product_view_type_configurable.xml
@@ -5,11 +5,18 @@
* See COPYING.txt for license details.
*/
-->
-
+
-
+
+
+ Magento\Swatches\ViewModel\Product\Renderer\Configurable
+
+
diff --git a/app/code/Magento/Swatches/view/frontend/layout/catalog_widget_product_list.xml b/app/code/Magento/Swatches/view/frontend/layout/catalog_widget_product_list.xml
index 91798cbd9947f..ce31f588c6c8c 100644
--- a/app/code/Magento/Swatches/view/frontend/layout/catalog_widget_product_list.xml
+++ b/app/code/Magento/Swatches/view/frontend/layout/catalog_widget_product_list.xml
@@ -3,10 +3,19 @@
~ See COPYING.txt for license details.
-->
-
+
-
+
+
+ Magento\Swatches\ViewModel\Product\Renderer\Configurable
+
+
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_advanced_result.xml b/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_advanced_result.xml
index c2dc36e83950c..86031a189d798 100644
--- a/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_advanced_result.xml
+++ b/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_advanced_result.xml
@@ -5,10 +5,19 @@
* See COPYING.txt for license details.
*/
-->
-
+
-
+
+
+ Magento\Swatches\ViewModel\Product\Renderer\Configurable
+
+
diff --git a/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_result_index.xml b/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_result_index.xml
index 9285d34efcd4c..86031a189d798 100644
--- a/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_result_index.xml
+++ b/app/code/Magento/Swatches/view/frontend/layout/catalogsearch_result_index.xml
@@ -5,10 +5,19 @@
* See COPYING.txt for license details.
*/
-->
-
+
-
+
+
+ Magento\Swatches\ViewModel\Product\Renderer\Configurable
+
+
diff --git a/app/code/Magento/Swatches/view/frontend/layout/wishlist_index_configure_type_configurable.xml b/app/code/Magento/Swatches/view/frontend/layout/wishlist_index_configure_type_configurable.xml
index 9982eb98d84da..c8159f1a43fe3 100644
--- a/app/code/Magento/Swatches/view/frontend/layout/wishlist_index_configure_type_configurable.xml
+++ b/app/code/Magento/Swatches/view/frontend/layout/wishlist_index_configure_type_configurable.xml
@@ -5,11 +5,18 @@
* See COPYING.txt for license details.
*/
-->
-
+
-
+
+
+ Magento\Swatches\ViewModel\Product\Renderer\Configurable
+
+
diff --git a/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml b/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml
index 777277a15d8cd..5838ba9625c6a 100644
--- a/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml
+++ b/app/code/Magento/Swatches/view/frontend/templates/product/listing/renderer.phtml
@@ -7,6 +7,8 @@
getProduct()->getId();
+/** @var \Magento\Swatches\ViewModel\Product\Renderer\Configurable $configurableViewModel */
+$configurableViewModel = $block->getConfigurableViewModel()
?>
@@ -22,7 +24,8 @@ $productId = $block->getProduct()->getId();
"jsonConfig": = /* @noEscape */ $block->getJsonConfig() ?>,
"jsonSwatchConfig": = /* @noEscape */ $block->getJsonSwatchConfig() ?>,
"mediaCallback": "= $block->escapeJs($block->escapeUrl($block->getMediaCallback())) ?>",
- "jsonSwatchImageSizeConfig": = /* @noEscape */ $block->getJsonSwatchSizeConfig() ?>
+ "jsonSwatchImageSizeConfig": = /* @noEscape */ $block->getJsonSwatchSizeConfig() ?>,
+ "showTooltip": = $block->escapeJs($configurableViewModel->getShowSwatchTooltip()) ?>
}
}
}
@@ -39,4 +42,4 @@ $productId = $block->getProduct()->getId();
}
}
}
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Swatches/view/frontend/templates/product/view/renderer.phtml b/app/code/Magento/Swatches/view/frontend/templates/product/view/renderer.phtml
index c85a6908413b5..bfabd5f3ab38f 100644
--- a/app/code/Magento/Swatches/view/frontend/templates/product/view/renderer.phtml
+++ b/app/code/Magento/Swatches/view/frontend/templates/product/view/renderer.phtml
@@ -4,7 +4,11 @@
* See COPYING.txt for license details.
*/
?>
-
+getConfigurableViewModel()
+?>