diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 615222321bca0..ec7ddb4085f24 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -15,7 +15,7 @@ For more detailed information on contribution please read our [beginners guide](
## Contribution requirements
-1. Contributions must adhere to the [Magento coding standards](https://devdocs.magento.com/guides/v2.4/coding-standards/bk-coding-standards.html).
+1. Contributions must adhere to the [Magento coding standards](https://developer.adobe.com/commerce/php/coding-standards/).
2. Pull requests (PRs) must be accompanied by a meaningful description of their purpose. Comprehensive descriptions increase the chances of a pull request being merged quickly and without additional clarification requests.
3. Commits must be accompanied by meaningful commit messages. Please see the [Magento Pull Request Template](https://github.com/magento/magento2/blob/HEAD/.github/PULL_REQUEST_TEMPLATE.md) for more information.
4. PRs which include bug fixes must be accompanied with a step-by-step description of how to reproduce the bug.
diff --git a/README.md b/README.md
index 55af19302871e..a02a955a9ebbe 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ However, for those who need a full-featured eCommerce solution, we recommend [Ad
## Contribute
-Our [Community](https://opensource.magento.com/) is large and diverse, and our project is enormous. As a contributor, you have countless opportunities to impact product development and delivery by introducing new features or improving existing ones, enhancing test coverage, updating documentation for [developers](https://devdocs.magento.com/) and [end-users](https://docs.magento.com/user-guide/), catching and fixing code bugs, suggesting points for optimization, and sharing your great ideas.
+Our [Community](https://opensource.magento.com/) is large and diverse, and our project is enormous. As a contributor, you have countless opportunities to impact product development and delivery by introducing new features or improving existing ones, enhancing test coverage, updating documentation for [developers](https://developer.adobe.com/commerce/docs/) and [end-users](https://docs.magento.com/user-guide/), catching and fixing code bugs, suggesting points for optimization, and sharing your great ideas.
- [Contribute to the code](https://developer.adobe.com/commerce/contributor/guides/code-contributions/)
- [Report an issue](https://developer.adobe.com/commerce/contributor/guides/code-contributions/#report)
@@ -36,7 +36,7 @@ Our [Community](https://opensource.magento.com/) is large and diverse, and our p
### Maintainers
-We encourage experts from the Community to help us with GitHub routines such as accepting, merging, or rejecting pull requests and reviewing issues. Adobe has granted the Community Maintainers permission to accept, merge, and reject pull requests, as well as review issues. Thanks to invaluable input from the Community Maintainers team, we can significantly improve contribution quality and accelerate the time to deliver your updates to production.
+We encourage experts from the Community to help us with GitHub routines such as accepting, merging, or rejecting pull requests and reviewing issues. Adobe has granted the Community Maintainers permission to accept, merge, and reject pull requests, as well as review issues. Thanks to invaluable input from the Community Maintainers team, we can significantly improve contribution quality and accelerate the time to deliver your updates to production.
- [Learn more about the Maintainer role](https://developer.adobe.com/commerce/contributor/guides/maintainers/)
- [Maintainer's Handbook](https://developer.adobe.com/commerce/contributor/guides/maintainers/handbook/)
@@ -64,9 +64,9 @@ Stay up-to-date on the latest security news and patches by signing up for [Secur
## Licensing
Each Magento source file included in this distribution is licensed under OSL 3.0 or the terms and conditions of the applicable ordering document between Licensee/Customer and Adobe (or Magento).
-
+
[Open Software License (OSL 3.0)](https://opensource.org/licenses/osl-3.0.php) – Please see [LICENSE.txt](LICENSE.txt) for the full text of the OSL 3.0 license.
-
+
Subject to Licensee's/Customer's payment of fees and compliance with the terms and conditions of the applicable ordering document between Licensee/Customer and Adobe (or Magento), the terms and conditions of the applicable ordering between Licensee/Customer and Adobe (or Magento) supersede the OSL 3.0 license for each source file.
## Communications
diff --git a/app/bootstrap.php b/app/bootstrap.php
index 8fbe2f770f53b..a7aea8094f816 100644
--- a/app/bootstrap.php
+++ b/app/bootstrap.php
@@ -17,12 +17,12 @@
if (!defined('PHP_VERSION_ID') || PHP_VERSION_ID < 80100) {
if (PHP_SAPI == 'cli') {
echo 'Magento supports PHP 8.1.0 or later. ' .
- 'Please read https://devdocs.magento.com/guides/v2.4/install-gde/system-requirements-tech.html';
+ 'Please read https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/system-requirements.html';
} else {
echo <<
Magento supports PHP 8.1.0 or later. Please read
-
+
Magento System Requirements .
HTML;
diff --git a/app/code/Magento/AdminAnalytics/README.md b/app/code/Magento/AdminAnalytics/README.md
index e905344031ad3..65a9e159f7aea 100644
--- a/app/code/Magento/AdminAnalytics/README.md
+++ b/app/code/Magento/AdminAnalytics/README.md
@@ -1 +1 @@
-The Magento\AdminAnalytics module gathers information about the features Magento administrators use. This information will be used to help improve the user experience on the Magento Admin.
\ No newline at end of file
+The Magento\AdminAnalytics module gathers information about the features Magento administrators use. This information will be used to help improve the user experience on the Magento Admin.
diff --git a/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationNavigateMenuTest.xml b/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationNavigateMenuTest.xml
index eb24b074bbffd..f3db9f1fd2636 100644
--- a/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationNavigateMenuTest.xml
+++ b/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationToolbarBlockAclTest.xml b/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationToolbarBlockAclTest.xml
index 1ab277b4f788a..53daccc815e50 100644
--- a/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationToolbarBlockAclTest.xml
+++ b/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationToolbarBlockAclTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/AdvancedSearch/Test/Mftf/Test/AdminAddSearchTermTest.xml b/app/code/Magento/AdvancedSearch/Test/Mftf/Test/AdminAddSearchTermTest.xml
index 8cdbaf781ed05..16a63d65d53c0 100644
--- a/app/code/Magento/AdvancedSearch/Test/Mftf/Test/AdminAddSearchTermTest.xml
+++ b/app/code/Magento/AdvancedSearch/Test/Mftf/Test/AdminAddSearchTermTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml
index b992c84814a29..66423b4210215 100644
--- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml
+++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml
index c5abff11e2849..97f36e4d66b81 100644
--- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml
+++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml
index 6090b5594a671..dd1d1e11cab7e 100644
--- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml
+++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml
index 9f07adb0223a9..6e88124f6f22c 100644
--- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml
+++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml
index 0df1a6809ccab..7b58b14681143 100644
--- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml
+++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationPermissionTest.xml
@@ -35,7 +35,7 @@
-
+
diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml
index badad120fdcca..a0df3f4229a7e 100644
--- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml
+++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/AsyncConfig/Test/Mftf/Suite/AsyncOperationsSuite.xml b/app/code/Magento/AsyncConfig/Test/Mftf/Suite/AsyncOperationsSuite.xml
new file mode 100644
index 0000000000000..c42fd98111e44
--- /dev/null
+++ b/app/code/Magento/AsyncConfig/Test/Mftf/Suite/AsyncOperationsSuite.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/AsyncConfig/Test/Mftf/Test/AsyncConfigurationTest.xml b/app/code/Magento/AsyncConfig/Test/Mftf/Test/AsyncConfigurationTest.xml
index c19e102e5d5e8..dd72cd0daa5e4 100644
--- a/app/code/Magento/AsyncConfig/Test/Mftf/Test/AsyncConfigurationTest.xml
+++ b/app/code/Magento/AsyncConfig/Test/Mftf/Test/AsyncConfigurationTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/AsynchronousOperations/Model/AccessValidator.php b/app/code/Magento/AsynchronousOperations/Model/AccessValidator.php
index 6f908a4c5b218..e7bd6d99cb3eb 100644
--- a/app/code/Magento/AsynchronousOperations/Model/AccessValidator.php
+++ b/app/code/Magento/AsynchronousOperations/Model/AccessValidator.php
@@ -6,13 +6,13 @@
namespace Magento\AsynchronousOperations\Model;
-/**
- * Class AccessValidator
- */
+use Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface;
+use Magento\Authorization\Model\UserContextInterface;
+
class AccessValidator
{
/**
- * @var \Magento\Authorization\Model\UserContextInterface
+ * @var UserContextInterface
*/
private $userContext;
@@ -27,13 +27,12 @@ class AccessValidator
private $bulkSummaryFactory;
/**
- * AccessValidator constructor.
- * @param \Magento\Authorization\Model\UserContextInterface $userContext
+ * @param UserContextInterface $userContext
* @param \Magento\Framework\EntityManager\EntityManager $entityManager
* @param \Magento\AsynchronousOperations\Api\Data\BulkSummaryInterfaceFactory $bulkSummaryFactory
*/
public function __construct(
- \Magento\Authorization\Model\UserContextInterface $userContext,
+ UserContextInterface $userContext,
\Magento\Framework\EntityManager\EntityManager $entityManager,
\Magento\AsynchronousOperations\Api\Data\BulkSummaryInterfaceFactory $bulkSummaryFactory
) {
@@ -50,11 +49,15 @@ public function __construct(
*/
public function isAllowed($bulkUuid)
{
- /** @var \Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface $bulkSummary */
+ /** @var BulkSummaryInterface $bulkSummary */
$bulkSummary = $this->entityManager->load(
$this->bulkSummaryFactory->create(),
$bulkUuid
);
+ if ((int) $bulkSummary->getUserType() === UserContextInterface::USER_TYPE_INTEGRATION) {
+ return true;
+ }
+
return ((int) $bulkSummary->getUserId()) === ((int) $this->userContext->getUserId());
}
}
diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkStatus/Options.php b/app/code/Magento/AsynchronousOperations/Model/BulkStatus/Options.php
index 47c317138ec64..58cc92e649ebf 100644
--- a/app/code/Magento/AsynchronousOperations/Model/BulkStatus/Options.php
+++ b/app/code/Magento/AsynchronousOperations/Model/BulkStatus/Options.php
@@ -7,32 +7,29 @@
use Magento\AsynchronousOperations\Api\Data\BulkSummaryInterface;
-/**
- * Class Options
- */
class Options implements \Magento\Framework\Data\OptionSourceInterface
{
/**
- * @return array
+ * @inheritDoc
*/
public function toOptionArray()
{
return [
[
'value' => BulkSummaryInterface::NOT_STARTED,
- 'label' => 'Not Started'
+ 'label' => __('Not Started')
],
[
'value' => BulkSummaryInterface::IN_PROGRESS,
- 'label' => 'In Progress'
+ 'label' => __('In Progress')
],
[
'value' => BulkSummaryInterface::FINISHED_SUCCESSFULLY,
- 'label' => 'Finished Successfully'
+ 'label' => __('Finished Successfully')
],
[
'value' => BulkSummaryInterface::FINISHED_WITH_FAILURE,
- 'label' => 'Finished with Failure'
+ 'label' => __('Finished with Failure')
]
];
}
diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkUserType/Options.php b/app/code/Magento/AsynchronousOperations/Model/BulkUserType/Options.php
new file mode 100644
index 0000000000000..92dd301608c18
--- /dev/null
+++ b/app/code/Magento/AsynchronousOperations/Model/BulkUserType/Options.php
@@ -0,0 +1,31 @@
+ UserContextInterface::USER_TYPE_ADMIN,
+ 'label' => __('Admin user')
+ ],
+ [
+ 'value' => UserContextInterface::USER_TYPE_INTEGRATION,
+ 'label' => __('Integration')
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResult.php b/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResult.php
index 5f2fbd9ea8b11..3fbd93344792d 100644
--- a/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResult.php
+++ b/app/code/Magento/AsynchronousOperations/Ui/Component/DataProvider/SearchResult.php
@@ -8,15 +8,13 @@
use Magento\Framework\Data\Collection\Db\FetchStrategyInterface as FetchStrategy;
use Magento\Framework\Data\Collection\EntityFactoryInterface as EntityFactory;
use Magento\Framework\Event\ManagerInterface as EventManager;
+use Magento\Framework\Model\ResourceModel\AbstractResource;
use Psr\Log\LoggerInterface as Logger;
use Magento\Authorization\Model\UserContextInterface;
use Magento\Framework\Bulk\BulkSummaryInterface;
use Magento\AsynchronousOperations\Model\StatusMapper;
use Magento\AsynchronousOperations\Model\BulkStatus\CalculatedStatusSql;
-/**
- * Class SearchResult
- */
class SearchResult extends \Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult
{
/**
@@ -40,7 +38,6 @@ class SearchResult extends \Magento\Framework\View\Element\UiComponent\DataProvi
private $calculatedStatusSql;
/**
- * SearchResult constructor.
* @param EntityFactory $entityFactory
* @param Logger $logger
* @param FetchStrategy $fetchStrategy
@@ -49,7 +46,7 @@ class SearchResult extends \Magento\Framework\View\Element\UiComponent\DataProvi
* @param StatusMapper $statusMapper
* @param CalculatedStatusSql $calculatedStatusSql
* @param string $mainTable
- * @param null $resourceModel
+ * @param AbstractResource $resourceModel
* @param string $identifierName
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
@@ -80,7 +77,7 @@ public function __construct(
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
protected function _initSelect()
{
@@ -93,12 +90,18 @@ protected function _initSelect()
)->where(
'user_id=?',
$this->userContext->getUserId()
+ )->where(
+ 'user_type=?',
+ UserContextInterface::USER_TYPE_ADMIN
+ )->orWhere(
+ 'user_type=?',
+ UserContextInterface::USER_TYPE_INTEGRATION
);
return $this;
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
protected function _afterLoad()
{
@@ -110,7 +113,7 @@ protected function _afterLoad()
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function addFieldToFilter($field, $condition = null)
{
@@ -133,7 +136,7 @@ public function addFieldToFilter($field, $condition = null)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function getSelectCountSql()
{
diff --git a/app/code/Magento/AsynchronousOperations/i18n/en_US.csv b/app/code/Magento/AsynchronousOperations/i18n/en_US.csv
index 44cc0a0ab7754..8a2fc7f774a25 100644
--- a/app/code/Magento/AsynchronousOperations/i18n/en_US.csv
+++ b/app/code/Magento/AsynchronousOperations/i18n/en_US.csv
@@ -33,3 +33,10 @@ Error,Error
"Dismiss All Completed Tasks","Dismiss All Completed Tasks"
"Action Details - #","Action Details - #"
"Number of Records Affected","Number of Records Affected"
+"User Type","User Type"
+"Admin user","Admin user"
+"Integration","Integration"
+"Not Started","Not Started"
+"In Progress","In Progress"
+"Finished Successfully","Finished Successfully"
+"Finished with Failure","Finished with Failure"
diff --git a/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/bulk_listing.xml b/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/bulk_listing.xml
index 87dc0525eb1c0..981e7ae980102 100644
--- a/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/bulk_listing.xml
+++ b/app/code/Magento/AsynchronousOperations/view/adminhtml/ui_component/bulk_listing.xml
@@ -81,6 +81,14 @@
Description of Operation
+
+
+ select
+
+ select
+ User Type
+
+
select
diff --git a/app/code/Magento/Authorization/Model/CompositeUserContext.php b/app/code/Magento/Authorization/Model/CompositeUserContext.php
index 149c33f861b35..1ad01a96af20d 100644
--- a/app/code/Magento/Authorization/Model/CompositeUserContext.php
+++ b/app/code/Magento/Authorization/Model/CompositeUserContext.php
@@ -7,6 +7,7 @@
namespace Magento\Authorization\Model;
use Magento\Framework\ObjectManager\Helper\Composite as CompositeHelper;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* User context.
@@ -17,7 +18,7 @@
* @api
* @since 100.0.2
*/
-class CompositeUserContext implements \Magento\Authorization\Model\UserContextInterface
+class CompositeUserContext implements \Magento\Authorization\Model\UserContextInterface, ResetAfterRequestInterface
{
/**
* @var UserContextInterface[]
@@ -92,4 +93,12 @@ protected function getUserContext()
}
return $this->chosenUserContext;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->chosenUserContext = null;
+ }
}
diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php
index def5088e89326..07657d373c95a 100644
--- a/app/code/Magento/AwsS3/Driver/AwsS3.php
+++ b/app/code/Magento/AwsS3/Driver/AwsS3.php
@@ -257,7 +257,7 @@ public function deleteDirectory($path): bool
/**
* @inheritDoc
*/
- public function filePutContents($path, $content, $mode = null): int
+ public function filePutContents($path, $content, $mode = null): bool|int
{
$path = $this->normalizeRelativePath($path, true);
$config = self::CONFIG;
@@ -272,10 +272,11 @@ public function filePutContents($path, $content, $mode = null): int
try {
$this->adapter->write($path, $content, new Config($config));
- return $this->adapter->fileSize($path)->fileSize();
+ return ($this->adapter->fileSize($path)->fileSize() !== null)??true;
+
} catch (FlysystemFilesystemException | UnableToRetrieveMetadata $e) {
$this->logger->error($e->getMessage());
- return 0;
+ return false;
}
}
diff --git a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php
index 66c95e97ace0c..b1cec93ed8f7e 100644
--- a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php
+++ b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php
@@ -54,6 +54,11 @@ class AwsS3Factory implements DriverFactoryInterface
*/
private $cachePrefix;
+ /**
+ * @var CachedCredentialsProvider
+ */
+ private $cachedCredentialsProvider;
+
/**
* @param ObjectManagerInterface $objectManager
* @param Config $config
@@ -61,6 +66,7 @@ class AwsS3Factory implements DriverFactoryInterface
* @param CacheInterfaceFactory $cacheInterfaceFactory
* @param CachedAdapterInterfaceFactory $cachedAdapterInterfaceFactory
* @param string|null $cachePrefix
+ * @param CachedCredentialsProvider|null $cachedCredentialsProvider
*/
public function __construct(
ObjectManagerInterface $objectManager,
@@ -68,7 +74,8 @@ public function __construct(
MetadataProviderInterfaceFactory $metadataProviderFactory,
CacheInterfaceFactory $cacheInterfaceFactory,
CachedAdapterInterfaceFactory $cachedAdapterInterfaceFactory,
- string $cachePrefix = null
+ string $cachePrefix = null,
+ ?CachedCredentialsProvider $cachedCredentialsProvider = null,
) {
$this->objectManager = $objectManager;
$this->config = $config;
@@ -76,6 +83,8 @@ public function __construct(
$this->cacheInterfaceFactory = $cacheInterfaceFactory;
$this->cachedAdapterInterfaceFactory = $cachedAdapterInterfaceFactory;
$this->cachePrefix = $cachePrefix;
+ $this->cachedCredentialsProvider = $cachedCredentialsProvider ??
+ $this->objectManager->get(CachedCredentialsProvider::class);
}
/**
@@ -94,18 +103,19 @@ public function create(): RemoteDriverInterface
}
/**
- * @inheritDoc
+ * Prepare config for S3Client
+ *
+ * @param array $config
+ * @return array
+ * @throws DriverException
*/
- public function createConfigured(
- array $config,
- string $prefix,
- string $cacheAdapter = '',
- array $cacheConfig = []
- ): RemoteDriverInterface {
+ private function prepareConfig(array $config)
+ {
$config['version'] = 'latest';
if (empty($config['credentials']['key']) || empty($config['credentials']['secret'])) {
- unset($config['credentials']);
+ //Access keys were not provided; request token from AWS config (local or EC2) and cache result
+ $config['credentials'] = $this->cachedCredentialsProvider->get();
}
if (empty($config['bucket']) || empty($config['region'])) {
@@ -120,6 +130,19 @@ public function createConfigured(
$config['use_path_style_endpoint'] = boolval($config['path_style']);
}
+ return $config;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function createConfigured(
+ array $config,
+ string $prefix,
+ string $cacheAdapter = '',
+ array $cacheConfig = []
+ ): RemoteDriverInterface {
+ $config = $this->prepareConfig($config);
$client = new S3Client($config);
$adapter = new AwsS3V3Adapter($client, $config['bucket'], $prefix);
$cache = $this->cacheInterfaceFactory->create(
diff --git a/app/code/Magento/AwsS3/Driver/CachedCredentialsProvider.php b/app/code/Magento/AwsS3/Driver/CachedCredentialsProvider.php
new file mode 100644
index 0000000000000..0137284358bd4
--- /dev/null
+++ b/app/code/Magento/AwsS3/Driver/CachedCredentialsProvider.php
@@ -0,0 +1,42 @@
+magentoCacheAdapter = $magentoCacheAdapter;
+ }
+
+ /**
+ * Provides cache mechanism to retrieve and store AWS credentials
+ *
+ * @return callable
+ */
+ public function get()
+ {
+ //phpcs:ignore Magento2.Functions.DiscouragedFunction
+ return call_user_func(
+ [CredentialProvider::class, 'cache'],
+ //phpcs:ignore Magento2.Functions.DiscouragedFunction
+ call_user_func([CredentialProvider::class, 'defaultProvider']),
+ $this->magentoCacheAdapter
+ );
+ }
+}
diff --git a/app/code/Magento/AwsS3/Driver/CredentialsCache.php b/app/code/Magento/AwsS3/Driver/CredentialsCache.php
new file mode 100644
index 0000000000000..337e9d2a2acff
--- /dev/null
+++ b/app/code/Magento/AwsS3/Driver/CredentialsCache.php
@@ -0,0 +1,82 @@
+magentoCache = $magentoCache;
+ $this->credentialsFactory = $credentialsFactory;
+ $this->json = $json;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function get($key)
+ {
+ $value = $this->magentoCache->load($key);
+
+ if (!is_string($value)) {
+ return null;
+ }
+
+ $result = $this->json->unserialize($value);
+ try {
+ return $this->credentialsFactory->create($result);
+ } catch (\Exception $e) {
+ return $result;
+ }
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function set($key, $value, $ttl = 0)
+ {
+ if (method_exists($value, 'toArray')) {
+ $value = $value->toArray();
+ }
+ $this->magentoCache->save($this->json->serialize($value), $key, [], $ttl);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function remove($key)
+ {
+ $this->magentoCache->remove($key);
+ }
+}
diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AdminAwsS3SyncZeroByteFilesTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AdminAwsS3SyncZeroByteFilesTest.xml
new file mode 100644
index 0000000000000..8ce9e8d72db1a
--- /dev/null
+++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AdminAwsS3SyncZeroByteFilesTest.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ dev/tests/acceptance/tests/_data/empty.jpg
+ pub/media/empty.jpg
+
+
+
+
+
+
+ pub/media/empty.jpg
+
+
+
+ pub/media/empty.jpg
+
+
+
+
+
+
+ Uploading media files to remote storage.\n- empty.jpg\nEnd of upload.
+ $syncRemoteStorage
+
+
+
+
diff --git a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3FactoryTest.php b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3FactoryTest.php
new file mode 100644
index 0000000000000..48ff824528d26
--- /dev/null
+++ b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3FactoryTest.php
@@ -0,0 +1,130 @@
+objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class);
+ $this->remoteStorageConfigMock = $this->createMock(Config::class);
+ $this->metadataFactoryMock = $this->createMock(MetadataProviderInterfaceFactory::class);
+ $this->remoteStorageCacheMock = $this->createMock(CacheInterfaceFactory::class);
+ $this->remoteCacheAdapterMock = $this->createMock(CachedAdapterInterfaceFactory::class);
+ $this->cachedCredsProviderMock = $this->createMock(CachedCredentialsProvider::class);
+
+ $this->factory = new AwsS3Factory(
+ $this->objectManagerMock,
+ $this->remoteStorageConfigMock,
+ $this->metadataFactoryMock,
+ $this->remoteStorageCacheMock,
+ $this->remoteCacheAdapterMock,
+ $this->cachePrefix,
+ $this->cachedCredsProviderMock
+ );
+ }
+
+ /**
+ * If no credentials in magento config, credentials retrieved from AWS should be cached
+ *
+ * @return void
+ */
+ public function testPrepareConfigUseCache()
+ {
+ $config = [
+ 'region' => 'us-west-1',
+ 'bucket' => 'someName',
+ 'credentials' => []
+ ];
+ $this->cachedCredsProviderMock->expects($this->once())->method('get');
+ $this->invokePrepareConfig($config);
+ }
+
+ public function testPrepareConfigMissingRequired()
+ {
+ $config = [
+ 'credentials' => [
+ 'key' => 'someKey',
+ 'secret' => 'verySecretKey'
+ ]
+ ];
+
+ $this->expectException('\Magento\RemoteStorage\Driver\DriverException');
+ $this->invokePrepareConfig($config);
+ }
+
+ /**
+ * Invoke private method via reflection
+ *
+ * @param array $config
+ * @return array
+ */
+ private function invokePrepareConfig(array $config): array
+ {
+ $method = new \ReflectionMethod(
+ AwsS3Factory::class,
+ 'prepareConfig'
+ );
+ $method->setAccessible(true);
+
+ return $method->invokeArgs($this->factory, [$config]);
+ }
+}
diff --git a/app/code/Magento/AwsS3/Test/Unit/Driver/CredentialsCacheTest.php b/app/code/Magento/AwsS3/Test/Unit/Driver/CredentialsCacheTest.php
new file mode 100644
index 0000000000000..f5c9b3138ea7f
--- /dev/null
+++ b/app/code/Magento/AwsS3/Test/Unit/Driver/CredentialsCacheTest.php
@@ -0,0 +1,70 @@
+cacheMock = $this->createMock(CacheInterface::class);
+ $this->credentialsFactory =
+ $this->getMockBuilder(CredentialsFactory::class)->disableOriginalConstructor()->getMock();
+ $this->jsonMock = $this->createMock(Json::class);
+ $this->adapter = new CredentialsCache($this->cacheMock, $this->credentialsFactory, $this->jsonMock);
+ }
+
+ public function testSet()
+ {
+ $this->jsonMock->expects($this->once())->method('serialize')->with('value')->willReturn('serialized');
+ $this->cacheMock->expects($this->once())->method('save')->with('serialized', 'key');
+ $this->adapter->set('key', 'value');
+ }
+
+ public function testGetEmpty()
+ {
+ $this->cacheMock->expects($this->once())->method('load')->with('key');
+ $actual = $this->adapter->get('key');
+ $this->assertEquals(null, $actual);
+ }
+
+ public function testRemove()
+ {
+ $this->cacheMock->expects($this->once())->method('remove');
+ $this->adapter->remove('key');
+ }
+}
diff --git a/app/code/Magento/Backend/Block/System/Store/Grid/Render/Group.php b/app/code/Magento/Backend/Block/System/Store/Grid/Render/Group.php
index 3d7154eb20f92..11cca3717ba20 100644
--- a/app/code/Magento/Backend/Block/System/Store/Grid/Render/Group.php
+++ b/app/code/Magento/Backend/Block/System/Store/Grid/Render/Group.php
@@ -5,6 +5,8 @@
*/
namespace Magento\Backend\Block\System\Store\Grid\Render;
+use Magento\Framework\DataObject;
+
/**
* Store render group
*
@@ -13,9 +15,9 @@
class Group extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer
{
/**
- * {@inheritdoc}
+ * @inheritDoc
*/
- public function render(\Magento\Framework\DataObject $row)
+ public function render(DataObject $row)
{
if (!$row->getData($this->getColumn()->getIndex())) {
return null;
@@ -28,6 +30,6 @@ public function render(\Magento\Framework\DataObject $row)
'">' .
$this->escapeHtml($row->getData($this->getColumn()->getIndex())) .
' '
- . '(' . __('Code') . ': ' . $row->getGroupCode() . ')';
+ . '(' . __('Code') . ': ' . $this->escapeHtml($row->getGroupCode()) . ')';
}
}
diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php
index 74117fbd666cc..66777b3968436 100644
--- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php
+++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php
@@ -109,25 +109,16 @@ public function getHtml()
' value="' .
$this->localeResolver->getLocale() .
'"/>';
- $scriptString = '
- require(["jquery", "mage/calendar"], function($){
- $("#' .
- $htmlId .
- '_range").dateRange({
- dateFormat: "' .
- $format .
- '",
- buttonText: "' . $this->escapeHtml(__('Date selector')) .
- '",
+ $scriptString = 'require(["jquery", "mage/calendar"], function($){
+ $("#' . $htmlId . '_range").dateRange({
+ dateFormat: "' . $format . '",
+ buttonText: "' . $this->escapeHtml(__('Date selector')) . '",
+ buttonImage: "' . $this->getViewFileUrl('Magento_Theme::calendar.png') . '",
from: {
- id: "' .
- $htmlId .
- '_from"
+ id: "' . $htmlId . '_from"
},
to: {
- id: "' .
- $htmlId .
- '_to"
+ id: "' . $htmlId . '_to"
}
})
});';
diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php
index a139d20191b57..c0c01c6201ce0 100644
--- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php
+++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php
@@ -17,7 +17,7 @@ class Datetime extends \Magento\Backend\Block\Widget\Grid\Column\Filter\Date
/**
* full day is 86400, we need 23 hours:59 minutes:59 seconds = 86399
*/
- const END_OF_DAY_IN_SECONDS = 86399;
+ public const END_OF_DAY_IN_SECONDS = 86399;
/**
* @inheritdoc
@@ -123,6 +123,7 @@ public function getHtml()
timeFormat: "' . $timeFormat . '",
showsTime: ' . ($this->getColumn()->getFilterTime() ? 'true' : 'false') . ',
buttonText: "' . $this->escapeHtml(__('Date selector')) . '",
+ buttonImage: "' . $this->getViewFileUrl('Magento_Theme::calendar.png') . '",
from: {
id: "' . $htmlId . '_from"
},
diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php
index 0da7e4db9b983..b7928eb027454 100644
--- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php
+++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php
@@ -15,6 +15,7 @@
*
* @api
* @deprecated 100.2.0 in favour of UI component implementation
+ * @see don't recommend this approach in favour of UI component implementation
* @since 100.0.2
*/
class Action extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Text
@@ -132,7 +133,7 @@ protected function _toLinkHtml($action, \Magento\Framework\DataObject $row)
}
if (empty($action['id'])) {
- $action['id'] = 'id' .$this->random->getRandomString(10);
+ $action['id'] = 'id' . $this->random->getRandomString(10);
}
$actionAttributes->setData($action);
$onclick = $actionAttributes->getData('onclick');
@@ -140,6 +141,8 @@ protected function _toLinkHtml($action, \Magento\Framework\DataObject $row)
$actionAttributes->unsetData(['onclick', 'style']);
$html = 'serialize() . '>' . $actionCaption . ' ';
if ($onclick) {
+ // phpcs:ignore Magento2.Functions.DiscouragedFunction
+ $onclick = html_entity_decode($onclick);
$html .= $this->secureHtmlRenderer->renderEventListenerAsTag('onclick', $onclick, "#{$action['id']}");
}
if ($style) {
diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Extended.php b/app/code/Magento/Backend/Block/Widget/Grid/Extended.php
index 22ca8a49c155b..5b180ba7f7de9 100644
--- a/app/code/Magento/Backend/Block/Widget/Grid/Extended.php
+++ b/app/code/Magento/Backend/Block/Widget/Grid/Extended.php
@@ -1067,10 +1067,11 @@ public function getCsv()
$data = [];
foreach ($this->getColumns() as $column) {
if (!$column->getIsSystem()) {
+ $exportField = (string)$column->getRowFieldExport($item);
$data[] = '"' . str_replace(
['"', '\\'],
['""', '\\\\'],
- $column->getRowFieldExport($item) ?: ''
+ $exportField ?: ''
) . '"';
}
}
diff --git a/app/code/Magento/Backend/Model/Auth/Session.php b/app/code/Magento/Backend/Model/Auth/Session.php
index e65caf13f2ea3..ec813472695d7 100644
--- a/app/code/Magento/Backend/Model/Auth/Session.php
+++ b/app/code/Magento/Backend/Model/Auth/Session.php
@@ -114,6 +114,16 @@ public function __construct(
);
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ parent::_resetState();
+ $this->_isFirstAfterLogin = null;
+ $this->acl = null;
+ }
+
/**
* Refresh ACL resources stored in session
*
diff --git a/app/code/Magento/Backend/Model/Session/Quote.php b/app/code/Magento/Backend/Model/Session/Quote.php
index ed0312874565c..b3067d3c98851 100644
--- a/app/code/Magento/Backend/Model/Session/Quote.php
+++ b/app/code/Magento/Backend/Model/Session/Quote.php
@@ -139,6 +139,17 @@ public function __construct(
}
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ parent::_resetState();
+ $this->_quote = null;
+ $this->_store = null;
+ $this->_order = null;
+ }
+
/**
* Retrieve quote model object
*
@@ -154,7 +165,7 @@ public function getQuote()
$this->_quote->setCustomerGroupId($customerGroupId);
$this->_quote->setIsActive(false);
$this->_quote->setStoreId($this->getStoreId());
-
+
$this->quoteRepository->save($this->_quote);
$this->setQuoteId($this->_quote->getId());
$this->_quote = $this->quoteRepository->get($this->getQuoteId(), [$this->getStoreId()]);
diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminLoginActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminLoginActionGroup.xml
index 3d596d248c42b..025255088d7e6 100644
--- a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminLoginActionGroup.xml
+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminLoginActionGroup.xml
@@ -14,7 +14,8 @@
-
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SecondaryGridActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SecondaryGridActionGroup.xml
index cd6eca91e9e30..b62a3d868d0d1 100644
--- a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SecondaryGridActionGroup.xml
+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SecondaryGridActionGroup.xml
@@ -27,8 +27,10 @@
+
+
-
+
diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetAdminAccountActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetAdminAccountActionGroup.xml
index 440c73bc73a91..371c8dfbb8bf7 100644
--- a/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetAdminAccountActionGroup.xml
+++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/SetAdminAccountActionGroup.xml
@@ -20,7 +20,7 @@
-
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml
index 5aaefc383f413..268ff07850f43 100644
--- a/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml
+++ b/app/code/Magento/Backend/Test/Mftf/Section/LocaleOptionsSection.xml
@@ -16,5 +16,10 @@
+
+
+
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml
index f7c5ae308c755..7bd1bf85a37fc 100644
--- a/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml
+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminContentScheduleNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminContentScheduleNavigateMenuTest.xml
index 88660b74cd6f9..1b1f1deada589 100644
--- a/app/code/Magento/Backend/Test/Mftf/Test/AdminContentScheduleNavigateMenuTest.xml
+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminContentScheduleNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml
index b4f44ea9e0a6e..1b80a1d897ccf 100644
--- a/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml
+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml
index eaf3fd240417d..4babc8a266b85 100644
--- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml
+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml
index f27d02c751945..a40e6f474c1ca 100644
--- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml
+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginFailedTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginFailedTest.xml
index 8c3ebd96f502e..61f8ee9461941 100644
--- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginFailedTest.xml
+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginFailedTest.xml
@@ -19,10 +19,11 @@
+
-
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulTest.xml
index 73b14bdc14151..915b00e9189d7 100644
--- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulTest.xml
+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulWithRewritesDisabledTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulWithRewritesDisabledTest.xml
index b19981f78df60..98c6c01053ffd 100644
--- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulWithRewritesDisabledTest.xml
+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginSuccessfulWithRewritesDisabledTest.xml
@@ -20,6 +20,7 @@
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginWithRestrictPermissionTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginWithRestrictPermissionTest.xml
index e5b92f61230b4..06a433e17ae6f 100644
--- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginWithRestrictPermissionTest.xml
+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginWithRestrictPermissionTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml
index 45a49f58788fc..5d4f1a4f31487 100644
--- a/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml
+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminPasswordResetSettingsTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminPasswordResetSettingsTest.xml
index c4cbfcfaa12b2..6ad97b44999b0 100644
--- a/app/code/Magento/Backend/Test/Mftf/Test/AdminPasswordResetSettingsTest.xml
+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminPasswordResetSettingsTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml
index d4ec0829604bb..133be0bd74bbd 100644
--- a/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml
+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminPrivacyPolicyTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml
index 664067a66d20e..1afd625167740 100644
--- a/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml
+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresAllStoresNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresAllStoresNavigateMenuTest.xml
index 22b45210fe6ba..d9aa8c2850c69 100644
--- a/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresAllStoresNavigateMenuTest.xml
+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresAllStoresNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresConfigurationNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresConfigurationNavigateMenuTest.xml
index b1b780190a699..d45171890e782 100644
--- a/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresConfigurationNavigateMenuTest.xml
+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminStoresConfigurationNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminSystemCacheManagementNavigateMenuTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminSystemCacheManagementNavigateMenuTest.xml
index d69ceeba29d18..452517b460652 100644
--- a/app/code/Magento/Backend/Test/Mftf/Test/AdminSystemCacheManagementNavigateMenuTest.xml
+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminSystemCacheManagementNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminUserLoginWithStoreCodeInUrlTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminUserLoginWithStoreCodeInUrlTest.xml
index 44c230e271a19..961e08b21efb9 100644
--- a/app/code/Magento/Backend/Test/Mftf/Test/AdminUserLoginWithStoreCodeInUrlTest.xml
+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminUserLoginWithStoreCodeInUrlTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/CustomerReorderSimpleProductTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/CustomerReorderSimpleProductTest.xml
index a6510c5d82717..b931d606a9da1 100644
--- a/app/code/Magento/Backend/Test/Mftf/Test/CustomerReorderSimpleProductTest.xml
+++ b/app/code/Magento/Backend/Test/Mftf/Test/CustomerReorderSimpleProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DateTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DateTest.php
index 575403824679f..0a387b31bf8e7 100644
--- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DateTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DateTest.php
@@ -10,18 +10,21 @@
use Magento\Backend\Block\Context;
use Magento\Backend\Block\Widget\Grid\Column;
use Magento\Backend\Block\Widget\Grid\Column\Filter\Date;
+use Magento\Framework\App\Request\Http;
use Magento\Framework\Escaper;
use Magento\Framework\Locale\ResolverInterface;
use Magento\Framework\Math\Random;
use Magento\Framework\Stdlib\DateTime\DateTimeFormatterInterface;
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
+use Magento\Framework\View\Asset\Repository;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
/**
* Class DateTest to test Magento\Backend\Block\Widget\Grid\Column\Filter\Date
*
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class DateTest extends TestCase
{
@@ -49,6 +52,16 @@ class DateTest extends TestCase
/** @var Context|MockObject */
private $contextMock;
+ /**
+ * @var Http|MockObject
+ */
+ private $request;
+
+ /**
+ * @var Repository|MockObject
+ */
+ private $repositoryMock;
+
protected function setUp(): void
{
$this->mathRandomMock = $this->getMockBuilder(Random::class)
@@ -88,6 +101,23 @@ protected function setUp(): void
$this->contextMock->expects($this->once())->method('getEscaper')->willReturn($this->escaperMock);
$this->contextMock->expects($this->once())->method('getLocaleDate')->willReturn($this->localeDateMock);
+ $this->request = $this->getMockBuilder(Http::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->contextMock->expects($this->once())
+ ->method('getRequest')
+ ->willReturn($this->request);
+
+ $this->repositoryMock = $this->getMockBuilder(Repository::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getUrlWithParams'])
+ ->getMock();
+
+ $this->contextMock->expects($this->once())
+ ->method('getAssetRepository')
+ ->willReturn($this->repositoryMock);
+
$objectManagerHelper = new ObjectManager($this);
$this->model = $objectManagerHelper->getObject(
Date::class,
@@ -116,6 +146,14 @@ public function testGetHtmlSuccessfulTimestamp()
'from' => $yesterday->getTimestamp(),
'to' => $tomorrow->getTimestamp()
];
+ $params = ['_secure' => false];
+ $fileId = 'Magento_Theme::calendar.png';
+ $fileUrl = 'file url';
+
+ $this->repositoryMock->expects($this->once())
+ ->method('getUrlWithParams')
+ ->with($fileId, $params)
+ ->willReturn($fileUrl);
$this->mathRandomMock->expects($this->any())->method('getUniqueHash')->willReturn($uniqueHash);
$this->columnMock->expects($this->once())->method('getHtmlId')->willReturn($id);
diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DatetimeTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DatetimeTest.php
index 3296680f43374..4b63e34cbc879 100644
--- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DatetimeTest.php
+++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/Column/Filter/DatetimeTest.php
@@ -10,17 +10,21 @@
use Magento\Backend\Block\Context;
use Magento\Backend\Block\Widget\Grid\Column;
use Magento\Backend\Block\Widget\Grid\Column\Filter\Datetime;
+use Magento\Framework\App\Request\Http;
use Magento\Framework\Escaper;
use Magento\Framework\Locale\ResolverInterface;
use Magento\Framework\Math\Random;
use Magento\Framework\Stdlib\DateTime\DateTimeFormatterInterface;
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
+use Magento\Framework\View\Asset\Repository;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
/**
* Class DateTimeTest to test Magento\Backend\Block\Widget\Grid\Column\Filter\Date
+ *
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class DatetimeTest extends TestCase
{
@@ -48,6 +52,16 @@ class DatetimeTest extends TestCase
/** @var Context|MockObject */
private $contextMock;
+ /**
+ * @var Http|MockObject
+ */
+ private $request;
+
+ /**
+ * @var Repository|MockObject
+ */
+ private $repositoryMock;
+
protected function setUp(): void
{
$this->mathRandomMock = $this->getMockBuilder(Random::class)
@@ -87,6 +101,23 @@ protected function setUp(): void
$this->contextMock->expects($this->once())->method('getEscaper')->willReturn($this->escaperMock);
$this->contextMock->expects($this->once())->method('getLocaleDate')->willReturn($this->localeDateMock);
+ $this->request = $this->getMockBuilder(Http::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->contextMock->expects($this->once())
+ ->method('getRequest')
+ ->willReturn($this->request);
+
+ $this->repositoryMock = $this->getMockBuilder(Repository::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getUrlWithParams'])
+ ->getMock();
+
+ $this->contextMock->expects($this->once())
+ ->method('getAssetRepository')
+ ->willReturn($this->repositoryMock);
+
$objectManagerHelper = new ObjectManager($this);
$this->model = $objectManagerHelper->getObject(
Datetime::class,
@@ -115,6 +146,14 @@ public function testGetHtmlSuccessfulTimestamp()
'from' => $yesterday->getTimestamp(),
'to' => $tomorrow->getTimestamp()
];
+ $params = ['_secure' => false];
+ $fileId = 'Magento_Theme::calendar.png';
+ $fileUrl = 'file url';
+
+ $this->repositoryMock->expects($this->once())
+ ->method('getUrlWithParams')
+ ->with($fileId, $params)
+ ->willReturn($fileUrl);
$this->mathRandomMock->expects($this->any())->method('getUniqueHash')->willReturn($uniqueHash);
$this->columnMock->expects($this->once())->method('getHtmlId')->willReturn($id);
diff --git a/app/code/Magento/Backend/etc/adminhtml/system.xml b/app/code/Magento/Backend/etc/adminhtml/system.xml
index 463976b58212f..1610ea9fde71f 100644
--- a/app/code/Magento/Backend/etc/adminhtml/system.xml
+++ b/app/code/Magento/Backend/etc/adminhtml/system.xml
@@ -349,9 +349,10 @@
smtp
-
+
Password
- Username
+ Password
+ Magento\Config\Model\Config\Backend\Encrypted
smtp
diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index/Download.php b/app/code/Magento/Backup/Controller/Adminhtml/Index/Download.php
index 864e5f4b37721..252ca89ae411b 100644
--- a/app/code/Magento/Backup/Controller/Adminhtml/Index/Download.php
+++ b/app/code/Magento/Backup/Controller/Adminhtml/Index/Download.php
@@ -6,9 +6,10 @@
*/
namespace Magento\Backup\Controller\Adminhtml\Index;
+use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\Filesystem\DirectoryList;
-class Download extends \Magento\Backup\Controller\Adminhtml\Index
+class Download extends \Magento\Backup\Controller\Adminhtml\Index implements HttpGetActionInterface
{
/**
* @var \Magento\Framework\Controller\Result\RawFactory
@@ -66,17 +67,12 @@ public function execute()
$fileName = $this->_objectManager->get(\Magento\Backup\Helper\Data::class)->generateBackupDownloadName($backup);
- $this->_fileFactory->create(
+ return $this->_fileFactory->create(
$fileName,
- null,
+ ['type' => 'filename', 'value' => $backup->getPath() . DIRECTORY_SEPARATOR . $backup->getFileName()],
DirectoryList::VAR_DIR,
'application/octet-stream',
$backup->getSize()
);
-
- /** @var \Magento\Framework\Controller\Result\Raw $resultRaw */
- $resultRaw = $this->resultRawFactory->create();
- $resultRaw->setContents($backup->output());
- return $resultRaw;
}
}
diff --git a/app/code/Magento/Backup/Model/ResourceModel/Db.php b/app/code/Magento/Backup/Model/ResourceModel/Db.php
index c38a7b3005e21..cf39406d54aeb 100644
--- a/app/code/Magento/Backup/Model/ResourceModel/Db.php
+++ b/app/code/Magento/Backup/Model/ResourceModel/Db.php
@@ -301,7 +301,7 @@ public function rollBackTransaction()
*/
public function runCommand($command)
{
- $this->connection->query($command);
+ $this->connection->multiQuery($command);
return $this;
}
}
diff --git a/app/code/Magento/Backup/Test/Unit/Controller/Adminhtml/Index/DownloadTest.php b/app/code/Magento/Backup/Test/Unit/Controller/Adminhtml/Index/DownloadTest.php
index 4ae20c711327f..b9c6b67cf1bc7 100644
--- a/app/code/Magento/Backup/Test/Unit/Controller/Adminhtml/Index/DownloadTest.php
+++ b/app/code/Magento/Backup/Test/Unit/Controller/Adminhtml/Index/DownloadTest.php
@@ -115,7 +115,7 @@ protected function setUp(): void
->getMock();
$this->backupModelMock = $this->getMockBuilder(Backup::class)
->disableOriginalConstructor()
- ->setMethods(['getTime', 'exists', 'getSize', 'output'])
+ ->setMethods(['getTime', 'exists', 'getSize', 'output', 'getPath', 'getFileName'])
->getMock();
$this->dataHelperMock = $this->getMockBuilder(Data::class)
->disableOriginalConstructor()
@@ -169,8 +169,13 @@ public function testExecuteBackupFound()
$type = 'db';
$filename = 'filename';
$size = 10;
- $output = 'test';
-
+ $path = 'testpath';
+ $this->backupModelMock->expects($this->atLeastOnce())
+ ->method('getPath')
+ ->willReturn($path);
+ $this->backupModelMock->expects($this->atLeastOnce())
+ ->method('getFileName')
+ ->willReturn($filename);
$this->backupModelMock->expects($this->atLeastOnce())
->method('getTime')
->willReturn($time);
@@ -180,9 +185,6 @@ public function testExecuteBackupFound()
$this->backupModelMock->expects($this->atLeastOnce())
->method('getSize')
->willReturn($size);
- $this->backupModelMock->expects($this->atLeastOnce())
- ->method('output')
- ->willReturn($output);
$this->requestMock->expects($this->any())
->method('getParam')
->willReturnMap(
@@ -206,20 +208,14 @@ public function testExecuteBackupFound()
$this->fileFactoryMock->expects($this->once())
->method('create')->with(
$filename,
- null,
+ ['type' => 'filename', 'value' => $path . '/' . $filename],
DirectoryList::VAR_DIR,
'application/octet-stream',
$size
)
->willReturn($this->responseMock);
- $this->resultRawMock->expects($this->once())
- ->method('setContents')
- ->with($output);
- $this->resultRawFactoryMock->expects($this->once())
- ->method('create')
- ->willReturn($this->resultRawMock);
- $this->assertSame($this->resultRawMock, $this->downloadController->execute());
+ $this->assertSame($this->responseMock, $this->downloadController->execute());
}
/**
diff --git a/app/code/Magento/Bundle/Api/ProductLinkManagementAddChildrenInterface.php b/app/code/Magento/Bundle/Api/ProductLinkManagementAddChildrenInterface.php
new file mode 100644
index 0000000000000..921f871da7265
--- /dev/null
+++ b/app/code/Magento/Bundle/Api/ProductLinkManagementAddChildrenInterface.php
@@ -0,0 +1,32 @@
+getData("bundle_option_qty/${optionId}") ?? [];
+ $preConfiguredQtys = $preConfiguredValues->getData("bundle_option_qty/{$optionId}") ?? [];
$selections = $options[$optionId]['selections'];
array_walk(
$selections,
@@ -423,4 +422,13 @@ function (&$selection, $selectionId) use ($preConfiguredQtys) {
return $options;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->selectedOptions = [];
+ $this->optionsPosition = [];
+ }
}
diff --git a/app/code/Magento/Bundle/Model/LinkManagement.php b/app/code/Magento/Bundle/Model/LinkManagement.php
index 9bc056a3e9a87..3569a026144b8 100644
--- a/app/code/Magento/Bundle/Model/LinkManagement.php
+++ b/app/code/Magento/Bundle/Model/LinkManagement.php
@@ -10,6 +10,7 @@
use Magento\Bundle\Api\Data\LinkInterface;
use Magento\Bundle\Api\Data\LinkInterfaceFactory;
use Magento\Bundle\Api\Data\OptionInterface;
+use Magento\Bundle\Api\ProductLinkManagementAddChildrenInterface;
use Magento\Bundle\Api\ProductLinkManagementInterface;
use Magento\Bundle\Model\Product\Type;
use Magento\Bundle\Model\ResourceModel\Bundle;
@@ -30,7 +31,7 @@
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
-class LinkManagement implements ProductLinkManagementInterface
+class LinkManagement implements ProductLinkManagementInterface, ProductLinkManagementAddChildrenInterface
{
/**
* @var ProductRepositoryInterface
@@ -189,6 +190,70 @@ public function saveChild(
return true;
}
+ /**
+ * Linked product processing
+ *
+ * @param LinkInterface $linkedProduct
+ * @param array $selections
+ * @param int $optionId
+ * @param ProductInterface $product
+ * @param string $linkField
+ * @param Bundle $resource
+ * @return int
+ * @throws CouldNotSaveException
+ * @throws InputException
+ * @throws NoSuchEntityException
+ */
+ private function processLinkedProduct(
+ LinkInterface $linkedProduct,
+ array $selections,
+ int $optionId,
+ ProductInterface $product,
+ string $linkField,
+ Bundle $resource
+ ): int {
+ $linkProductModel = $this->productRepository->get($linkedProduct->getSku());
+ if ($linkProductModel->isComposite()) {
+ throw new InputException(__('The bundle product can\'t contain another composite product.'));
+ }
+
+ if ($selections) {
+ foreach ($selections as $selection) {
+ if ($selection['option_id'] == $optionId &&
+ $selection['product_id'] == $linkProductModel->getEntityId() &&
+ $selection['parent_product_id'] == $product->getData($linkField)) {
+ if (!$product->getCopyFromView()) {
+ throw new CouldNotSaveException(
+ __(
+ 'Child with specified sku: "%1" already assigned to product: "%2"',
+ [$linkedProduct->getSku(), $product->getSku()]
+ )
+ );
+ }
+ }
+ }
+ }
+
+ $selectionModel = $this->bundleSelection->create();
+ $selectionModel = $this->mapProductLinkToBundleSelectionModel(
+ $selectionModel,
+ $linkedProduct,
+ $product,
+ (int)$linkProductModel->getEntityId()
+ );
+
+ $selectionModel->setOptionId($optionId);
+
+ try {
+ $selectionModel->save();
+ $resource->addProductRelation($product->getData($linkField), $linkProductModel->getEntityId());
+ } catch (\Exception $e) {
+ throw new CouldNotSaveException(__('Could not save child: "%1"', $e->getMessage()), $e);
+ }
+
+ return (int)$selectionModel->getId();
+ }
+
/**
* Fill selection model with product link data
*
@@ -196,12 +261,11 @@ public function saveChild(
* @param LinkInterface $productLink
* @param string $linkedProductId
* @param string $parentProductId
- *
* @return Selection
- *
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
- * @deprecated use mapProductLinkToBundleSelectionModel
+ * @deprecated
+ * @see mapProductLinkToBundleSelectionModel
*/
protected function mapProductLinkToSelectionModel(
Selection $selectionModel,
@@ -246,9 +310,9 @@ protected function mapProductLinkToSelectionModel(
* @param LinkInterface $productLink
* @param ProductInterface $parentProduct
* @param int $linkedProductId
- * @param string $linkField
* @return Selection
* @throws NoSuchEntityException
+ * @SuppressWarnings(PHPMD.NPathComplexity)
*/
private function mapProductLinkToBundleSelectionModel(
Selection $selectionModel,
@@ -290,8 +354,6 @@ private function mapProductLinkToBundleSelectionModel(
/**
* @inheritDoc
- *
- * @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function addChild(
ProductInterface $product,
@@ -325,49 +387,52 @@ public function addChild(
/* @var $resource Bundle */
$resource = $this->bundleFactory->create();
$selections = $resource->getSelectionsData($product->getData($linkField));
- /** @var Product $linkProductModel */
- $linkProductModel = $this->productRepository->get($linkedProduct->getSku());
- if ($linkProductModel->isComposite()) {
- throw new InputException(__('The bundle product can\'t contain another composite product.'));
- }
-
- if ($selections) {
- foreach ($selections as $selection) {
- if ($selection['option_id'] == $optionId &&
- $selection['product_id'] == $linkProductModel->getEntityId() &&
- $selection['parent_product_id'] == $product->getData($linkField)) {
- if (!$product->getCopyFromView()) {
- throw new CouldNotSaveException(
- __(
- 'Child with specified sku: "%1" already assigned to product: "%2"',
- [$linkedProduct->getSku(), $product->getSku()]
- )
- );
- }
-
- return $this->bundleSelection->create()->load($linkProductModel->getEntityId());
- }
- }
- }
-
- $selectionModel = $this->bundleSelection->create();
- $selectionModel = $this->mapProductLinkToBundleSelectionModel(
- $selectionModel,
+ return $this->processLinkedProduct(
$linkedProduct,
+ $selections,
+ (int)$optionId,
$product,
- (int)$linkProductModel->getEntityId()
+ $linkField,
+ $resource
);
+ }
- $selectionModel->setOptionId($optionId);
+ /**
+ * @inheritDoc
+ */
+ public function addChildren(
+ ProductInterface $product,
+ int $optionId,
+ array $linkedProducts
+ ) : void {
+ if ($product->getTypeId() != Product\Type::TYPE_BUNDLE) {
+ throw new InputException(
+ __('The product with the "%1" SKU isn\'t a bundle product.', $product->getSku())
+ );
+ }
- try {
- $selectionModel->save();
- $resource->addProductRelation($product->getData($linkField), $linkProductModel->getEntityId());
- } catch (\Exception $e) {
- throw new CouldNotSaveException(__('Could not save child: "%1"', $e->getMessage()), $e);
+ $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField();
+ $options = $this->optionCollection->create();
+ $options->setIdFilter($optionId);
+ $options->setProductLinkFilter($product->getData($linkField));
+ $existingOption = $options->getFirstItem();
+
+ if (!$existingOption->getId()) {
+ throw new InputException(
+ __(
+ 'Product with specified sku: "%1" does not contain option: "%2"',
+ [$product->getSku(), $optionId]
+ )
+ );
}
- return (int)$selectionModel->getId();
+ /* @var $resource Bundle */
+ $resource = $this->bundleFactory->create();
+ $selections = $resource->getSelectionsData($product->getData($linkField));
+
+ foreach ($linkedProducts as $linkedProduct) {
+ $this->processLinkedProduct($linkedProduct, $selections, $optionId, $product, $linkField, $resource);
+ }
}
/**
diff --git a/app/code/Magento/Bundle/Model/Option/SaveAction.php b/app/code/Magento/Bundle/Model/Option/SaveAction.php
index 0fe0f7d97ea07..2776f11db33f8 100644
--- a/app/code/Magento/Bundle/Model/Option/SaveAction.php
+++ b/app/code/Magento/Bundle/Model/Option/SaveAction.php
@@ -7,20 +7,26 @@
namespace Magento\Bundle\Model\Option;
+use Exception;
use Magento\Bundle\Api\Data\LinkInterface;
use Magento\Bundle\Api\Data\OptionInterface;
+use Magento\Bundle\Api\ProductLinkManagementInterface;
+use Magento\Bundle\Api\ProductLinkManagementAddChildrenInterface;
+use Magento\Bundle\Model\Product\Type;
use Magento\Bundle\Model\ResourceModel\Option;
+use Magento\Bundle\Model\ResourceModel\Option\Collection;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Framework\App\ObjectManager;
+use Magento\Framework\EntityManager\EntityMetadataInterface;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\Exception\CouldNotSaveException;
-use Magento\Bundle\Model\Product\Type;
-use Magento\Bundle\Api\ProductLinkManagementInterface;
+use Magento\Framework\Exception\InputException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Store\Model\StoreManagerInterface;
/**
* Encapsulates logic for saving a bundle option, including coalescing the parent product's data.
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class SaveAction
{
@@ -44,12 +50,18 @@ class SaveAction
*/
private $linkManagement;
+ /**
+ * @var ProductLinkManagementAddChildrenInterface
+ */
+ private $addChildren;
+
/**
* @param Option $optionResource
* @param MetadataPool $metadataPool
* @param Type $type
* @param ProductLinkManagementInterface $linkManagement
* @param StoreManagerInterface|null $storeManager
+ * @param ProductLinkManagementAddChildrenInterface|null $addChildren
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __construct(
@@ -57,41 +69,68 @@ public function __construct(
MetadataPool $metadataPool,
Type $type,
ProductLinkManagementInterface $linkManagement,
- ?StoreManagerInterface $storeManager = null
+ ?StoreManagerInterface $storeManager = null,
+ ?ProductLinkManagementAddChildrenInterface $addChildren = null
) {
$this->optionResource = $optionResource;
$this->metadataPool = $metadataPool;
$this->type = $type;
$this->linkManagement = $linkManagement;
+ $this->addChildren = $addChildren ?:
+ ObjectManager::getInstance()->get(ProductLinkManagementAddChildrenInterface::class);
}
/**
- * Manage the logic of saving a bundle option, including the coalescence of its parent product data.
+ * Bulk options save
*
* @param ProductInterface $bundleProduct
- * @param OptionInterface $option
- * @return OptionInterface
+ * @param OptionInterface[] $options
+ * @return void
* @throws CouldNotSaveException
- * @throws \Exception
+ * @throws NoSuchEntityException
+ * @throws InputException
*/
- public function save(ProductInterface $bundleProduct, OptionInterface $option)
+ public function saveBulk(ProductInterface $bundleProduct, array $options): void
{
$metadata = $this->metadataPool->getMetadata(ProductInterface::class);
+ $optionCollection = $this->type->getOptionsCollection($bundleProduct);
+
+ foreach ($options as $option) {
+ $this->saveOptionItem($bundleProduct, $option, $optionCollection, $metadata);
+ }
+
+ $bundleProduct->setIsRelationsChanged(true);
+ }
+
+ /**
+ * Process option save
+ *
+ * @param ProductInterface $bundleProduct
+ * @param OptionInterface $option
+ * @param Collection $optionCollection
+ * @param EntityMetadataInterface $metadata
+ * @return void
+ * @throws CouldNotSaveException
+ * @throws NoSuchEntityException
+ * @throws InputException
+ */
+ private function saveOptionItem(
+ ProductInterface $bundleProduct,
+ OptionInterface $option,
+ Collection $optionCollection,
+ EntityMetadataInterface $metadata
+ ) : void {
+ $linksToAdd = [];
$option->setStoreId($bundleProduct->getStoreId());
$parentId = $bundleProduct->getData($metadata->getLinkField());
$option->setParentId($parentId);
-
$optionId = $option->getOptionId();
- $linksToAdd = [];
- $optionCollection = $this->type->getOptionsCollection($bundleProduct);
/** @var \Magento\Bundle\Model\Option $existingOption */
$existingOption = $optionCollection->getItemById($option->getOptionId())
?? $optionCollection->getNewEmptyItem();
if (!$optionId || $existingOption->getParentId() != $parentId) {
- //If option ID is empty or existing option's parent ID is different
- //we'd need a new ID for the option.
$option->setOptionId(null);
$option->setDefaultTitle($option->getTitle());
if (is_array($option->getProductLinks())) {
@@ -110,7 +149,7 @@ public function save(ProductInterface $bundleProduct, OptionInterface $option)
try {
$this->optionResource->save($option);
- } catch (\Exception $e) {
+ } catch (Exception $e) {
throw new CouldNotSaveException(__("The option couldn't be saved."), $e);
}
@@ -118,8 +157,23 @@ public function save(ProductInterface $bundleProduct, OptionInterface $option)
foreach ($linksToAdd as $linkedProduct) {
$this->linkManagement->addChild($bundleProduct, $option->getOptionId(), $linkedProduct);
}
+ }
- $bundleProduct->setIsRelationsChanged(true);
+ /**
+ * Manage the logic of saving a bundle option, including the coalescence of its parent product data.
+ *
+ * @param ProductInterface $bundleProduct
+ * @param OptionInterface $option
+ * @return OptionInterface
+ * @throws CouldNotSaveException
+ * @throws Exception
+ */
+ public function save(ProductInterface $bundleProduct, OptionInterface $option)
+ {
+ $metadata = $this->metadataPool->getMetadata(ProductInterface::class);
+ $optionCollection = $this->type->getOptionsCollection($bundleProduct);
+
+ $this->saveOptionItem($bundleProduct, $option, $optionCollection, $metadata);
return $option;
}
@@ -149,6 +203,7 @@ private function updateOptionSelection(ProductInterface $product, OptionInterfac
}
/** @var LinkInterface[] $linksToDelete */
$linksToDelete = $this->compareLinks($existingLinks, $linksToUpdate);
+ $linksToUpdate = $this->verifyLinksToUpdate($existingLinks, $linksToUpdate);
}
foreach ($linksToUpdate as $linkedProduct) {
$this->linkManagement->saveChild($product->getSku(), $linkedProduct);
@@ -160,9 +215,56 @@ private function updateOptionSelection(ProductInterface $product, OptionInterfac
$linkedProduct->getSku()
);
}
- foreach ($linksToAdd as $linkedProduct) {
- $this->linkManagement->addChild($product, $option->getOptionId(), $linkedProduct);
+ $this->addChildren->addChildren($product, (int)$option->getOptionId(), $linksToAdd);
+ }
+
+ /**
+ * Verify that updated data actually changed
+ *
+ * @param LinkInterface[] $existing
+ * @param LinkInterface[] $updates
+ * @return array
+ */
+ private function verifyLinksToUpdate(array $existing, array $updates) : array
+ {
+ $linksToUpdate = [];
+ $beforeLinksMap = [];
+
+ foreach ($existing as $beforeLink) {
+ $beforeLinksMap[$beforeLink->getId()] = $beforeLink;
+ }
+
+ foreach ($updates as $updatedLink) {
+ if (array_key_exists($updatedLink->getId(), $beforeLinksMap)) {
+ $beforeLink = $beforeLinksMap[$updatedLink->getId()];
+ if ($this->isLinkChanged($beforeLink, $updatedLink)) {
+ $linksToUpdate[] = $updatedLink;
+ }
+ } else {
+ $linksToUpdate[] = $updatedLink;
+ }
}
+ return $linksToUpdate;
+ }
+
+ /**
+ * Check is updated link actually updated
+ *
+ * @param LinkInterface $beforeLink
+ * @param LinkInterface $updatedLink
+ * @return bool
+ */
+ private function isLinkChanged(LinkInterface $beforeLink, LinkInterface $updatedLink) : bool
+ {
+ return (int)$beforeLink->getOptionId() !== (int)$updatedLink->getOptionId()
+ || $beforeLink->getIsDefault() !== $updatedLink->getIsDefault()
+ || (float)$beforeLink->getQty() !== (float)$updatedLink->getQty()
+ || $beforeLink->getPrice() !== $updatedLink->getPrice()
+ || $beforeLink->getCanChangeQuantity() !== $updatedLink->getCanChangeQuantity()
+ || (array)$beforeLink->getExtensionAttributes() !== (array)$updatedLink->getExtensionAttributes()
+ || (int)$beforeLink->getPosition() !== (int)$updatedLink->getPosition()
+ || $beforeLink->getSku() !== $updatedLink->getSku()
+ || $beforeLink->getPriceType() !== $updatedLink->getPriceType();
}
/**
diff --git a/app/code/Magento/Bundle/Model/Plugin/Frontend/ProductIdentitiesExtender.php b/app/code/Magento/Bundle/Model/Plugin/Frontend/ProductIdentitiesExtender.php
index 2f6708a17639e..b7260dd49b20b 100644
--- a/app/code/Magento/Bundle/Model/Plugin/Frontend/ProductIdentitiesExtender.php
+++ b/app/code/Magento/Bundle/Model/Plugin/Frontend/ProductIdentitiesExtender.php
@@ -9,11 +9,12 @@
use Magento\Bundle\Model\Product\Type as BundleType;
use Magento\Catalog\Model\Product as CatalogProduct;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Add child identities to product identities on storefront.
*/
-class ProductIdentitiesExtender
+class ProductIdentitiesExtender implements ResetAfterRequestInterface
{
/**
* @var BundleType
@@ -68,4 +69,12 @@ private function getChildrenIds($entityId): array
return $this->cacheChildrenIds[$entityId];
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->cacheChildrenIds = [];
+ }
}
diff --git a/app/code/Magento/Bundle/Model/Plugin/ProductIdentitiesExtender.php b/app/code/Magento/Bundle/Model/Plugin/ProductIdentitiesExtender.php
index 42c6930469ac9..ff5b902c37e74 100644
--- a/app/code/Magento/Bundle/Model/Plugin/ProductIdentitiesExtender.php
+++ b/app/code/Magento/Bundle/Model/Plugin/ProductIdentitiesExtender.php
@@ -9,11 +9,12 @@
use Magento\Bundle\Model\Product\Type as BundleType;
use Magento\Catalog\Model\Product as CatalogProduct;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Add parent identities to product identities.
*/
-class ProductIdentitiesExtender
+class ProductIdentitiesExtender implements ResetAfterRequestInterface
{
/**
* @var BundleType
@@ -68,4 +69,12 @@ private function getParentIdsByChild($entityId): array
return $this->cacheParentIdsByChild[$entityId];
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->cacheParentIdsByChild = [];
+ }
}
diff --git a/app/code/Magento/Bundle/Model/Product/SaveHandler.php b/app/code/Magento/Bundle/Model/Product/SaveHandler.php
index e536b8961ebb9..412783565486e 100644
--- a/app/code/Magento/Bundle/Model/Product/SaveHandler.php
+++ b/app/code/Magento/Bundle/Model/Product/SaveHandler.php
@@ -103,9 +103,7 @@ public function execute($entity, $arguments = [])
$existingOptionsIds = !empty($existingBundleProductOptions)
? $this->getOptionIds($existingBundleProductOptions)
: [];
- $optionIds = !empty($bundleProductOptions)
- ? $this->getOptionIds($bundleProductOptions)
- : [];
+ $optionIds = $this->getOptionIds($bundleProductOptions);
if (!$entity->getCopyFromView()) {
$this->processRemovedOptions($entity, $existingOptionsIds, $optionIds);
@@ -161,12 +159,11 @@ protected function removeOptionLinks($entitySku, $option)
private function saveOptions($entity, array $options, array $newOptionsIds = []): void
{
foreach ($options as $option) {
- if (in_array($option->getOptionId(), $newOptionsIds, true)) {
+ if (in_array($option->getOptionId(), $newOptionsIds)) {
$option->setOptionId(null);
}
-
- $this->optionSave->save($entity, $option);
}
+ $this->optionSave->saveBulk($entity, $options);
}
/**
@@ -184,7 +181,7 @@ private function getOptionIds(array $options): array
/** @var OptionInterface $option */
foreach ($options as $option) {
if ($option->getOptionId()) {
- $optionIds[] = $option->getOptionId();
+ $optionIds[] = (int)$option->getOptionId();
}
}
}
diff --git a/app/code/Magento/Bundle/Model/Product/SelectionProductsDisabledRequired.php b/app/code/Magento/Bundle/Model/Product/SelectionProductsDisabledRequired.php
index d3f1c2f1c9991..424330a1671eb 100644
--- a/app/code/Magento/Bundle/Model/Product/SelectionProductsDisabledRequired.php
+++ b/app/code/Magento/Bundle/Model/Product/SelectionProductsDisabledRequired.php
@@ -10,6 +10,7 @@
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Catalog\Model\Product\Attribute\Source\Status;
use Magento\Bundle\Model\ResourceModel\Selection as BundleSelection;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory;
use Magento\Catalog\Model\Product;
@@ -18,7 +19,7 @@
/**
* Class to return ids of options and child products when all products in required option are disabled in bundle product
*/
-class SelectionProductsDisabledRequired
+class SelectionProductsDisabledRequired implements ResetAfterRequestInterface
{
/**
* @var BundleSelection
@@ -161,4 +162,12 @@ private function getCacheKey(int $bundleId, int $websiteId): string
{
return $bundleId . '-' . $websiteId;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->productsDisabledRequired = [];
+ }
}
diff --git a/app/code/Magento/Bundle/Model/Product/Type.php b/app/code/Magento/Bundle/Model/Product/Type.php
index d542458c365a7..511dbd092bcfe 100644
--- a/app/code/Magento/Bundle/Model/Product/Type.php
+++ b/app/code/Magento/Bundle/Model/Product/Type.php
@@ -7,6 +7,7 @@
namespace Magento\Bundle\Model\Product;
use Magento\Bundle\Model\Option;
+use Magento\Bundle\Model\ResourceModel\Option\AreBundleOptionsSalable;
use Magento\Bundle\Model\ResourceModel\Option\Collection;
use Magento\Bundle\Model\ResourceModel\Selection\Collection as Selections;
use Magento\Bundle\Model\ResourceModel\Selection\Collection\FilterApplier as SelectionCollectionFilterApplier;
@@ -170,6 +171,11 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType
*/
private $arrayUtility;
+ /**
+ * @var AreBundleOptionsSalable
+ */
+ private $areBundleOptionsSalable;
+
/**
* @param \Magento\Catalog\Model\Product\Option $catalogProductOption
* @param \Magento\Eav\Model\Config $eavConfig
@@ -196,7 +202,8 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType
* @param MetadataPool|null $metadataPool
* @param SelectionCollectionFilterApplier|null $selectionCollectionFilterApplier
* @param ArrayUtils|null $arrayUtility
- * @param UploaderFactory $uploaderFactory
+ * @param UploaderFactory|null $uploaderFactory
+ * @param AreBundleOptionsSalable|null $areBundleOptionsSalable
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -225,7 +232,8 @@ public function __construct(
MetadataPool $metadataPool = null,
SelectionCollectionFilterApplier $selectionCollectionFilterApplier = null,
ArrayUtils $arrayUtility = null,
- UploaderFactory $uploaderFactory = null
+ UploaderFactory $uploaderFactory = null,
+ AreBundleOptionsSalable $areBundleOptionsSalable = null
) {
$this->_catalogProduct = $catalogProduct;
$this->_catalogData = $catalogData;
@@ -246,6 +254,8 @@ public function __construct(
$this->selectionCollectionFilterApplier = $selectionCollectionFilterApplier
?: ObjectManager::getInstance()->get(SelectionCollectionFilterApplier::class);
$this->arrayUtility= $arrayUtility ?: ObjectManager::getInstance()->get(ArrayUtils::class);
+ $this->areBundleOptionsSalable = $areBundleOptionsSalable
+ ?? ObjectManager::getInstance()->get(AreBundleOptionsSalable::class);
parent::__construct(
$catalogProductOption,
@@ -595,44 +605,8 @@ public function isSalable($product)
return $product->getData('all_items_salable');
}
- $metadata = $this->metadataPool->getMetadata(
- \Magento\Catalog\Api\Data\ProductInterface::class
- );
-
- $isSalable = false;
- foreach ($this->getOptionsCollection($product) as $option) {
- $hasSalable = false;
-
- $selectionsCollection = $this->_bundleCollection->create();
- $selectionsCollection->addAttributeToSelect('status');
- $selectionsCollection->addQuantityFilter();
- $selectionsCollection->setFlag('product_children', true);
- $selectionsCollection->addFilterByRequiredOptions();
- $selectionsCollection->setOptionIdsFilter([$option->getId()]);
-
- $this->selectionCollectionFilterApplier->apply(
- $selectionsCollection,
- 'parent_product_id',
- $product->getData($metadata->getLinkField())
- );
-
- foreach ($selectionsCollection as $selection) {
- if ($selection->isSalable()) {
- $hasSalable = true;
- break;
- }
- }
-
- if ($hasSalable) {
- $isSalable = true;
- }
-
- if (!$hasSalable && $option->getRequired()) {
- $isSalable = false;
- break;
- }
- }
-
+ $store = $this->_storeManager->getStore();
+ $isSalable = $this->areBundleOptionsSalable->execute((int) $product->getEntityId(), (int) $store->getId());
$product->setData('all_items_salable', $isSalable);
return $isSalable;
diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/BundleOptionStockDataSelectBuilder.php b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/BundleOptionStockDataSelectBuilder.php
index c322a4b26241d..1562620fe03a2 100644
--- a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/BundleOptionStockDataSelectBuilder.php
+++ b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/BundleOptionStockDataSelectBuilder.php
@@ -40,6 +40,8 @@ public function __construct(
}
/**
+ * Build bundle options select
+ *
* @param string $idxTable
* @return Select
*/
@@ -67,6 +69,10 @@ public function buildSelect($idxTable)
['i' => $idxTable],
'i.product_id = bs.product_id AND i.website_id = cis.website_id AND i.stock_id = cis.stock_id',
[]
+ )->joinLeft(
+ ['cisi' => $this->resourceConnection->getTableName('cataloginventory_stock_item')],
+ 'cisi.product_id = i.product_id AND cisi.stock_id = i.stock_id',
+ []
)->joinLeft(
['e' => $this->resourceConnection->getTableName('catalog_product_entity')],
'e.entity_id = bs.product_id',
diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price/DisabledProductOptionPriceModifier.php b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price/DisabledProductOptionPriceModifier.php
index b3c3e74e1fa60..1730c3b62d3bf 100644
--- a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price/DisabledProductOptionPriceModifier.php
+++ b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price/DisabledProductOptionPriceModifier.php
@@ -15,11 +15,12 @@
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceModifierInterface;
use Magento\Bundle\Model\ResourceModel\Selection as BundleSelection;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Remove bundle product from price index when all products in required option are disabled
*/
-class DisabledProductOptionPriceModifier implements PriceModifierInterface
+class DisabledProductOptionPriceModifier implements PriceModifierInterface, ResetAfterRequestInterface
{
/**
* @var ResourceConnection
@@ -145,4 +146,12 @@ private function getBundleIds(array $entityIds): \Traversable
yield $id;
}
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->websiteIdsOfProduct = [];
+ }
}
diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Stock.php b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Stock.php
index 6808081506dd7..173e8f257b063 100644
--- a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Stock.php
+++ b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Stock.php
@@ -92,12 +92,7 @@ protected function _prepareBundleOptionStockData($entityIds = null, $usePrimaryT
$idxTable = $usePrimaryTable ? $table : $this->getIdxTable();
$select = $this->bundleOptionStockDataSelectBuilder->buildSelect($idxTable);
- $status = new \Zend_Db_Expr(
- 'MAX('
- . $connection->getCheckSql('e.required_options = 0', 'i.stock_status', '0')
- . ')'
- );
-
+ $status = $this->getOptionsStatusExpression();
$select->columns(['status' => $status]);
if ($entityIds !== null) {
@@ -194,4 +189,49 @@ protected function _cleanBundleOptionStockData()
$this->getConnection()->delete($this->_getBundleOptionTable());
return $this;
}
+
+ /**
+ * Build expression for bundle options stock status
+ *
+ * @return \Zend_Db_Expr
+ */
+ private function getOptionsStatusExpression(): \Zend_Db_Expr
+ {
+ $connection = $this->getConnection();
+ $isAvailableExpr = $connection->getCheckSql(
+ 'bs.selection_can_change_qty = 0 AND bs.selection_qty > i.qty',
+ '0',
+ 'i.stock_status'
+ );
+ if ($this->stockConfiguration->getBackorders()) {
+ $backordersExpr = $connection->getCheckSql(
+ 'cisi.use_config_backorders = 0 AND cisi.backorders = 0',
+ $isAvailableExpr,
+ 'i.stock_status'
+ );
+ } else {
+ $backordersExpr = $connection->getCheckSql(
+ 'cisi.use_config_backorders = 0 AND cisi.backorders > 0',
+ 'i.stock_status',
+ $isAvailableExpr
+ );
+ }
+ if ($this->stockConfiguration->getManageStock()) {
+ $statusExpr = $connection->getCheckSql(
+ 'cisi.use_config_manage_stock = 0 AND cisi.manage_stock = 0',
+ 1,
+ $backordersExpr
+ );
+ } else {
+ $statusExpr = $connection->getCheckSql(
+ 'cisi.use_config_manage_stock = 0 AND cisi.manage_stock = 1',
+ $backordersExpr,
+ 1
+ );
+ }
+
+ return new \Zend_Db_Expr(
+ 'MAX(' . $connection->getCheckSql('e.required_options = 0', $statusExpr, '0') . ')'
+ );
+ }
}
diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalable.php b/app/code/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalable.php
new file mode 100644
index 0000000000000..bfea7dc1295c5
--- /dev/null
+++ b/app/code/Magento/Bundle/Model/ResourceModel/Option/AreBundleOptionsSalable.php
@@ -0,0 +1,120 @@
+resourceConnection = $resourceConnection;
+ $this->metadataPool = $metadataPool;
+ $this->productAttributeRepository = $productAttributeRepository;
+ }
+
+ /**
+ * Check are bundle product options salable
+ *
+ * @param int $entityId
+ * @param int $storeId
+ * @return bool
+ */
+ public function execute(int $entityId, int $storeId): bool
+ {
+ $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField();
+ $connection = $this->resourceConnection->getConnection();
+ $optionsSaleabilitySelect = $connection->select()
+ ->from(
+ ['parent_products' => $this->resourceConnection->getTableName('catalog_product_entity')],
+ []
+ )->joinInner(
+ ['bundle_options' => $this->resourceConnection->getTableName('catalog_product_bundle_option')],
+ "bundle_options.parent_id = parent_products.{$linkField}",
+ []
+ )->joinInner(
+ ['bundle_selections' => $this->resourceConnection->getTableName('catalog_product_bundle_selection')],
+ 'bundle_selections.option_id = bundle_options.option_id',
+ []
+ )->joinInner(
+ ['child_products' => $this->resourceConnection->getTableName('catalog_product_entity')],
+ 'child_products.entity_id = bundle_selections.product_id',
+ []
+ )->group(
+ ['bundle_options.parent_id', 'bundle_options.option_id']
+ )->where(
+ 'parent_products.entity_id = ?',
+ $entityId
+ );
+ $statusAttr = $this->productAttributeRepository->get(ProductInterface::STATUS);
+ $optionsSaleabilitySelect->joinInner(
+ ['child_status_global' => $statusAttr->getBackendTable()],
+ "child_status_global.{$linkField} = child_products.{$linkField}"
+ . " AND child_status_global.attribute_id = {$statusAttr->getAttributeId()}"
+ . " AND child_status_global.store_id = 0",
+ []
+ )->joinLeft(
+ ['child_status_store' => $statusAttr->getBackendTable()],
+ "child_status_store.{$linkField} = child_products.{$linkField}"
+ . " AND child_status_store.attribute_id = {$statusAttr->getAttributeId()}"
+ . " AND child_status_store.store_id = {$storeId}",
+ []
+ );
+ $isOptionSalableExpr = new \Zend_Db_Expr(
+ sprintf(
+ 'MAX(IFNULL(child_status_store.value, child_status_global.value) != %s)',
+ ProductStatus::STATUS_DISABLED
+ )
+ );
+ $isRequiredOptionUnsalable = $connection->getCheckSql(
+ 'required = 1 AND ' . $isOptionSalableExpr . ' = 0',
+ '1',
+ '0'
+ );
+ $optionsSaleabilitySelect->columns([
+ 'required' => 'bundle_options.required',
+ 'is_salable' => $isOptionSalableExpr,
+ 'is_required_and_unsalable' => $isRequiredOptionUnsalable,
+ ]);
+
+ $select = $connection->select()->from(
+ $optionsSaleabilitySelect,
+ [new \Zend_Db_Expr('(MAX(is_salable) = 1 AND MAX(is_required_and_unsalable) = 0)')]
+ );
+ $isSalable = $connection->fetchOne($select);
+
+ return (bool) $isSalable;
+ }
+}
diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php b/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php
index 303c33b571d35..04f4305bdf77d 100644
--- a/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php
+++ b/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php
@@ -128,7 +128,6 @@ public function __construct(
$metadataPool,
$tableMaintainer
);
-
$this->stockItem = $stockItem
?? ObjectManager::getInstance()->get(\Magento\CatalogInventory\Model\ResourceModel\Stock\Item::class);
}
@@ -145,6 +144,17 @@ protected function _construct()
$this->_selectionTable = $this->getTable('catalog_product_bundle_selection');
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ parent::_resetState();
+ $this->itemPrototype = null;
+ $this->catalogRuleProcessor = null;
+ $this->websiteScopePriceJoined = false;
+ }
+
/**
* Set store id for each collection item when collection was loaded.
* phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod
@@ -355,8 +365,6 @@ public function addPriceFilter($product, $searchMin, $useRegularPrice = false)
* Get Catalog Rule Processor.
*
* @return \Magento\CatalogRule\Model\ResourceModel\Product\CollectionProcessor
- *
- * @deprecated 100.2.0
*/
private function getCatalogRuleProcessor()
{
diff --git a/app/code/Magento/Bundle/Model/Sales/Order/BundleOrderTypeValidator.php b/app/code/Magento/Bundle/Model/Sales/Order/BundleOrderTypeValidator.php
new file mode 100644
index 0000000000000..3475eb5cd3d85
--- /dev/null
+++ b/app/code/Magento/Bundle/Model/Sales/Order/BundleOrderTypeValidator.php
@@ -0,0 +1,235 @@
+request = $request;
+ }
+
+ /**
+ * Validates shipment items based on order item properties
+ *
+ * @param Shipment $value
+ * @return bool
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ * @throws \Magento\Sales\Exception\DocumentValidationException
+ */
+ public function isValid($value): bool
+ {
+ if (false === $this->validationNeeded()) {
+ return true;
+ }
+
+ $result = $shippingInfo = [];
+ foreach ($value->getItems() as $shipmentItem) {
+ $shippingInfo[$shipmentItem->getOrderItemId()] = [
+ 'shipment_info' => $shipmentItem,
+ 'order_info' => $value->getOrder()->getItemById($shipmentItem->getOrderItemId())
+ ];
+ }
+
+ foreach ($shippingInfo as $shippingItemInfo) {
+ if ($shippingItemInfo['order_info']->getProductType() === Type::TYPE_BUNDLE) {
+ $result[] = $this->checkBundleItem($shippingItemInfo, $shippingInfo);
+ } elseif ($shippingItemInfo['order_info']->getParentItem() &&
+ $shippingItemInfo['order_info']->getParentItem()->getProductType() === Type::TYPE_BUNDLE
+ ) {
+ $result[] = $this->checkChildItem($shippingItemInfo['order_info'], $shippingInfo);
+ }
+ $this->renderValidationMessages($result);
+ }
+
+ return empty($this->messages);
+ }
+
+ /**
+ * Returns validation messages
+ *
+ * @return array|string[]
+ */
+ public function getMessages(): array
+ {
+ return $this->messages;
+ }
+
+ /**
+ * Checks if shipment child item can be processed
+ *
+ * @param Item $orderItem
+ * @param array $shipmentInfo
+ * @return Phrase|null
+ * @throws NoSuchEntityException
+ */
+ private function checkChildItem(Item $orderItem, array $shipmentInfo): ?Phrase
+ {
+ $result = null;
+ if ($orderItem->getParentItem()->getProductType() === Type::TYPE_BUNDLE &&
+ $orderItem->getParentItem()->getProduct()->getShipmentType() === self::SHIPMENT_TYPE_TOGETHER) {
+ $result = __(
+ 'Cannot create shipment as bundle product "%1" has shipment type "%2". ' .
+ '%3 should be shipped instead.',
+ $orderItem->getParentItem()->getSku(),
+ __('Together'),
+ __('Bundle product itself')
+ );
+ }
+
+ if ($orderItem->getParentItem()->getProductType() === Type::TYPE_BUNDLE &&
+ $orderItem->getParentItem()->getProduct()->getShipmentType() === self::SHIPMENT_TYPE_SEPARATELY &&
+ false === $this->hasParentInShipping($orderItem, $shipmentInfo)
+ ) {
+ $result = __(
+ 'Cannot create shipment as bundle product %1 should be included as well.',
+ $orderItem->getParentItem()->getSku()
+ );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Checks if bundle item can be processed as a shipment item
+ *
+ * @param array $shippingItemInfo
+ * @param array $shippingInfo
+ * @return Phrase|null
+ */
+ private function checkBundleItem(array $shippingItemInfo, array $shippingInfo): ?Phrase
+ {
+ $result = null;
+ /** @var Item $orderItem */
+ $orderItem = $shippingItemInfo['order_info'];
+ /** @var ShipmentItemInterface $shipmentItem */
+ $shipmentItem = $shippingItemInfo['shipment_info'];
+
+ if ($orderItem->getProduct()->getShipmentType() === self::SHIPMENT_TYPE_TOGETHER &&
+ $this->hasChildrenInShipping($shipmentItem, $shippingInfo)
+ ) {
+ $result = __(
+ 'Cannot create shipment as bundle product "%1" has shipment type "%2". ' .
+ '%3 should be shipped instead.',
+ $orderItem->getSku(),
+ __('Together'),
+ __('Bundle product itself')
+ );
+ }
+ if ($orderItem->getProduct()->getShipmentType() === self::SHIPMENT_TYPE_SEPARATELY &&
+ false === $this->hasChildrenInShipping($shipmentItem, $shippingInfo)
+ ) {
+ $result = __(
+ 'Cannot create shipment as bundle product "%1" has shipment type "%2". ' .
+ 'Shipment should also incorporate bundle options.',
+ $orderItem->getSku(),
+ __('Separately')
+ );
+ }
+ return $result;
+ }
+
+ /**
+ * Determines if a child shipment item has its corresponding parent in shipment
+ *
+ * @param Item $childItem
+ * @param array $shipmentInfo
+ * @return bool
+ */
+ private function hasParentInShipping(Item $childItem, array $shipmentInfo): bool
+ {
+ /** @var Item $orderItem */
+ foreach (array_column($shipmentInfo, 'order_info') as $orderItem) {
+ if (!$orderItem->getParentItemId() &&
+ $orderItem->getProductType() === Type::TYPE_BUNDLE &&
+ $childItem->getParentItemId() == $orderItem->getItemId()
+ ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Determines if a bundle shipment item has at least one child in shipment
+ *
+ * @param ShipmentItemInterface $bundleItem
+ * @param array $shippingInfo
+ * @return bool
+ */
+ private function hasChildrenInShipping(ShipmentItemInterface $bundleItem, array $shippingInfo): bool
+ {
+ /** @var Item $orderItem */
+ foreach (array_column($shippingInfo, 'order_info') as $orderItem) {
+ if ($orderItem->getParentItemId() &&
+ $orderItem->getParentItemId() == $bundleItem->getOrderItemId()
+ ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Determines if the validation should be triggered or not
+ *
+ * @return bool
+ */
+ private function validationNeeded(): bool
+ {
+ return str_contains(strtolower($this->request->getUri()->getPath()), self::SHIPMENT_API_ROUTE);
+ }
+
+ /**
+ * Creates text based validation messages
+ *
+ * @param array $validationMessages
+ * @return void
+ */
+ private function renderValidationMessages(array $validationMessages): void
+ {
+ foreach ($validationMessages as $message) {
+ if ($message instanceof Phrase) {
+ $this->messages[] = $message->render();
+ }
+ }
+ $this->messages = array_unique($this->messages);
+ }
+}
diff --git a/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php b/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php
index 3051394eaf512..5e38edcb37607 100644
--- a/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php
+++ b/app/code/Magento/Bundle/Pricing/Adjustment/Calculator.php
@@ -12,6 +12,7 @@
use Magento\Bundle\Pricing\Price\BundleSelectionFactory;
use Magento\Bundle\Pricing\Price\BundleSelectionPrice;
use Magento\Catalog\Model\Product;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Framework\Pricing\Adjustment\Calculator as CalculatorBase;
use Magento\Framework\Pricing\Amount\AmountFactory;
use Magento\Framework\Pricing\Amount\AmountInterface;
@@ -25,7 +26,7 @@
* Bundle price calculator
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
-class Calculator implements BundleCalculatorInterface
+class Calculator implements BundleCalculatorInterface, ResetAfterRequestInterface
{
/**
* @var CalculatorBase
@@ -214,7 +215,8 @@ protected function getSelectionAmounts(Product $bundleProduct, $searchMin, $useR
* @param Option $option
* @param bool $canSkipRequiredOption
* @return bool
- * @deprecated 100.2.0
+ * @deprecated 100.2.0 Not used anymore.
+ * @see Nothing
*/
protected function canSkipOption($option, $canSkipRequiredOption)
{
@@ -226,7 +228,8 @@ protected function canSkipOption($option, $canSkipRequiredOption)
*
* @param Product $bundleProduct
* @return bool
- * @deprecated 100.2.0
+ * @deprecated 100.2.0 Not used anymore.
+ * @see Nothing
*/
protected function hasRequiredOption($bundleProduct)
{
@@ -245,6 +248,7 @@ function ($item) {
* @param Product $saleableItem
* @return \Magento\Bundle\Model\ResourceModel\Option\Collection
* @deprecated 100.2.0
+ * @see Nothing
*/
protected function getBundleOptions(Product $saleableItem)
{
@@ -425,4 +429,12 @@ public function processOptions($option, $selectionPriceList, $searchMin = true)
}
return $result;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->optionAmount = [];
+ }
}
diff --git a/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php b/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php
index 5d9e703c2414c..361eac3062341 100644
--- a/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php
+++ b/app/code/Magento/Bundle/Pricing/Adjustment/DefaultSelectionPriceListProvider.php
@@ -11,13 +11,14 @@
use Magento\Catalog\Model\Product;
use Magento\Bundle\Model\Product\Price;
use Magento\Catalog\Helper\Data as CatalogData;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Store\Api\WebsiteRepositoryInterface;
/**
* Provide lightweight implementation which uses price index
*/
-class DefaultSelectionPriceListProvider implements SelectionPriceListProviderInterface
+class DefaultSelectionPriceListProvider implements SelectionPriceListProviderInterface, ResetAfterRequestInterface
{
/**
* @var BundleSelectionFactory
@@ -245,4 +246,12 @@ private function getBundleOptions(Product $saleableItem)
{
return $saleableItem->getTypeInstance()->getOptionsCollection($saleableItem);
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->priceList = [];
+ }
}
diff --git a/app/code/Magento/Bundle/Pricing/Price/BundleOptions.php b/app/code/Magento/Bundle/Pricing/Price/BundleOptions.php
index e4951cc311737..4ac7bdd798e36 100644
--- a/app/code/Magento/Bundle/Pricing/Price/BundleOptions.php
+++ b/app/code/Magento/Bundle/Pricing/Price/BundleOptions.php
@@ -8,6 +8,7 @@
namespace Magento\Bundle\Pricing\Price;
use Magento\Bundle\Pricing\Adjustment\BundleCalculatorInterface;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Framework\Pricing\SaleableInterface;
use Magento\Framework\Pricing\Amount\AmountInterface;
use Magento\Catalog\Model\Product;
@@ -15,7 +16,7 @@
/**
* Bundle option price calculation model.
*/
-class BundleOptions
+class BundleOptions implements ResetAfterRequestInterface
{
/**
* @var BundleCalculatorInterface
@@ -91,6 +92,7 @@ public function calculateOptions(
/** @var \Magento\Bundle\Pricing\Price\BundleSelectionPrice $selectionPriceList */
$selectionPriceList = $this->calculator->createSelectionPriceList($option, $bundleProduct);
$selectionPriceList = $this->calculator->processOptions($option, $selectionPriceList, $searchMin);
+ // phpcs:ignore Magento2.Performance.ForeachArrayMerge
$priceList = array_merge($priceList, $selectionPriceList);
}
$amount = $this->calculator->calculateBundleAmount(0., $bundleProduct, $priceList);
@@ -135,4 +137,12 @@ public function getOptionSelectionAmount(
return $this->optionSelectionAmountCache[$cacheKey];
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->optionSelectionAmountCache = [];
+ }
}
diff --git a/app/code/Magento/Bundle/Pricing/Price/BundleRegularPrice.php b/app/code/Magento/Bundle/Pricing/Price/BundleRegularPrice.php
index 9bda194df4b0e..5028c35eea008 100644
--- a/app/code/Magento/Bundle/Pricing/Price/BundleRegularPrice.php
+++ b/app/code/Magento/Bundle/Pricing/Price/BundleRegularPrice.php
@@ -7,6 +7,8 @@
namespace Magento\Bundle\Pricing\Price;
use Magento\Bundle\Pricing\Adjustment\BundleCalculatorInterface;
+use Magento\Catalog\Pricing\Price\RegularPrice;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Framework\Pricing\Amount\AmountInterface;
use Magento\Catalog\Pricing\Price\CustomOptionPrice;
use Magento\Bundle\Model\Product\Price;
@@ -14,7 +16,7 @@
/**
* Bundle product regular price model
*/
-class BundleRegularPrice extends \Magento\Catalog\Pricing\Price\RegularPrice implements RegularPriceInterface
+class BundleRegularPrice extends RegularPrice implements RegularPriceInterface, ResetAfterRequestInterface
{
/**
* @var BundleCalculatorInterface
@@ -72,4 +74,13 @@ public function getMinimalPrice()
{
return $this->getAmount();
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->maximalPrice = null;
+ $this->amount = [];
+ }
}
diff --git a/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml
index e5f557dd22ded..67844e8759bf5 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Data/ProductData.xml
@@ -31,6 +31,11 @@
$10.00
Default
+
+ BundleProduct
+ bu/ndle
+ 1
+
FixedBundleProduct
fixed-bundle-product
diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml
index 8b78ac7b5fe6e..295243f8a81d3 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml
@@ -125,5 +125,7 @@
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml
index 2fde274dc5288..05f6c73f77826 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleProductToCartFromWishListPageTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleProductToCartFromWishListPageTest.xml
index 9722835b201c1..3cd206f083dce 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleProductToCartFromWishListPageTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleProductToCartFromWishListPageTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDecimalDefaultToBundleItemsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDecimalDefaultToBundleItemsTest.xml
new file mode 100644
index 0000000000000..25c94f015a109
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDecimalDefaultToBundleItemsTest.xml
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+ $grabbedFirstBundleOptionQuantity
+
+
+
+ 1
+ $grabbedSecondBundleOptionQuantity
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml
index 5936948d0a8c2..9c40d19644ff9 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml
index cbba284859697..ed0ba38674184 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml
index d7d053c3e1f54..9538542c75ded 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceCalculationOnProductPageTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceCalculationOnProductPageTest.xml
index a41e1f369b707..c1bd617eccce2 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceCalculationOnProductPageTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceCalculationOnProductPageTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceValidationErrorDisappearedAfterSwitchToDynamicPriceTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceValidationErrorDisappearedAfterSwitchToDynamicPriceTest.xml
index 3edca9a5bb7ff..f37224cd76ced 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceValidationErrorDisappearedAfterSwitchToDynamicPriceTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBundleProductPriceValidationErrorDisappearedAfterSwitchToDynamicPriceTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCheckingBundleSKUsCreationTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCheckingBundleSKUsCreationTest.xml
index 244ea386e7a11..8cd46fb082b1e 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCheckingBundleSKUsCreationTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCheckingBundleSKUsCreationTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductSettingsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductSettingsTest.xml
index 7bcd4d0899ede..f48dcc6fee50a 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductSettingsTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductSettingsTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateBundleProductTest.xml
index 9d24d4f8d38b6..2a097d105c27c 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateBundleProductTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateBundleProductTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProductTest.xml
index 2f7dd14d1d712..1333d460b7214 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProductTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml
index 467fd965e3282..ae44d098e8e7c 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicPriceProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml
index 79c7d113477fb..0d10071fdc991 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml
index b5b0fa3187b03..07b3b7096bd04 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductInDutchUserLanguageTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductInDutchUserLanguageTest.xml
index 77a43721d4e67..97c46b07b2e5c 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductInDutchUserLanguageTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductInDutchUserLanguageTest.xml
@@ -8,7 +8,7 @@
-
+
@@ -42,6 +42,12 @@
+
+
+
+
+
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProductsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProductsTest.xml
index 5c23360e74d78..3a096a1a6cbd4 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProductsTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProductsTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml
index 643e71626e62b..12eb267e3ec8d 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml
index 482c8ed503676..3d2b096249c9a 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleBySkuTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleBySkuTest.xml
index eadf7667b010b..e1ec035c6f326 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleBySkuTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdvanceCatalogSearchBundleBySkuTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml
index 30397d8473550..92db02dd4205c 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithDynamicTierPriceInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithDynamicTierPriceInCartTest.xml
index c56e09562d49a..fc577bf4dd8e9 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithDynamicTierPriceInCartTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithDynamicTierPriceInCartTest.xml
@@ -13,6 +13,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithOptionTierPriceInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithOptionTierPriceInCartTest.xml
index 1b33bb08b1b03..55d9439a544ef 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithOptionTierPriceInCartTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithOptionTierPriceInCartTest.xml
@@ -13,6 +13,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml
index eb047822cd230..0d3c9dbf2d09c 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/EditOrderWithBundleProductBackendProductEvenAfterOneOfMoreSelectedOptionsAreRemovedFromAdminTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/EditOrderWithBundleProductBackendProductEvenAfterOneOfMoreSelectedOptionsAreRemovedFromAdminTest.xml
index 963f144dfeaad..1633491410233 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/EditOrderWithBundleProductBackendProductEvenAfterOneOfMoreSelectedOptionsAreRemovedFromAdminTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/EditOrderWithBundleProductBackendProductEvenAfterOneOfMoreSelectedOptionsAreRemovedFromAdminTest.xml
@@ -16,6 +16,7 @@
+
@@ -85,8 +86,8 @@
-
-
+
+
@@ -114,12 +115,12 @@
-
+
-
+
-
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/EditOrderWithBundleProductBackendTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/EditOrderWithBundleProductBackendTest.xml
index 8859f9d2a3c86..51f701f251ab3 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/EditOrderWithBundleProductBackendTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/EditOrderWithBundleProductBackendTest.xml
@@ -84,7 +84,7 @@
-
+
@@ -110,7 +110,7 @@
-
+
@@ -134,7 +134,7 @@
-
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml
index 5758a782d3b55..55e3a4106f351 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml
index f4b81e9ba9577..b6ad937eac4a5 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml
index 378c59048cdea..f1ce131d13e38 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml
index 37e743c0dc049..7d8f793883ce3 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml
index 55bb27d317c1c..5846190c80bc5 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGridTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGridTest.xml
index 918e6014dbb97..9961855e93518 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGridTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGridTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml
index 61545268ef63e..800df1d991389 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml
index 9c334fea8d80a..50fe5c76549de 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontPlaceOrderBundleProductFixedPriceWithUpdatedPriceTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontPlaceOrderBundleProductFixedPriceWithUpdatedPriceTest.xml
index 3fa758effc18a..5d710475251d3 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontPlaceOrderBundleProductFixedPriceWithUpdatedPriceTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontPlaceOrderBundleProductFixedPriceWithUpdatedPriceTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml
index 9ab7df0f5dc7a..0933a1ecceb6c 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontValidateQuantityBundleProductsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontValidateQuantityBundleProductsTest.xml
index b486d95ac3e4b..e4739f416ce4a 100644
--- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontValidateQuantityBundleProductsTest.xml
+++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontValidateQuantityBundleProductsTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Option/SaveActionTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Option/SaveActionTest.php
new file mode 100644
index 0000000000000..1b8fd65455f3d
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Unit/Model/Option/SaveActionTest.php
@@ -0,0 +1,121 @@
+linkManagement = $this->getMockBuilder(ProductLinkManagementInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->metadataPool = $this->getMockBuilder(MetadataPool::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->type = $this->getMockBuilder(Type::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->optionResource = $this->getMockBuilder(OptionResource::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->product = $this->getMockBuilder(ProductInterface::class)
+ ->addMethods(['getStoreId', 'getData', 'setIsRelationsChanged'])
+ ->getMockForAbstractClass();
+
+ $this->saveAction = new SaveAction(
+ $this->optionResource,
+ $this->metadataPool,
+ $this->type,
+ $this->linkManagement
+ );
+ }
+
+ public function testSaveBulk()
+ {
+ $option = $this->getMockBuilder(Option::class)
+ ->onlyMethods(['getOptionId', 'setData', 'getData'])
+ ->addMethods(['setStoreId', 'setParentId', 'getParentId'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $option->expects($this->any())
+ ->method('getOptionId')
+ ->willReturn(1);
+ $option->expects($this->any())
+ ->method('getData')
+ ->willReturn([]);
+ $bundleOptions = [$option];
+
+ $collection = $this->getMockBuilder(Collection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $collection->expects($this->once())
+ ->method('getItemById')
+ ->with(1)
+ ->willReturn($option);
+ $this->type->expects($this->once())
+ ->method('getOptionsCollection')
+ ->willReturn($collection);
+
+ $metadata = $this->getMockBuilder(EntityMetadataInterface::class)
+ ->getMockForAbstractClass();
+ $this->metadataPool->expects($this->once())
+ ->method('getMetadata')
+ ->willReturn($metadata);
+
+ $this->linkManagement->expects($this->once())
+ ->method('getChildren')
+ ->willReturn([]);
+ $this->product->expects($this->once())
+ ->method('setIsRelationsChanged')
+ ->with(true);
+
+ $this->saveAction->saveBulk($this->product, $bundleOptions);
+ }
+}
diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/SaveHandlerTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/SaveHandlerTest.php
new file mode 100644
index 0000000000000..3eeac7a324c07
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/SaveHandlerTest.php
@@ -0,0 +1,140 @@
+productLinkManagement = $this->getMockBuilder(ProductLinkManagementInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->optionRepository = $this->getMockBuilder(OptionRepository::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->optionSave = $this->getMockBuilder(SaveAction::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->metadataPool = $this->getMockBuilder(MetadataPool::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->checkOptionLinkIfExist = $this->getMockBuilder(CheckOptionLinkIfExist::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->productRelationsProcessorComposite = $this->getMockBuilder(ProductRelationsProcessorComposite::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->entity = $this->getMockBuilder(ProductInterface::class)
+ ->addMethods(['getCopyFromView', 'getData'])
+ ->getMockForAbstractClass();
+ $this->entity->expects($this->any())
+ ->method('getTypeId')
+ ->willReturn(Type::TYPE_CODE);
+
+ $this->saveHandler = new SaveHandler(
+ $this->optionRepository,
+ $this->productLinkManagement,
+ $this->optionSave,
+ $this->metadataPool,
+ $this->checkOptionLinkIfExist,
+ $this->productRelationsProcessorComposite
+ );
+ }
+
+ /**
+ * @return void
+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
+ */
+ public function testExecuteWithBulkOptionsProcessing(): void
+ {
+ $option = $this->getMockBuilder(OptionInterface::class)
+ ->onlyMethods(['getOptionId'])
+ ->getMockForAbstractClass();
+ $option->expects($this->any())
+ ->method('getOptionId')
+ ->willReturn(1);
+ $bundleOptions = [$option];
+
+ $extensionAttributes = $this->getMockBuilder(ProductExtensionInterface::class)
+ ->addMethods(['getBundleProductOptions'])
+ ->getMockForAbstractClass();
+ $extensionAttributes->expects($this->any())
+ ->method('getBundleProductOptions')
+ ->willReturn($bundleOptions);
+ $this->entity->expects($this->once())
+ ->method('getExtensionAttributes')
+ ->willReturn($extensionAttributes);
+ $metadata = $this->getMockBuilder(EntityMetadataInterface::class)
+ ->getMockForAbstractClass();
+ $this->metadataPool->expects($this->once())
+ ->method('getMetadata')
+ ->willReturn($metadata);
+ $this->optionRepository->expects($this->any())
+ ->method('getList')
+ ->willReturn($bundleOptions);
+
+ $this->optionSave->expects($this->once())
+ ->method('saveBulk');
+ $this->saveHandler->execute($this->entity);
+ }
+}
diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php
index 68310d1d2bb44..a222b3c3eff3c 100644
--- a/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php
+++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php
@@ -2129,61 +2129,6 @@ public function testIsSalableFalse(): void
$this->assertFalse($this->model->isSalable($product));
}
- /**
- * @return void
- */
- public function testIsSalableWithoutOptions(): void
- {
- $optionCollectionMock = $this->getOptionCollectionMock([]);
- $product = new DataObject(
- [
- 'is_salable' => true,
- '_cache_instance_options_collection' => $optionCollectionMock,
- 'status' => Status::STATUS_ENABLED
- ]
- );
-
- $this->assertFalse($this->model->isSalable($product));
- }
-
- /**
- * @return void
- */
- public function testIsSalableWithRequiredOptionsTrue(): void
- {
- $option1 = $this->getRequiredOptionMock(10, 10);
- $option2 = $this->getRequiredOptionMock(20, 10);
-
- $option3 = $this->getMockBuilder(\Magento\Bundle\Model\Option::class)
- ->onlyMethods(['getRequired', 'getOptionId', 'getId'])
- ->disableOriginalConstructor()
- ->getMock();
- $option3->method('getRequired')
- ->willReturn(false);
- $option3->method('getOptionId')
- ->willReturn(30);
- $option3->method('getId')
- ->willReturn(30);
-
- $this->expectProductEntityMetadata();
-
- $optionCollectionMock = $this->getOptionCollectionMock([$option1, $option2, $option3]);
- $selectionCollectionMock = $this->getSelectionCollectionMock([$option1, $option2]);
- $this->bundleCollectionFactory->expects($this->atLeastOnce())
- ->method('create')
- ->willReturn($selectionCollectionMock);
-
- $product = new DataObject(
- [
- 'is_salable' => true,
- '_cache_instance_options_collection' => $optionCollectionMock,
- 'status' => Status::STATUS_ENABLED
- ]
- );
-
- $this->assertTrue($this->model->isSalable($product));
- }
-
/**
* @return void
*/
@@ -2200,124 +2145,6 @@ public function testIsSalableCache(): void
$this->assertTrue($this->model->isSalable($product));
}
- /**
- * @return void
- */
- public function testIsSalableWithEmptySelectionsCollection(): void
- {
- $option = $this->getRequiredOptionMock(1, 10);
- $optionCollectionMock = $this->getOptionCollectionMock([$option]);
- $selectionCollectionMock = $this->getSelectionCollectionMock([]);
- $this->expectProductEntityMetadata();
-
- $this->bundleCollectionFactory->expects($this->once())
- ->method('create')
- ->willReturn($selectionCollectionMock);
-
- $product = new DataObject(
- [
- 'is_salable' => true,
- '_cache_instance_options_collection' => $optionCollectionMock,
- 'status' => Status::STATUS_ENABLED
- ]
- );
-
- $this->assertFalse($this->model->isSalable($product));
- }
-
- /**
- * @return void
- */
- public function testIsSalableWithNonSalableRequiredOptions(): void
- {
- $option1 = $this->getRequiredOptionMock(10, 10);
- $option2 = $this->getRequiredOptionMock(20, 10);
- $optionCollectionMock = $this->getOptionCollectionMock([$option1, $option2]);
- $this->expectProductEntityMetadata();
-
- $selection1 = $this->getMockBuilder(Product::class)
- ->onlyMethods(['isSalable'])
- ->disableOriginalConstructor()
- ->getMock();
-
- $selection1->expects($this->once())
- ->method('isSalable')
- ->willReturn(true);
-
- $selection2 = $this->getMockBuilder(Product::class)
- ->onlyMethods(['isSalable'])
- ->disableOriginalConstructor()
- ->getMock();
-
- $selection2->expects($this->once())
- ->method('isSalable')
- ->willReturn(false);
-
- $selectionCollectionMock1 = $this->getSelectionCollectionMock([$selection1]);
- $selectionCollectionMock2 = $this->getSelectionCollectionMock([$selection2]);
-
- $this->bundleCollectionFactory->expects($this->exactly(2))
- ->method('create')
- ->will($this->onConsecutiveCalls(
- $selectionCollectionMock1,
- $selectionCollectionMock2
- ));
-
- $product = new DataObject(
- [
- 'is_salable' => true,
- '_cache_instance_options_collection' => $optionCollectionMock,
- 'status' => Status::STATUS_ENABLED
- ]
- );
-
- $this->assertFalse($this->model->isSalable($product));
- }
-
- /**
- * @param int $id
- * @param int $selectionQty
- *
- * @return MockObject
- */
- private function getRequiredOptionMock(int $id, int $selectionQty): MockObject
- {
- $option = $this->getMockBuilder(\Magento\Bundle\Model\Option::class)
- ->onlyMethods(
- [
- 'getRequired',
- 'getOptionId',
- 'getId'
- ]
- )
- ->addMethods(
- [
- 'isSalable',
- 'hasSelectionQty',
- 'getSelectionQty',
- 'getSelectionCanChangeQty'
- ]
- )
- ->disableOriginalConstructor()
- ->getMock();
- $option->method('getRequired')
- ->willReturn(true);
- $option->method('isSalable')
- ->willReturn(true);
- $option->method('hasSelectionQty')
- ->willReturn(true);
- $option->method('getSelectionQty')
- ->willReturn($selectionQty);
- $option->method('getOptionId')
- ->willReturn($id);
- $option->method('getSelectionCanChangeQty')
- ->willReturn(false);
- $option->method('getId')
- ->willReturn($id);
-
- return $option;
- }
-
/**
* @param array $selectedOptions
*
@@ -2338,25 +2165,6 @@ private function getSelectionCollectionMock(array $selectedOptions): MockObject
return $selectionCollectionMock;
}
- /**
- * @param array $options
- *
- * @return MockObject
- */
- private function getOptionCollectionMock(array $options): MockObject
- {
- $optionCollectionMock = $this->getMockBuilder(\Magento\Bundle\Model\ResourceModel\Option\Collection::class)
- ->onlyMethods(['getIterator'])
- ->disableOriginalConstructor()
- ->getMock();
-
- $optionCollectionMock->expects($this->any())
- ->method('getIterator')
- ->willReturn(new \ArrayIterator($options));
-
- return $optionCollectionMock;
- }
-
/**
* @param bool $isManageStock
*
diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Sales/Order/BundleOrderTypeValidatorTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Sales/Order/BundleOrderTypeValidatorTest.php
new file mode 100644
index 0000000000000..a2ac9b109de20
--- /dev/null
+++ b/app/code/Magento/Bundle/Test/Unit/Model/Sales/Order/BundleOrderTypeValidatorTest.php
@@ -0,0 +1,246 @@
+request = $this->createMock(Request::class);
+ $uri = $this->createMock(HttpUri::class);
+ $uri->expects($this->any())->method('getPath')->willReturn('V1/shipment/');
+ $this->request->expects($this->any())->method('getUri')->willReturn($uri);
+
+ $this->validator = new BundleOrderTypeValidator($this->request);
+
+ parent::setUp();
+ }
+
+ /**
+ * @return void
+ */
+ public function testIsValidSuccessShipmentTypeTogether(): void
+ {
+ $bundleProduct = $this->getMockBuilder(Product::class)
+ ->disableOriginalConstructor()
+ ->addMethods(['getShipmentType'])
+ ->getMock();
+ $bundleProduct->expects($this->any())
+ ->method('getShipmentType')
+ ->willReturn(BundleOrderTypeValidator::SHIPMENT_TYPE_TOGETHER);
+
+ $bundleOrderItem = $this->getBundleOrderItemMock();
+ $bundleOrderItem->expects($this->any())->method('getProductType')->willReturn(Type::TYPE_BUNDLE);
+ $bundleOrderItem->expects($this->any())->method('getProduct')->willReturn($bundleProduct);
+
+ $order = $this->createMock(\Magento\Sales\Model\Order::class);
+ $order->expects($this->once())
+ ->method('getItemById')
+ ->willReturn($bundleOrderItem);
+
+ $bundleShipmentItem = $this->createMock(\Magento\Sales\Api\Data\ShipmentItemInterface::class);
+ $bundleShipmentItem->expects($this->any())->method('getOrderItemId')->willReturn(1);
+
+ $shipment = $this->createMock(Shipment::class);
+ $shipment->expects($this->once())
+ ->method('getItems')
+ ->willReturn([$bundleShipmentItem]);
+ $shipment->expects($this->once())->method('getOrder')->willReturn($order);
+
+ try {
+ $this->validator->isValid($shipment);
+ $this->assertEmpty($this->validator->getMessages());
+ } catch (\Exception $e) {
+ $this->fail('Could not perform shipment validation. ' . $e->getMessage());
+ }
+ }
+
+ public function testIsValidSuccessShipmentTypeSeparately()
+ {
+ $bundleProduct = $this->getMockBuilder(Product::class)
+ ->disableOriginalConstructor()
+ ->addMethods(['getShipmentType'])
+ ->getMock();
+ $bundleProduct->expects($this->any())
+ ->method('getShipmentType')
+ ->willReturn(BundleOrderTypeValidator::SHIPMENT_TYPE_SEPARATELY);
+
+ $bundleOrderItem = $this->getBundleOrderItemMock();
+ $bundleOrderItem->expects($this->any())->method('getProductType')->willReturn(Type::TYPE_BUNDLE);
+ $bundleOrderItem->expects($this->any())->method('getProduct')->willReturn($bundleProduct);
+
+ $childOrderItem = $this->createMock(\Magento\Sales\Model\Order\Item::class);
+ $childOrderItem->expects($this->any())->method('getParentItemId')
+ ->willReturn(1);
+
+ $order = $this->createMock(\Magento\Sales\Model\Order::class);
+ $order->expects($this->any())
+ ->method('getItemById')
+ ->willReturnOnConsecutiveCalls($bundleOrderItem, $childOrderItem);
+
+ $bundleShipmentItem = $this->createMock(\Magento\Sales\Api\Data\ShipmentItemInterface::class);
+ $bundleShipmentItem->expects($this->any())->method('getOrderItemId')->willReturn(1);
+ $bundleShipmentItem->expects($this->exactly(3))->method('getOrderItemId')->willReturn(1);
+
+ $childShipmentItem = $this->createMock(\Magento\Sales\Api\Data\ShipmentItemInterface::class);
+ $childShipmentItem->expects($this->any())->method('getOrderItemId')->willReturn(2);
+
+ $shipment = $this->createMock(Shipment::class);
+ $shipment->expects($this->once())
+ ->method('getItems')
+ ->willReturn([$bundleShipmentItem, $childShipmentItem]);
+ $shipment->expects($this->exactly(2))->method('getOrder')->willReturn($order);
+
+ try {
+ $this->validator->isValid($shipment);
+ $this->assertEmpty($this->validator->getMessages());
+ } catch (\Exception $e) {
+ $this->fail('Could not perform shipment validation. ' . $e->getMessage());
+ }
+ }
+
+ /**
+ * @return void
+ */
+ public function testIsValidFailSeparateShipmentType(): void
+ {
+ $bundleProduct = $this->getMockBuilder(Product::class)
+ ->disableOriginalConstructor()
+ ->addMethods(['getShipmentType'])
+ ->getMock();
+ $bundleProduct->expects($this->any())
+ ->method('getShipmentType')
+ ->willReturn(BundleOrderTypeValidator::SHIPMENT_TYPE_SEPARATELY);
+
+ $bundleOrderItem = $this->getBundleOrderItemMock();
+ $bundleOrderItem->expects($this->any())->method('getProductType')->willReturn(Type::TYPE_BUNDLE);
+ $bundleOrderItem->expects($this->any())->method('getProduct')->willReturn($bundleProduct);
+ $bundleOrderItem->expects($this->any())->method('getSku')->willReturn('sku');
+
+ $childOrderItem = $this->createMock(\Magento\Sales\Model\Order\Item::class);
+ $childOrderItem->expects($this->any())->method('getParentItemId')
+ ->willReturn(1);
+ $childOrderItem->expects($this->any())->method('getParentItem')->willReturn($bundleOrderItem);
+
+ $order = $this->createMock(\Magento\Sales\Model\Order::class);
+ $order->expects($this->any())
+ ->method('getItemById')
+ ->willReturn($childOrderItem);
+
+ $childShipmentItem = $this->createMock(\Magento\Sales\Api\Data\ShipmentItemInterface::class);
+ $childShipmentItem->expects($this->any())->method('getOrderItemId')->willReturn(2);
+
+ $shipment = $this->createMock(Shipment::class);
+ $shipment->expects($this->once())
+ ->method('getItems')
+ ->willReturn([$childShipmentItem]);
+ $shipment->expects($this->once())->method('getOrder')->willReturn($order);
+
+ try {
+ $this->validator->isValid($shipment);
+ $this->assertNotEmpty($this->validator->getMessages());
+ $this->assertTrue(
+ in_array(
+ 'Cannot create shipment as bundle product sku should be included as well.',
+ $this->validator->getMessages()
+ )
+ );
+ } catch (\Exception $e) {
+ $this->fail('Could not perform shipment validation. ' . $e->getMessage());
+ }
+ }
+
+ /**
+ * @return void
+ */
+ public function testIsValidFailTogetherShipmentType(): void
+ {
+ $bundleProduct = $this->getMockBuilder(Product::class)
+ ->disableOriginalConstructor()
+ ->addMethods(['getShipmentType'])
+ ->getMock();
+ $bundleProduct->expects($this->any())
+ ->method('getShipmentType')
+ ->willReturn(BundleOrderTypeValidator::SHIPMENT_TYPE_TOGETHER);
+
+ $bundleOrderItem = $this->getBundleOrderItemMock();
+ $bundleOrderItem->expects($this->any())->method('getProductType')->willReturn(Type::TYPE_BUNDLE);
+ $bundleOrderItem->expects($this->any())->method('getProduct')->willReturn($bundleProduct);
+ $bundleOrderItem->expects($this->any())->method('getSku')->willReturn('sku');
+
+ $bundleShipmentItem = $this->createMock(\Magento\Sales\Api\Data\ShipmentItemInterface::class);
+ $bundleShipmentItem->expects($this->any())->method('getOrderItemId')->willReturn(1);
+ $bundleShipmentItem->expects($this->exactly(3))->method('getOrderItemId')->willReturn(1);
+
+ $childShipmentItem = $this->createMock(\Magento\Sales\Api\Data\ShipmentItemInterface::class);
+ $childShipmentItem->expects($this->any())->method('getOrderItemId')->willReturn(2);
+
+ $childOrderItem = $this->createMock(\Magento\Sales\Model\Order\Item::class);
+ $childOrderItem->expects($this->any())->method('getParentItemId')
+ ->willReturn(1);
+
+ $order = $this->createMock(\Magento\Sales\Model\Order::class);
+ $order->expects($this->any())
+ ->method('getItemById')
+ ->willReturnOnConsecutiveCalls($bundleOrderItem, $childOrderItem);
+
+ $shipment = $this->createMock(Shipment::class);
+ $shipment->expects($this->once())
+ ->method('getItems')
+ ->willReturn([$bundleShipmentItem, $childShipmentItem]);
+ $shipment->expects($this->exactly(2))->method('getOrder')->willReturn($order);
+
+ try {
+ $this->validator->isValid($shipment);
+ $this->assertNotEmpty($this->validator->getMessages());
+ $this->assertTrue(
+ in_array(
+ 'Cannot create shipment as bundle product "sku" has shipment type "Together". '
+ . 'Bundle product itself should be shipped instead.',
+ $this->validator->getMessages()
+ )
+ );
+ } catch (\Exception $e) {
+ $this->fail('Could not perform shipment validation. ' . $e->getMessage());
+ }
+ }
+
+ /**
+ * @return MockObject
+ */
+ private function getBundleOrderItemMock(): MockObject
+ {
+ return $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class)
+ ->disableOriginalConstructor()
+ ->addMethods(['getHasChildren'])
+ ->onlyMethods(['getItemId', 'isDummy', 'getProductType', 'getSku', 'getParentItem', 'getProduct'])
+ ->getMock();
+ }
+}
diff --git a/app/code/Magento/Bundle/Test/Unit/Ui/DataProvider/Product/BundleDataProviderTest.php b/app/code/Magento/Bundle/Test/Unit/Ui/DataProvider/Product/BundleDataProviderTest.php
index 9eb0e7aa8946c..2cf0c201f1203 100644
--- a/app/code/Magento/Bundle/Test/Unit/Ui/DataProvider/Product/BundleDataProviderTest.php
+++ b/app/code/Magento/Bundle/Test/Unit/Ui/DataProvider/Product/BundleDataProviderTest.php
@@ -14,12 +14,13 @@
use Magento\Framework\App\RequestInterface;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
use Magento\Store\Model\Store;
+use Magento\Ui\DataProvider\Modifier\PoolInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
class BundleDataProviderTest extends TestCase
{
- const ALLOWED_TYPE = 'simple';
+ private const ALLOWED_TYPE = 'simple';
/**
* @var ObjectManager
@@ -46,6 +47,11 @@ class BundleDataProviderTest extends TestCase
*/
protected $dataHelperMock;
+ /**
+ * @var PoolInterface|MockObject
+ */
+ private $modifierPool;
+
/**
* @return void
*/
@@ -53,6 +59,9 @@ protected function setUp(): void
{
$this->objectManager = new ObjectManager($this);
+ $this->modifierPool = $this->getMockBuilder(PoolInterface::class)
+ ->getMockForAbstractClass();
+
$this->requestMock = $this->getMockBuilder(RequestInterface::class)
->getMockForAbstractClass();
$this->collectionMock = $this->getMockBuilder(Collection::class)
@@ -97,6 +106,7 @@ protected function getModel()
'addFilterStrategies' => [],
'meta' => [],
'data' => [],
+ 'modifiersPool' => $this->modifierPool,
]);
}
@@ -128,6 +138,9 @@ public function testGetData()
$this->collectionMock->expects($this->once())
->method('getSize')
->willReturn(count($items));
+ $this->modifierPool->expects($this->once())
+ ->method('getModifiersInstances')
+ ->willReturn([]);
$this->assertEquals($expectedData, $this->getModel()->getData());
}
diff --git a/app/code/Magento/Bundle/Ui/DataProvider/Product/BundleDataProvider.php b/app/code/Magento/Bundle/Ui/DataProvider/Product/BundleDataProvider.php
index 5f1ffc3c26823..827082dc77445 100644
--- a/app/code/Magento/Bundle/Ui/DataProvider/Product/BundleDataProvider.php
+++ b/app/code/Magento/Bundle/Ui/DataProvider/Product/BundleDataProvider.php
@@ -8,6 +8,9 @@
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\Catalog\Ui\DataProvider\Product\ProductDataProvider;
use Magento\Bundle\Helper\Data;
+use Magento\Framework\App\ObjectManager;
+use Magento\Ui\DataProvider\Modifier\ModifierInterface;
+use Magento\Ui\DataProvider\Modifier\PoolInterface;
class BundleDataProvider extends ProductDataProvider
{
@@ -16,6 +19,11 @@ class BundleDataProvider extends ProductDataProvider
*/
protected $dataHelper;
+ /**
+ * @var PoolInterface
+ */
+ private $modifiersPool;
+
/**
* Construct
*
@@ -24,10 +32,12 @@ class BundleDataProvider extends ProductDataProvider
* @param string $requestFieldName
* @param CollectionFactory $collectionFactory
* @param Data $dataHelper
- * @param \Magento\Ui\DataProvider\AddFieldToCollectionInterface[] $addFieldStrategies
- * @param \Magento\Ui\DataProvider\AddFilterToCollectionInterface[] $addFilterStrategies
* @param array $meta
* @param array $data
+ * @param \Magento\Ui\DataProvider\AddFieldToCollectionInterface[] $addFieldStrategies
+ * @param \Magento\Ui\DataProvider\AddFilterToCollectionInterface[] $addFilterStrategies
+ * @param PoolInterface|null $modifiersPool
+ * @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
$name,
@@ -38,7 +48,8 @@ public function __construct(
array $meta = [],
array $data = [],
array $addFieldStrategies = [],
- array $addFilterStrategies = []
+ array $addFilterStrategies = [],
+ PoolInterface $modifiersPool = null
) {
parent::__construct(
$name,
@@ -52,6 +63,7 @@ public function __construct(
);
$this->dataHelper = $dataHelper;
+ $this->modifiersPool = $modifiersPool ?: ObjectManager::getInstance()->get(PoolInterface::class);
}
/**
@@ -72,11 +84,34 @@ public function getData()
);
$this->getCollection()->load();
}
+
$items = $this->getCollection()->toArray();
- return [
+ $data = [
'totalRecords' => $this->getCollection()->getSize(),
'items' => array_values($items),
];
+
+ /** @var ModifierInterface $modifier */
+ foreach ($this->modifiersPool->getModifiersInstances() as $modifier) {
+ $data = $modifier->modifyData($data);
+ }
+
+ return $data;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getMeta()
+ {
+ $meta = parent::getMeta();
+
+ /** @var ModifierInterface $modifier */
+ foreach ($this->modifiersPool->getModifiersInstances() as $modifier) {
+ $meta = $modifier->modifyMeta($meta);
+ }
+
+ return $meta;
}
}
diff --git a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/AddSelectionQtyTypeToProductsData.php b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/AddSelectionQtyTypeToProductsData.php
new file mode 100644
index 0000000000000..a2170aa30f8d3
--- /dev/null
+++ b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/AddSelectionQtyTypeToProductsData.php
@@ -0,0 +1,76 @@
+stockRegistryPreloader = $stockRegistryPreloader;
+ }
+
+ /**
+ * Modify Meta
+ *
+ * @param array $meta
+ * @return array
+ */
+ public function modifyMeta(array $meta)
+ {
+ return $meta;
+ }
+
+ /**
+ * Modify Data - checks if new selection can have decimal quantity
+ *
+ * @param array $data
+ * @return array
+ * @throws NoSuchEntityException
+ */
+ public function modifyData(array $data): array
+ {
+ $productIds = array_column($data['items'], 'entity_id');
+
+ $stockItems = [];
+ if ($productIds) {
+ $stockItems = $this->stockRegistryPreloader->preloadStockItems($productIds);
+ }
+
+ $isQtyDecimals = [];
+ foreach ($stockItems as $stockItem) {
+ $isQtyDecimals[$stockItem->getProductId()] = $stockItem->getIsQtyDecimal();
+ }
+
+ foreach ($data['items'] as &$item) {
+ if (isset($isQtyDecimals[$item['entity_id']])) {
+ $item['selection_qty_is_integer'] = !$isQtyDecimals[$item['entity_id']];
+ }
+ }
+
+ return $data;
+ }
+}
diff --git a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php
index 4e2f17fa46d45..7b1c254eae6f9 100644
--- a/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php
+++ b/app/code/Magento/Bundle/Ui/DataProvider/Product/Form/Modifier/BundlePanel.php
@@ -403,6 +403,7 @@ protected function getBundleOptions()
'selection_price_type' => '',
'selection_price_value' => '',
'selection_qty' => '',
+ 'selection_qty_is_integer'=> 'selection_qty_is_integer',
],
'links' => [
'insertData' => '${ $.provider }:${ $.dataProvider }',
diff --git a/app/code/Magento/Bundle/etc/adminhtml/di.xml b/app/code/Magento/Bundle/etc/adminhtml/di.xml
index f173bb26fcc3d..4f3069dee65b0 100644
--- a/app/code/Magento/Bundle/etc/adminhtml/di.xml
+++ b/app/code/Magento/Bundle/etc/adminhtml/di.xml
@@ -76,4 +76,21 @@
+
+
+
+ -
+
- Magento\Bundle\Ui\DataProvider\Product\Form\Modifier\AddSelectionQtyTypeToProductsData
+ - 200
+
+
+
+
+
+
+
+ Magento\Bundle\Ui\DataProvider\Product\Form\Modifier\ModifiersPool
+
+
+
diff --git a/app/code/Magento/Bundle/etc/di.xml b/app/code/Magento/Bundle/etc/di.xml
index c5c4a491234ed..7601224056bee 100644
--- a/app/code/Magento/Bundle/etc/di.xml
+++ b/app/code/Magento/Bundle/etc/di.xml
@@ -9,6 +9,7 @@
+
diff --git a/app/code/Magento/Bundle/etc/webapi_rest/di.xml b/app/code/Magento/Bundle/etc/webapi_rest/di.xml
index 28a236d1fb359..29f2fd4494107 100644
--- a/app/code/Magento/Bundle/etc/webapi_rest/di.xml
+++ b/app/code/Magento/Bundle/etc/webapi_rest/di.xml
@@ -17,4 +17,9 @@
+
+
+ Magento\Bundle\Model\Sales\Order\BundleOrderTypeValidator
+
+
diff --git a/app/code/Magento/Bundle/view/base/templates/product/price/final_price.phtml b/app/code/Magento/Bundle/view/base/templates/product/price/final_price.phtml
index 26264cc2cc87f..a77a6654247da 100644
--- a/app/code/Magento/Bundle/view/base/templates/product/price/final_price.phtml
+++ b/app/code/Magento/Bundle/view/base/templates/product/price/final_price.phtml
@@ -6,6 +6,7 @@
?>
getIdSuffix() ? $block->getIdSuffix() : '';
/** @var \Magento\Bundle\Pricing\Render\FinalPriceBox $block */
@@ -22,7 +23,7 @@ $regularPriceAttributes = [
'display_label' => __('Regular Price'),
'price_id' => $block->getPriceId('old-price-' . $idSuffix),
'include_container' => true,
- 'skip_adjustments' => true
+ 'skip_adjustments' => false
];
$renderMinimalRegularPrice = $block->renderAmount($minimalRegularPrice, $regularPriceAttributes);
?>
diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml
index 706b28049470e..5f3e219866ba6 100644
--- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml
+++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/radio.phtml
@@ -4,6 +4,8 @@
* See COPYING.txt for license details.
*/
use Magento\Bundle\ViewModel\ValidateQuantity;
+
+// phpcs:disable Generic.Files.LineLength.TooLong
?>
getOption(); ?>
@@ -20,42 +22,45 @@ $viewModel = $block->getData('validateQuantityViewModel');
- showSingle()) : ?>
+ showSingle()): ?>
= /* @noEscape */ $block->getSelectionTitlePrice($_selections[0]) ?>
= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?>
-
- getRequired()) : ?>
+
+ getRequired()): ?>
isSalable())?'':' checked="checked" ' ?>
+ = ($_default && $_default->isSalable())?'':' checked="checked" ' ?>
value=""/>
= $block->escapeHtml(__('None')) ?>
-
+
getRequired()) { echo 'data-validate="{\'validate-one-required-by-name\':true}"'; }?>
+ getRequired()) {
+ echo 'data-validate="{\'validate-one-required-by-name\':true}"';
+ } ?>
name="bundle_option[= $block->escapeHtmlAttr($_option->getId()) ?>]"
data-selector="bundle_option[= $block->escapeHtmlAttr($_option->getId()) ?>]"
- isSelected($_selection)) { echo ' checked="checked"'; } ?>
- isSaleable()) { echo ' disabled="disabled"'; } ?>
- value="= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"/>
+ isSelected($_selection)) { echo ' checked="checked"'; } ?>
+ isSaleable()) { echo ' disabled="disabled"'; } ?>
+ value="= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"
+ data-errors-message-box="#validation-message-box-radio"/>
= /* @noEscape */ $block->getSelectionTitlePrice($_selection) ?>
@@ -65,6 +70,7 @@ $viewModel = $block->getData('validateQuantityViewModel');
+
diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItemLinks.php b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItemLinks.php
index 184f7177a995c..2d842b87faefc 100644
--- a/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItemLinks.php
+++ b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItemLinks.php
@@ -7,12 +7,14 @@
namespace Magento\BundleGraphQl\Model\Resolver;
-use Magento\Framework\Exception\LocalizedException;
-use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\BundleGraphQl\Model\Resolver\Links\Collection;
+use Magento\BundleGraphQl\Model\Resolver\Links\CollectionFactory;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\Resolver\ValueFactory;
use Magento\Framework\GraphQl\Query\ResolverInterface;
+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
/**
* @inheritdoc
@@ -20,24 +22,28 @@
class BundleItemLinks implements ResolverInterface
{
/**
- * @var Collection
+ * @var CollectionFactory
*/
- private $linkCollection;
+ private CollectionFactory $linkCollectionFactory;
/**
* @var ValueFactory
*/
- private $valueFactory;
+ private ValueFactory $valueFactory;
/**
- * @param Collection $linkCollection
+ * @param Collection $linkCollection Deprecated. Use $linkCollectionFactory instead
* @param ValueFactory $valueFactory
+ * @param CollectionFactory|null $linkCollectionFactory
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __construct(
Collection $linkCollection,
- ValueFactory $valueFactory
+ ValueFactory $valueFactory,
+ CollectionFactory $linkCollectionFactory = null
) {
- $this->linkCollection = $linkCollection;
+ $this->linkCollectionFactory = $linkCollectionFactory
+ ?: ObjectManager::getInstance()->get(CollectionFactory::class);
$this->valueFactory = $valueFactory;
}
@@ -49,12 +55,11 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
if (!isset($value['option_id']) || !isset($value['parent_id'])) {
throw new LocalizedException(__('"option_id" and "parent_id" values should be specified'));
}
-
- $this->linkCollection->addIdFilters((int)$value['option_id'], (int)$value['parent_id']);
- $result = function () use ($value) {
- return $this->linkCollection->getLinksForOptionId((int)$value['option_id']);
+ $linkCollection = $this->linkCollectionFactory->create();
+ $linkCollection->addIdFilters((int)$value['option_id'], (int)$value['parent_id']);
+ $result = function () use ($value, $linkCollection) {
+ return $linkCollection->getLinksForOptionId((int)$value['option_id']);
};
-
return $this->valueFactory->create($result);
}
}
diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItems.php b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItems.php
index b67bd69ecf924..028772f5b2884 100644
--- a/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItems.php
+++ b/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItems.php
@@ -7,14 +7,16 @@
namespace Magento\BundleGraphQl\Model\Resolver;
-use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Bundle\Model\Product\Type;
use Magento\BundleGraphQl\Model\Resolver\Options\Collection;
+use Magento\BundleGraphQl\Model\Resolver\Options\CollectionFactory;
use Magento\Catalog\Api\Data\ProductInterface;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\Resolver\ValueFactory;
use Magento\Framework\GraphQl\Query\ResolverInterface;
+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
/**
* @inheritdoc
@@ -22,39 +24,41 @@
class BundleItems implements ResolverInterface
{
/**
- * @var Collection
+ * @var CollectionFactory
*/
- private $bundleOptionCollection;
+ private CollectionFactory $bundleOptionCollectionFactory;
/**
* @var ValueFactory
*/
- private $valueFactory;
+ private ValueFactory $valueFactory;
/**
* @var MetadataPool
*/
- private $metadataPool;
+ private MetadataPool $metadataPool;
/**
- * @param Collection $bundleOptionCollection
+ * @param Collection $bundleOptionCollection Deprecated. Use $bundleOptionCollectionFactory
* @param ValueFactory $valueFactory
* @param MetadataPool $metadataPool
+ * @param CollectionFactory|null $bundleOptionCollectionFactory
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __construct(
Collection $bundleOptionCollection,
ValueFactory $valueFactory,
- MetadataPool $metadataPool
+ MetadataPool $metadataPool,
+ CollectionFactory $bundleOptionCollectionFactory = null
) {
- $this->bundleOptionCollection = $bundleOptionCollection;
+ $this->bundleOptionCollectionFactory = $bundleOptionCollectionFactory
+ ?: ObjectManager::getInstance()->get(CollectionFactory::class);
$this->valueFactory = $valueFactory;
$this->metadataPool = $metadataPool;
}
/**
- * Fetch and format bundle option items.
- *
- * {@inheritDoc}
+ * @inheritDoc
*/
public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null)
{
@@ -68,17 +72,15 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
};
return $this->valueFactory->create($result);
}
-
- $this->bundleOptionCollection->addParentFilterData(
+ $bundleOptionCollection = $this->bundleOptionCollectionFactory->create();
+ $bundleOptionCollection->addParentFilterData(
(int)$value[$linkField],
(int)$value['entity_id'],
$value[ProductInterface::SKU]
);
-
- $result = function () use ($value, $linkField) {
- return $this->bundleOptionCollection->getOptionsByParentId((int)$value[$linkField]);
+ $result = function () use ($value, $linkField, $bundleOptionCollection) {
+ return $bundleOptionCollection->getOptionsByParentId((int)$value[$linkField]);
};
-
return $this->valueFactory->create($result);
}
}
diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php
index 660e65dc36f64..9a4e5b94c40a8 100644
--- a/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php
+++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php
@@ -15,12 +15,13 @@
use Magento\Framework\Exception\RuntimeException;
use Magento\Framework\GraphQl\Query\EnumLookup;
use Magento\Framework\GraphQl\Query\Uid;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Zend_Db_Select_Exception;
/**
* Collection to fetch link data at resolution time.
*/
-class Collection
+class Collection implements ResetAfterRequestInterface
{
/**
* @var CollectionFactory
@@ -156,4 +157,14 @@ private function fetch() : array
return $this->links;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->links = [];
+ $this->optionIds = [];
+ $this->parentIds = [];
+ }
}
diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php
index 2fa0ce6def9d3..5f1fe2c580a72 100644
--- a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php
+++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Collection.php
@@ -11,12 +11,13 @@
use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\GraphQl\Query\Uid;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Store\Model\StoreManagerInterface;
/**
* Collection to fetch bundle option data at resolution time.
*/
-class Collection
+class Collection implements ResetAfterRequestInterface
{
/**
* Option type name
@@ -145,4 +146,13 @@ private function fetch() : array
return $this->optionMap;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->optionMap = [];
+ $this->skuMap = [];
+ }
}
diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Label.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Label.php
index dfdf4e904a475..8da272dce33fb 100644
--- a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Label.php
+++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/Label.php
@@ -7,12 +7,14 @@
namespace Magento\BundleGraphQl\Model\Resolver\Options;
-use Magento\Framework\Exception\LocalizedException;
-use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\Product as ProductDataProvider;
+use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\ProductFactory as ProductDataProviderFactory;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\Resolver\ValueFactory;
use Magento\Framework\GraphQl\Query\ResolverInterface;
+use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
/**
* Bundle product option label resolver
@@ -22,21 +24,27 @@ class Label implements ResolverInterface
/**
* @var ValueFactory
*/
- private $valueFactory;
+ private ValueFactory $valueFactory;
/**
- * @var ProductDataProvider
+ * @var ProductDataProviderFactory
*/
- private $product;
+ private ProductDataProviderFactory $productFactory;
/**
* @param ValueFactory $valueFactory
- * @param ProductDataProvider $product
+ * @param ProductDataProvider $product Deprecated. Use $productFactory
+ * @param ProductDataProviderFactory|null $productFactory
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
- public function __construct(ValueFactory $valueFactory, ProductDataProvider $product)
- {
+ public function __construct(
+ ValueFactory $valueFactory,
+ ProductDataProvider $product,
+ ProductDataProviderFactory $productFactory = null
+ ) {
$this->valueFactory = $valueFactory;
- $this->product = $product;
+ $this->productFactory = $productFactory
+ ?: ObjectManager::getInstance()->get(ProductDataProviderFactory::class);
}
/**
@@ -52,17 +60,15 @@ public function resolve(
if (!isset($value['sku'])) {
throw new LocalizedException(__('"sku" value should be specified'));
}
-
- $this->product->addProductSku($value['sku']);
- $this->product->addEavAttributes(['name']);
-
- $result = function () use ($value, $context) {
- $productData = $this->product->getProductBySku($value['sku'], $context);
+ $product = $this->productFactory->create();
+ $product->addProductSku($value['sku']);
+ $product->addEavAttributes(['name']);
+ $result = function () use ($value, $context, $product) {
+ $productData = $product->getProductBySku($value['sku'], $context);
/** @var \Magento\Catalog\Model\Product $productModel */
$productModel = isset($productData['model']) ? $productData['model'] : null;
return $productModel ? $productModel->getName() : null;
};
-
return $this->valueFactory->create($result);
}
}
diff --git a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php
index c6a6ba0eb05e5..2917a23d1005b 100644
--- a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php
+++ b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php
@@ -14,6 +14,7 @@
use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\EntityManager\MetadataPool;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\ImportExport\Model\Import;
use Magento\Store\Model\Store;
use Magento\Store\Model\StoreManagerInterface;
@@ -24,7 +25,8 @@
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
-class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType
+class Bundle extends \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType implements
+ ResetAfterRequestInterface
{
/**
* Delimiter before product option value.
@@ -783,4 +785,15 @@ private function getStoreIdByCode(string $storeCode): int
return $this->storeCodeToId[$storeCode];
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->_cachedOptions = [];
+ $this->_cachedSkus = [];
+ $this->_cachedOptionSelectQuery = [];
+ $this->_cachedSkuToProducts = [];
+ }
}
diff --git a/app/code/Magento/CacheInvalidate/README.md b/app/code/Magento/CacheInvalidate/README.md
index 6cca6ffec03e4..f12b0435e71bc 100644
--- a/app/code/Magento/CacheInvalidate/README.md
+++ b/app/code/Magento/CacheInvalidate/README.md
@@ -1,2 +1,2 @@
The CacheInvalidate module is used to invalidate the Varnish cache if it is configured.
-It listens for events that request the cache to be flushed or cause the cache to be invalid, then sends Varnish a purge request using cURL.
\ No newline at end of file
+It listens for events that request the cache to be flushed or cause the cache to be invalid, then sends Varnish a purge request using cURL.
diff --git a/app/code/Magento/Captcha/README.md b/app/code/Magento/Captcha/README.md
index 35979fb2b4892..d4119e03e1d9f 100644
--- a/app/code/Magento/Captcha/README.md
+++ b/app/code/Magento/Captcha/README.md
@@ -1 +1 @@
-The Captcha module allows applying Turing test in the process of user authentication or similar tasks.
\ No newline at end of file
+The Captcha module allows applying Turing test in the process of user authentication or similar tasks.
diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaFormsDisplayingTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaFormsDisplayingTest.xml
index 132d5628b400d..6ebb0fda03089 100644
--- a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaFormsDisplayingTest.xml
+++ b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaFormsDisplayingTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaCheckoutWithEnabledCaptchaTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaCheckoutWithEnabledCaptchaTest.xml
index 3a55535e33ae0..d89b80e76ab64 100644
--- a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaCheckoutWithEnabledCaptchaTest.xml
+++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaCheckoutWithEnabledCaptchaTest.xml
@@ -57,7 +57,7 @@
-
+
diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnOnepageCheckoutPyamentTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnOnepageCheckoutPyamentTest.xml
index 912e637dc534e..4ab4ec7f055f9 100644
--- a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnOnepageCheckoutPyamentTest.xml
+++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnOnepageCheckoutPyamentTest.xml
@@ -21,6 +21,7 @@
+
20
@@ -62,6 +63,7 @@
+
diff --git a/app/code/Magento/CardinalCommerce/README.md b/app/code/Magento/CardinalCommerce/README.md
index 54db9114a2a0e..aa68470a496bc 100644
--- a/app/code/Magento/CardinalCommerce/README.md
+++ b/app/code/Magento/CardinalCommerce/README.md
@@ -1 +1 @@
-The CardinalCommerce module provides a possibility to enable 3-D Secure 2.0 support for payment methods.
\ No newline at end of file
+The CardinalCommerce module provides a possibility to enable 3-D Secure 2.0 support for payment methods.
diff --git a/app/code/Magento/Catalog/Block/Breadcrumbs.php b/app/code/Magento/Catalog/Block/Breadcrumbs.php
index 674c99001b01a..558b833f0794a 100644
--- a/app/code/Magento/Catalog/Block/Breadcrumbs.php
+++ b/app/code/Magento/Catalog/Block/Breadcrumbs.php
@@ -16,8 +16,6 @@
class Breadcrumbs extends \Magento\Framework\View\Element\Template
{
/**
- * Catalog data
- *
* @var Data
*/
protected $_catalogData = null;
@@ -66,15 +64,11 @@ protected function _prepareLayout()
]
);
- $title = [];
$path = $this->_catalogData->getBreadcrumbPath();
foreach ($path as $name => $breadcrumb) {
$breadcrumbsBlock->addCrumb($name, $breadcrumb);
- $title[] = $breadcrumb['label'];
}
-
- $this->pageConfig->getTitle()->set(join($this->getTitleSeparator(), array_reverse($title)));
}
return parent::_prepareLayout();
}
diff --git a/app/code/Magento/Catalog/Block/Category/View.php b/app/code/Magento/Catalog/Block/Category/View.php
index da0211ad20552..a91f33ba74340 100644
--- a/app/code/Magento/Catalog/Block/Category/View.php
+++ b/app/code/Magento/Catalog/Block/Category/View.php
@@ -6,23 +6,18 @@
namespace Magento\Catalog\Block\Category;
/**
- * Class View
+ * Category View Block class
* @api
- * @package Magento\Catalog\Block\Category
* @since 100.0.2
*/
class View extends \Magento\Framework\View\Element\Template implements \Magento\Framework\DataObject\IdentityInterface
{
/**
- * Core registry
- *
* @var \Magento\Framework\Registry
*/
protected $_coreRegistry = null;
/**
- * Catalog layer
- *
* @var \Magento\Catalog\Model\Layer
*/
protected $_catalogLayer;
@@ -32,40 +27,56 @@ class View extends \Magento\Framework\View\Element\Template implements \Magento\
*/
protected $_categoryHelper;
+ /**
+ * @var \Magento\Catalog\Helper\Data|null
+ */
+ private $catalogData;
+
/**
* @param \Magento\Framework\View\Element\Template\Context $context
* @param \Magento\Catalog\Model\Layer\Resolver $layerResolver
* @param \Magento\Framework\Registry $registry
* @param \Magento\Catalog\Helper\Category $categoryHelper
* @param array $data
+ * @param \Magento\Catalog\Helper\Data|null $catalogData
*/
public function __construct(
\Magento\Framework\View\Element\Template\Context $context,
\Magento\Catalog\Model\Layer\Resolver $layerResolver,
\Magento\Framework\Registry $registry,
\Magento\Catalog\Helper\Category $categoryHelper,
- array $data = []
+ array $data = [],
+ \Magento\Catalog\Helper\Data $catalogData = null
) {
$this->_categoryHelper = $categoryHelper;
$this->_catalogLayer = $layerResolver->get();
$this->_coreRegistry = $registry;
+ $this->catalogData = $catalogData ?? \Magento\Framework\App\ObjectManager::getInstance()
+ ->get(\Magento\Catalog\Helper\Data::class);
parent::__construct($context, $data);
}
/**
+ * @inheritdoc
* @return $this
*/
protected function _prepareLayout()
{
parent::_prepareLayout();
- $this->getLayout()->createBlock(\Magento\Catalog\Block\Breadcrumbs::class);
+ $block = $this->getLayout()->createBlock(\Magento\Catalog\Block\Breadcrumbs::class);
$category = $this->getCurrentCategory();
if ($category) {
$title = $category->getMetaTitle();
if ($title) {
$this->pageConfig->getTitle()->set($title);
+ } else {
+ $title = [];
+ foreach ($this->catalogData->getBreadcrumbPath() as $breadcrumb) {
+ $title[] = $breadcrumb['label'];
+ }
+ $this->pageConfig->getTitle()->set(join($block->getTitleSeparator(), array_reverse($title)));
}
$description = $category->getMetaDescription();
if ($description) {
@@ -93,6 +104,8 @@ protected function _prepareLayout()
}
/**
+ * Return Product list html
+ *
* @return string
*/
public function getProductListHtml()
@@ -114,6 +127,8 @@ public function getCurrentCategory()
}
/**
+ * Return CMS block html
+ *
* @return mixed
*/
public function getCmsBlockHtml()
@@ -131,6 +146,7 @@ public function getCmsBlockHtml()
/**
* Check if category display mode is "Products Only"
+ *
* @return bool
*/
public function isProductMode()
@@ -140,6 +156,7 @@ public function isProductMode()
/**
* Check if category display mode is "Static Block and Products"
+ *
* @return bool
*/
public function isMixedMode()
@@ -149,6 +166,7 @@ public function isMixedMode()
/**
* Check if category display mode is "Static Block Only"
+ *
* For anchor category with applied filter Static Block Only mode not allowed
*
* @return bool
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php
index 4491ad1ee99b5..95b7a9bfe5204 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php
@@ -448,9 +448,9 @@ private function getLinkResolver()
}
/**
- * Remove ids of non selected websites from $websiteIds array and return filtered data
+ * Remove ids of non-selected websites from $websiteIds array and return filtered data
*
- * $websiteIds parameter expects array with website ids as keys and 1 (selected) or 0 (non selected) as values
+ * $websiteIds parameter expects array with website ids as keys and id (selected) or 0 (non-selected) as values
* Only one id (default website ID) will be set to $websiteIds array when the single store mode is turned on
*
* @param array $websiteIds
@@ -461,7 +461,8 @@ private function filterWebsiteIds($websiteIds)
if (!$this->storeManager->isSingleStoreMode()) {
$websiteIds = array_filter((array) $websiteIds);
} else {
- $websiteIds[$this->storeManager->getWebsite(true)->getId()] = 1;
+ $websiteId = $this->storeManager->getWebsite(true)->getId();
+ $websiteIds[$websiteId] = $websiteId;
}
return $websiteIds;
diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/NewAction.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/NewAction.php
index 0b1ef98c386c4..ea14dbc1ce627 100644
--- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/NewAction.php
+++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/NewAction.php
@@ -4,18 +4,21 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\Catalog\Controller\Adminhtml\Product;
-use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface;
-use Magento\Backend\App\Action;
use Magento\Catalog\Controller\Adminhtml\Product;
+use Magento\Framework\App\Action\HttpGetActionInterface as HttpGetActionInterface;
use Magento\Framework\App\ObjectManager;
+use Magento\Framework\RegexValidator;
class NewAction extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpGetActionInterface
{
/**
* @var Initialization\StockDataFilter
* @deprecated 101.0.0
+ * @see Initialization\StockDataFilter
*/
protected $stockFilter;
@@ -30,23 +33,32 @@ class NewAction extends \Magento\Catalog\Controller\Adminhtml\Product implements
protected $resultForwardFactory;
/**
- * @param Action\Context $context
+ * @var RegexValidator
+ */
+ private RegexValidator $regexValidator;
+
+ /**
+ * @param Context $context
* @param Builder $productBuilder
* @param Initialization\StockDataFilter $stockFilter
* @param \Magento\Framework\View\Result\PageFactory $resultPageFactory
* @param \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory
+ * @param RegexValidator|null $regexValidator
*/
public function __construct(
\Magento\Backend\App\Action\Context $context,
Product\Builder $productBuilder,
Initialization\StockDataFilter $stockFilter,
\Magento\Framework\View\Result\PageFactory $resultPageFactory,
- \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory
+ \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory,
+ RegexValidator $regexValidator = null
) {
$this->stockFilter = $stockFilter;
parent::__construct($context, $productBuilder);
$this->resultPageFactory = $resultPageFactory;
$this->resultForwardFactory = $resultForwardFactory;
+ $this->regexValidator = $regexValidator
+ ?: ObjectManager::getInstance()->get(RegexValidator::class);
}
/**
@@ -56,6 +68,11 @@ public function __construct(
*/
public function execute()
{
+ $typeId = $this->getRequest()->getParam('type');
+ if (!$this->regexValidator->validateParamRegex($typeId)) {
+ return $this->resultForwardFactory->create()->forward('noroute');
+ }
+
if (!$this->getRequest()->getParam('set')) {
return $this->resultForwardFactory->create()->forward('noroute');
}
diff --git a/app/code/Magento/Catalog/Helper/Category.php b/app/code/Magento/Catalog/Helper/Category.php
index ae42acf4b196e..c09ba2df1aac0 100644
--- a/app/code/Magento/Catalog/Helper/Category.php
+++ b/app/code/Magento/Catalog/Helper/Category.php
@@ -9,6 +9,7 @@
use Magento\Catalog\Model\Category as ModelCategory;
use Magento\Framework\App\Helper\AbstractHelper;
use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Store\Model\Store;
/**
@@ -16,11 +17,11 @@
*
* @SuppressWarnings(PHPMD.LongVariable)
*/
-class Category extends AbstractHelper
+class Category extends AbstractHelper implements ResetAfterRequestInterface
{
- const XML_PATH_USE_CATEGORY_CANONICAL_TAG = 'catalog/seo/category_canonical_tag';
+ public const XML_PATH_USE_CATEGORY_CANONICAL_TAG = 'catalog/seo/category_canonical_tag';
- const XML_PATH_CATEGORY_ROOT_ID = 'catalog/category/root_id';
+ public const XML_PATH_CATEGORY_ROOT_ID = 'catalog/category/root_id';
/**
* Store categories cache
@@ -30,14 +31,14 @@ class Category extends AbstractHelper
protected $_storeCategories = [];
/**
- * Store manager
+ * Store manager instance
*
* @var \Magento\Store\Model\StoreManagerInterface
*/
protected $_storeManager;
/**
- * Category factory
+ * Category factory instance
*
* @var \Magento\Catalog\Model\CategoryFactory
*/
@@ -176,4 +177,12 @@ public function canUseCanonicalTag($store = null)
$store
);
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->_storeCategories = [];
+ }
}
diff --git a/app/code/Magento/Catalog/Model/CategoryRepository.php b/app/code/Magento/Catalog/Model/CategoryRepository.php
index 7082fa4747fdc..4e85853cd33bc 100644
--- a/app/code/Magento/Catalog/Model/CategoryRepository.php
+++ b/app/code/Magento/Catalog/Model/CategoryRepository.php
@@ -7,6 +7,7 @@
namespace Magento\Catalog\Model;
+use Magento\Catalog\Api\CategoryRepositoryInterface;
use Magento\Catalog\Model\CategoryRepository\PopulateWithValues;
use Magento\Catalog\Model\ResourceModel\Category as CategoryResource;
use Magento\Framework\Api\ExtensibleDataObjectConverter;
@@ -16,6 +17,7 @@
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Exception\StateException;
use Magento\Catalog\Api\Data\CategoryInterface;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Store\Model\StoreManagerInterface;
/**
@@ -23,7 +25,7 @@
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
-class CategoryRepository implements \Magento\Catalog\Api\CategoryRepositoryInterface
+class CategoryRepository implements CategoryRepositoryInterface, ResetAfterRequestInterface
{
/**
* @var Category[]
@@ -231,6 +233,7 @@ protected function validateCategory(Category $category)
* @return ExtensibleDataObjectConverter
*
* @deprecated 101.0.0
+ * @see we don't recommend this approach anymore
*/
private function getExtensibleDataObjectConverter()
{
@@ -254,4 +257,12 @@ private function getMetadataPool()
}
return $this->metadataPool;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->instances = [];
+ }
}
diff --git a/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php b/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php
index c6feb049e1a10..f38597c67f2ab 100644
--- a/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php
+++ b/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php
@@ -15,12 +15,13 @@
use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface;
use Magento\Framework\Api\FilterBuilder;
use Magento\Framework\Api\SearchCriteriaBuilder;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Store\Model\Store;
/**
* Add data to category entity and populate with default values
*/
-class PopulateWithValues
+class PopulateWithValues implements ResetAfterRequestInterface
{
/**
* @var ScopeOverriddenValue
@@ -150,4 +151,12 @@ private function getAttributes(): array
return $this->attributes;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->attributes = [];
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php
index 49d8336dddb21..4119335d37b41 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php
@@ -13,6 +13,7 @@
use Magento\Framework\DB\Query\Generator as QueryGenerator;
use Magento\Framework\DB\Select;
use Magento\Framework\EntityManager\MetadataPool;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Store\Api\Data\StoreInterface;
use Magento\Store\Model\Store;
@@ -24,8 +25,9 @@
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @since 100.0.2
+ * phpcs:disable Magento2.Annotation.MethodAnnotationStructure.InvalidDeprecatedTagUsage
*/
-abstract class AbstractAction
+abstract class AbstractAction implements ResetAfterRequestInterface
{
/**
* Chunk size
@@ -44,7 +46,8 @@ abstract class AbstractAction
/**
* Suffix for table to show it is temporary
- * @deprecated see getIndexTable
+ * @deprecated
+ * @see getIndexTable
*/
public const TEMPORARY_TABLE_SUFFIX = '_tmp';
@@ -156,6 +159,20 @@ public function __construct(
$this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class);
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->nonAnchorSelects = [];
+ $this->anchorSelects = [];
+ $this->productsSelects = [];
+ $this->categoryPath = [];
+ $this->useTempTable = true;
+ $this->tempTreeIndexTableName = null;
+ $this->currentStore = null;
+ }
+
/**
* Run full reindex
*
diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/Website.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/Website.php
index 50700e672237e..fc06f6228d043 100644
--- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/Website.php
+++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/Website.php
@@ -6,6 +6,7 @@
declare(strict_types=1);
namespace Magento\Catalog\Model\Indexer\Category\Product\Plugin;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Model\ResourceModel\Db\AbstractDb;
use Magento\Framework\Model\AbstractModel;
use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer;
@@ -17,13 +18,22 @@ class Website
*/
private $tableMaintainer;
+ /**
+ * @var \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface
+ */
+ private $pillPut;
+
/**
* @param TableMaintainer $tableMaintainer
+ * @param \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface|null $pillPut
*/
public function __construct(
- TableMaintainer $tableMaintainer
+ TableMaintainer $tableMaintainer,
+ \Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface $pillPut = null
) {
$this->tableMaintainer = $tableMaintainer;
+ $this->pillPut = $pillPut ?: ObjectManager::getInstance()
+ ->get(\Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface::class);
}
/**
@@ -35,12 +45,14 @@ public function __construct(
*
* @return AbstractDb
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ * @throws \Exception
*/
public function afterDelete(AbstractDb $subject, AbstractDb $objectResource, AbstractModel $website)
{
foreach ($website->getStoreIds() as $storeId) {
$this->tableMaintainer->dropTablesForStore((int)$storeId);
}
+ $this->pillPut->put();
return $objectResource;
}
}
diff --git a/app/code/Magento/Catalog/Model/Layer.php b/app/code/Magento/Catalog/Model/Layer.php
index fb94e82f0007c..65f2db475e02e 100644
--- a/app/code/Magento/Catalog/Model/Layer.php
+++ b/app/code/Magento/Catalog/Model/Layer.php
@@ -8,6 +8,7 @@
use Magento\Catalog\Api\CategoryRepositoryInterface;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory as AttributeCollectionFactory;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Catalog view layer model
@@ -17,7 +18,7 @@
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @since 100.0.2
*/
-class Layer extends \Magento\Framework\DataObject
+class Layer extends \Magento\Framework\DataObject implements ResetAfterRequestInterface
{
/**
* Product collections array
@@ -41,29 +42,21 @@ class Layer extends \Magento\Framework\DataObject
protected $registry = null;
/**
- * Store manager
- *
* @var \Magento\Store\Model\StoreManagerInterface
*/
protected $_storeManager;
/**
- * Catalog product
- *
* @var \Magento\Catalog\Model\ResourceModel\Product
*/
protected $_catalogProduct;
/**
- * Attribute collection factory
- *
* @var AttributeCollectionFactory
*/
protected $_attributeCollectionFactory;
/**
- * Layer state factory
- *
* @var \Magento\Catalog\Model\Layer\StateFactory
*/
protected $_layerStateFactory;
@@ -187,6 +180,7 @@ public function apply()
/**
* Retrieve current category model
+ *
* If no category found in registry, the root will be taken
*
* @return \Magento\Catalog\Model\Category
@@ -266,4 +260,12 @@ public function getState()
return $state;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->_productCollections = [];
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Layer/FilterList.php b/app/code/Magento/Catalog/Model/Layer/FilterList.php
index 86a7d1fb61938..08d0441e919f2 100644
--- a/app/code/Magento/Catalog/Model/Layer/FilterList.php
+++ b/app/code/Magento/Catalog/Model/Layer/FilterList.php
@@ -9,16 +9,17 @@
use Magento\Catalog\Model\Config\LayerCategoryConfig;
use Magento\Framework\App\ObjectManager;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Layer navigation filters
*/
-class FilterList
+class FilterList implements ResetAfterRequestInterface
{
- const CATEGORY_FILTER = 'category';
- const ATTRIBUTE_FILTER = 'attribute';
- const PRICE_FILTER = 'price';
- const DECIMAL_FILTER = 'decimal';
+ public const CATEGORY_FILTER = 'category';
+ public const ATTRIBUTE_FILTER = 'attribute';
+ public const PRICE_FILTER = 'price';
+ public const DECIMAL_FILTER = 'decimal';
/**
* Filter factory
@@ -131,4 +132,12 @@ protected function getAttributeFilterClass(\Magento\Catalog\Model\ResourceModel\
return $filterClassName;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->filters = [];
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Layer/Resolver.php b/app/code/Magento/Catalog/Model/Layer/Resolver.php
index a4224aeafe7e0..b6ca16f1ac029 100644
--- a/app/code/Magento/Catalog/Model/Layer/Resolver.php
+++ b/app/code/Magento/Catalog/Model/Layer/Resolver.php
@@ -9,15 +9,17 @@
namespace Magento\Catalog\Model\Layer;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
+
/**
* Layer Resolver
*
* @api
*/
-class Resolver
+class Resolver implements ResetAfterRequestInterface
{
- const CATALOG_LAYER_CATEGORY = 'category';
- const CATALOG_LAYER_SEARCH = 'search';
+ public const CATALOG_LAYER_CATEGORY = 'category';
+ public const CATALOG_LAYER_SEARCH = 'search';
/**
* Catalog view layer models list
@@ -79,4 +81,12 @@ public function get()
}
return $this->layer;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->layer = null;
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php
index 0191c0239fc20..910d65a028382 100644
--- a/app/code/Magento/Catalog/Model/Product.php
+++ b/app/code/Magento/Catalog/Model/Product.php
@@ -16,6 +16,7 @@
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\DataObject\IdentityInterface;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Framework\Pricing\SaleableInterface;
/**
@@ -43,7 +44,8 @@
class Product extends \Magento\Catalog\Model\AbstractModel implements
IdentityInterface,
SaleableInterface,
- ProductInterface
+ ProductInterface,
+ ResetAfterRequestInterface
{
/**
* @var ProductLinkRepositoryInterface
@@ -55,22 +57,22 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements
* Entity code.
* Can be used as part of method name for entity processing
*/
- const ENTITY = 'catalog_product';
+ public const ENTITY = 'catalog_product';
/**
* Product cache tag
*/
- const CACHE_TAG = 'cat_p';
+ public const CACHE_TAG = 'cat_p';
/**
* Category product relation cache tag
*/
- const CACHE_PRODUCT_CATEGORY_TAG = 'cat_c_p';
+ public const CACHE_PRODUCT_CATEGORY_TAG = 'cat_c_p';
/**
* Product Store Id
*/
- const STORE_ID = 'store_id';
+ public const STORE_ID = 'store_id';
/**
* @var string|bool
@@ -170,8 +172,6 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements
protected $_calculatePrice = true;
/**
- * Catalog product
- *
* @var \Magento\Catalog\Helper\Product
*/
protected $_catalogProduct = null;
@@ -187,43 +187,31 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements
protected $_collectionFactory;
/**
- * Catalog product type
- *
* @var Product\Type
*/
protected $_catalogProductType;
/**
- * Catalog product media config
- *
* @var Product\Media\Config
*/
protected $_catalogProductMediaConfig;
/**
- * Catalog product status
- *
* @var Status
*/
protected $_catalogProductStatus;
/**
- * Catalog product visibility
- *
* @var Product\Visibility
*/
protected $_catalogProductVisibility;
/**
- * Stock item factory
- *
* @var \Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory
*/
protected $_stockItemFactory;
/**
- * Item option factory
- *
* @var \Magento\Catalog\Model\Product\Configuration\Item\OptionFactory
*/
protected $_itemOptionFactory;
@@ -279,27 +267,28 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements
/**
* @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface
- * @deprecated 102.0.6 Not used anymore due to performance issue (loaded all product attributes)
+ * @deprecated 102.0.6
+ * @see Not used anymore due to performance issue (loaded all product attributes)
*/
protected $metadataService;
/**
- * @param \Magento\Catalog\Model\ProductLink\CollectionProvider
+ * @var \Magento\Catalog\Model\ProductLink\CollectionProvider
*/
protected $entityCollectionProvider;
/**
- * @param \Magento\Catalog\Model\Product\LinkTypeProvider
+ * @var \Magento\Catalog\Model\Product\LinkTypeProvider
*/
protected $linkProvider;
/**
- * @param \Magento\Catalog\Api\Data\ProductLinkInterfaceFactory
+ * @var \Magento\Catalog\Api\Data\ProductLinkInterfaceFactory
*/
protected $productLinkFactory;
/**
- * @param \Magento\Catalog\Api\Data\ProductLinkExtensionFactory
+ * @var \Magento\Catalog\Api\Data\ProductLinkExtensionFactory
*/
protected $productLinkExtensionFactory;
@@ -494,7 +483,8 @@ protected function _construct()
*
* @throws \Magento\Framework\Exception\LocalizedException
* @return \Magento\Catalog\Model\ResourceModel\Product
- * @deprecated 102.0.6 because resource models should be used directly
+ * @deprecated 102.0.6
+ * @see \Magento\Catalog\Model\ResourceModel\Product
* @since 102.0.6
*/
protected function _getResource()
@@ -643,6 +633,7 @@ public function getUpdatedAt()
* @param bool $calculate
* @return void
* @deprecated 102.0.4
+ * @see we don't recommend this approach anymore
*/
public function setPriceCalculation($calculate = true)
{
@@ -2841,4 +2832,15 @@ public function setStockData($stockData)
$this->setData('stock_data', $stockData);
return $this;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->_customOptions = [];
+ $this->_errors = [];
+ $this->_canAffectOptions = [];
+ $this->_productIdCached = null;
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php
index 68aeabfc70d34..4623c095e6d8c 100644
--- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php
+++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/GroupPrice/AbstractGroupPrice.php
@@ -10,13 +10,14 @@
use Magento\Catalog\Model\Attribute\ScopeOverriddenValue;
use Magento\Catalog\Model\Product\Attribute\Backend\Price;
use Magento\Customer\Api\GroupManagementInterface;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Catalog product abstract group price backend attribute model
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
-abstract class AbstractGroupPrice extends Price
+abstract class AbstractGroupPrice extends Price implements ResetAfterRequestInterface
{
/**
* @var \Magento\Framework\EntityManager\MetadataPool
@@ -26,7 +27,7 @@ abstract class AbstractGroupPrice extends Price
/**
* Website currency codes and rates
*
- * @var array
+ * @var array|null
*/
protected $_rates;
@@ -39,8 +40,6 @@ abstract class AbstractGroupPrice extends Price
abstract protected function _getDuplicateErrorMessage();
/**
- * Catalog product type
- *
* @var \Magento\Catalog\Model\Product\Type
*/
protected $_catalogProductType;
@@ -82,6 +81,14 @@ public function __construct(
);
}
+ /**
+ * @inheritdoc
+ */
+ public function _resetState() : void
+ {
+ $this->_rates = null;
+ }
+
/**
* Retrieve websites currency rates and base currency codes
*
diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php
index c0a13aa8b934a..020176738160e 100644
--- a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php
+++ b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php
@@ -11,50 +11,62 @@
*/
namespace Magento\Catalog\Model\Product\Attribute\Source;
+use Magento\Directory\Model\CountryFactory;
use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource;
+use Magento\Framework\App\Cache\Type\Config;
use Magento\Framework\Data\OptionSourceInterface;
+use Magento\Framework\Locale\ResolverInterface;
+use Magento\Framework\Serialize\SerializerInterface;
+use Magento\Store\Model\StoreManagerInterface;
class Countryofmanufacture extends AbstractSource implements OptionSourceInterface
{
/**
- * @var \Magento\Framework\App\Cache\Type\Config
+ * @var Config
*/
protected $_configCacheType;
/**
- * Store manager
- *
- * @var \Magento\Store\Model\StoreManagerInterface
+ * @var StoreManagerInterface
*/
protected $_storeManager;
/**
- * Country factory
- *
- * @var \Magento\Directory\Model\CountryFactory
+ * @var CountryFactory
*/
protected $_countryFactory;
/**
- * @var \Magento\Framework\Serialize\SerializerInterface
+ * @var SerializerInterface
*/
private $serializer;
+ /**
+ * @var ResolverInterface
+ */
+ private $localeResolver;
+
/**
* Construct
*
- * @param \Magento\Directory\Model\CountryFactory $countryFactory
- * @param \Magento\Store\Model\StoreManagerInterface $storeManager
- * @param \Magento\Framework\App\Cache\Type\Config $configCacheType
+ * @param CountryFactory $countryFactory
+ * @param StoreManagerInterface $storeManager
+ * @param Config $configCacheType
+ * @param ResolverInterface $localeResolver
+ * @param SerializerInterface $serializer
*/
public function __construct(
- \Magento\Directory\Model\CountryFactory $countryFactory,
- \Magento\Store\Model\StoreManagerInterface $storeManager,
- \Magento\Framework\App\Cache\Type\Config $configCacheType
+ CountryFactory $countryFactory,
+ StoreManagerInterface $storeManager,
+ Config $configCacheType,
+ ResolverInterface $localeResolver,
+ SerializerInterface $serializer
) {
$this->_countryFactory = $countryFactory;
$this->_storeManager = $storeManager;
$this->_configCacheType = $configCacheType;
+ $this->localeResolver = $localeResolver;
+ $this->serializer = $serializer;
}
/**
@@ -64,32 +76,20 @@ public function __construct(
*/
public function getAllOptions()
{
- $cacheKey = 'COUNTRYOFMANUFACTURE_SELECT_STORE_' . $this->_storeManager->getStore()->getCode();
+ $storeCode = $this->_storeManager->getStore()->getCode();
+ $locale = $this->localeResolver->getLocale();
+
+ $cacheKey = 'COUNTRYOFMANUFACTURE_SELECT_STORE_' . $storeCode . '_LOCALE_' . $locale;
if ($cache = $this->_configCacheType->load($cacheKey)) {
- $options = $this->getSerializer()->unserialize($cache);
+ $options = $this->serializer->unserialize($cache);
} else {
/** @var \Magento\Directory\Model\Country $country */
$country = $this->_countryFactory->create();
/** @var \Magento\Directory\Model\ResourceModel\Country\Collection $collection */
$collection = $country->getResourceCollection();
$options = $collection->load()->toOptionArray();
- $this->_configCacheType->save($this->getSerializer()->serialize($options), $cacheKey);
+ $this->_configCacheType->save($this->serializer->serialize($options), $cacheKey);
}
return $options;
}
-
- /**
- * Get serializer
- *
- * @return \Magento\Framework\Serialize\SerializerInterface
- * @deprecated 102.0.0
- */
- private function getSerializer()
- {
- if ($this->serializer === null) {
- $this->serializer = \Magento\Framework\App\ObjectManager::getInstance()
- ->get(\Magento\Framework\Serialize\SerializerInterface::class);
- }
- return $this->serializer;
- }
}
diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php b/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php
index 4034c75f7373c..85a69a9e69be0 100644
--- a/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php
+++ b/app/code/Magento/Catalog/Model/Product/Gallery/GalleryManagement.php
@@ -6,6 +6,7 @@
namespace Magento\Catalog\Model\Product\Gallery;
+use Magento\AwsS3\Driver\AwsS3;
use Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterface;
use Magento\Catalog\Api\Data\ProductInterfaceFactory;
use Magento\Catalog\Api\ProductRepositoryInterface;
@@ -287,10 +288,18 @@ private function getImageContent($product, $entry): ImageContentInterface
$mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA);
$path = $mediaDirectory->getAbsolutePath($product->getMediaConfig()->getMediaPath($entry->getFile()));
$fileName = $this->file->getPathInfo($path)['basename'];
- $imageFileContent = $mediaDirectory->getDriver()->fileGetContents($path);
+ $fileDriver = $mediaDirectory->getDriver();
+ $imageFileContent = $fileDriver->fileGetContents($path);
+
+ if ($fileDriver instanceof AwsS3) {
+ $remoteMediaMimeType = $fileDriver->getMetadata($path);
+ $mediaMimeType = $remoteMediaMimeType['mimetype'];
+ } else {
+ $mediaMimeType = $this->mime->getMimeType($path);
+ }
return $this->imageContentInterface->create()
->setName($fileName)
->setBase64EncodedData(base64_encode($imageFileContent))
- ->setType($this->mime->getMimeType($path));
+ ->setType($mediaMimeType);
}
}
diff --git a/app/code/Magento/Catalog/Model/Product/Image/Cache.php b/app/code/Magento/Catalog/Model/Product/Image/Cache.php
index c5e5e0ecac4c0..0105a224b2c0d 100644
--- a/app/code/Magento/Catalog/Model/Product/Image/Cache.php
+++ b/app/code/Magento/Catalog/Model/Product/Image/Cache.php
@@ -7,11 +7,12 @@
use Magento\Catalog\Helper\Image as ImageHelper;
use Magento\Catalog\Model\Product;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Theme\Model\ResourceModel\Theme\Collection as ThemeCollection;
use Magento\Framework\App\Area;
use Magento\Framework\View\ConfigInterface;
-class Cache
+class Cache implements ResetAfterRequestInterface
{
/**
* @var ConfigInterface
@@ -66,6 +67,7 @@ protected function getData()
]);
$images = $config->getMediaEntities('Magento_Catalog', ImageHelper::MEDIA_TYPE_CONFIG_NODE);
foreach ($images as $imageId => $imageData) {
+ // phpcs:ignore Magento2.Performance.ForeachArrayMerge
$this->data[$theme->getCode() . $imageId] = array_merge(['id' => $imageId], $imageData);
}
}
@@ -127,4 +129,12 @@ protected function processImageData(Product $product, array $imageData, $file)
return $this;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->data = [];
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Product/Media/Config.php b/app/code/Magento/Catalog/Model/Product/Media/Config.php
index 2297f39829aa3..99c2513f5187a 100644
--- a/app/code/Magento/Catalog/Model/Product/Media/Config.php
+++ b/app/code/Magento/Catalog/Model/Product/Media/Config.php
@@ -7,6 +7,7 @@
namespace Magento\Catalog\Model\Product\Media;
use Magento\Eav\Model\Entity\Attribute;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Framework\UrlInterface;
use Magento\Store\Model\StoreManagerInterface;
@@ -16,7 +17,7 @@
* @api
* @since 100.0.2
*/
-class Config implements ConfigInterface
+class Config implements ConfigInterface, ResetAfterRequestInterface
{
/**
* @var StoreManagerInterface
@@ -29,7 +30,7 @@ class Config implements ConfigInterface
private $attributeHelper;
/**
- * @var string[]
+ * @var string[]|null
*/
private $mediaAttributeCodes;
@@ -199,4 +200,12 @@ private function getAttributeHelper()
}
return $this->attributeHelper;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->mediaAttributeCodes = null;
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php
index 225f1bb3d10e3..6a894278a0494 100644
--- a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php
+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php
@@ -13,6 +13,7 @@
use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface;
use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface;
use Magento\Catalog\Model\Product\Option\Value;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Catalog product option default type
@@ -23,7 +24,7 @@
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @since 100.0.2
*/
-class DefaultType extends \Magento\Framework\DataObject
+class DefaultType extends \Magento\Framework\DataObject implements ResetAfterRequestInterface
{
/**
* Option Instance
@@ -426,4 +427,12 @@ protected function _getChargeableOptionPrice($price, $isPercent, $basePrice)
return $price;
}
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->_productOptions = [];
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/RequestAwareValidatorFile.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/RequestAwareValidatorFile.php
new file mode 100644
index 0000000000000..609d02e33757a
--- /dev/null
+++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/RequestAwareValidatorFile.php
@@ -0,0 +1,70 @@
+request = $request ?: ObjectManager::getInstance()->get(Request::class);
+ parent::__construct(
+ $scopeConfig,
+ $filesystem,
+ $fileSize,
+ $httpFactory,
+ $isImageValidator,
+ $random
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function validateContentLength(): bool
+ {
+ return isset($this->request->getServer()['CONTENT_LENGTH'])
+ && $this->request->getServer()['CONTENT_LENGTH'] > $this->fileSize->getMaxFileSize();
+ }
+}
diff --git a/app/code/Magento/Catalog/Model/Product/Price/TierPriceFactory.php b/app/code/Magento/Catalog/Model/Product/Price/TierPriceFactory.php
index f23ce0faf708c..6bbb0951ec192 100644
--- a/app/code/Magento/Catalog/Model/Product/Price/TierPriceFactory.php
+++ b/app/code/Magento/Catalog/Model/Product/Price/TierPriceFactory.php
@@ -8,8 +8,9 @@
use Magento\Catalog\Api\Data\TierPriceInterface;
use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
-class TierPriceFactory
+class TierPriceFactory implements ResetAfterRequestInterface
{
/**
* @var \Magento\Catalog\Api\Data\TierPriceInterfaceFactory
@@ -168,4 +169,12 @@ private function retrieveGroupValue($code)
return $this->customerGroupsByCode[$code];
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->customerGroupsByCode = [];
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Product/Price/Validation/TierPriceValidator.php b/app/code/Magento/Catalog/Model/Product/Price/Validation/TierPriceValidator.php
index 45ba1de85260d..872a3e31eb573 100644
--- a/app/code/Magento/Catalog/Model/Product/Price/Validation/TierPriceValidator.php
+++ b/app/code/Magento/Catalog/Model/Product/Price/Validation/TierPriceValidator.php
@@ -14,6 +14,7 @@
use Magento\Framework\Api\FilterBuilder;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Store\Api\WebsiteRepositoryInterface;
/**
@@ -21,7 +22,7 @@
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
-class TierPriceValidator
+class TierPriceValidator implements ResetAfterRequestInterface
{
/**
* @var ProductIdLocatorInterface
@@ -475,10 +476,19 @@ private function retrieveGroupValue(string $code)
$item = array_shift($items);
if (!$item) {
+ $this->customerGroupsByCode[$code] = false;
return false;
}
- $this->customerGroupsByCode[strtolower($item->getCode())] = $item->getId();
+ $itemCode = $item->getCode();
+ $itemId = $item->getId();
+
+ if (strtolower($itemCode) !== $code) {
+ $this->customerGroupsByCode[$code] = false;
+ return false;
+ }
+
+ $this->customerGroupsByCode[strtolower($itemCode)] = $itemId;
}
return $this->customerGroupsByCode[$code];
@@ -499,4 +509,12 @@ private function compareWebsiteValue(TierPriceInterface $price, TierPriceInterfa
)
&& $price->getWebsiteId() != $tierPrice->getWebsiteId();
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->customerGroupsByCode = [];
+ }
}
diff --git a/app/code/Magento/Catalog/Model/Product/Type/Price.php b/app/code/Magento/Catalog/Model/Product/Type/Price.php
index fb25b6703b730..eee62527094f6 100644
--- a/app/code/Magento/Catalog/Model/Product/Type/Price.php
+++ b/app/code/Magento/Catalog/Model/Product/Type/Price.php
@@ -9,6 +9,7 @@
use Magento\Catalog\Model\Product;
use Magento\Customer\Api\GroupManagementInterface;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Framework\Pricing\PriceCurrencyInterface;
use Magento\Store\Model\Store;
use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory;
@@ -23,7 +24,7 @@
* @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
* @since 100.0.2
*/
-class Price
+class Price implements ResetAfterRequestInterface
{
/**
* Product price cache tag
@@ -657,4 +658,12 @@ public function isTierPriceFixed()
{
return true;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ self::$attributeCache = [];
+ }
}
diff --git a/app/code/Magento/Catalog/Model/ProductCategoryList.php b/app/code/Magento/Catalog/Model/ProductCategoryList.php
index c3a88a505c516..d8af4a9656304 100644
--- a/app/code/Magento/Catalog/Model/ProductCategoryList.php
+++ b/app/code/Magento/Catalog/Model/ProductCategoryList.php
@@ -8,13 +8,14 @@
use Magento\Framework\DB\Select;
use Magento\Framework\DB\Sql\UnionExpression;
use Magento\Framework\App\ObjectManager;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer;
/**
* Provides info about product categories.
*/
-class ProductCategoryList
+class ProductCategoryList implements ResetAfterRequestInterface
{
/**
* @var array
@@ -106,4 +107,12 @@ public function getCategorySelect($productId, $tableName)
$productId
);
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->categoryIdList = [];
+ }
}
diff --git a/app/code/Magento/Catalog/Model/ProductIdLocator.php b/app/code/Magento/Catalog/Model/ProductIdLocator.php
index daf8790c419f9..eb493671e613c 100644
--- a/app/code/Magento/Catalog/Model/ProductIdLocator.php
+++ b/app/code/Magento/Catalog/Model/ProductIdLocator.php
@@ -6,10 +6,12 @@
namespace Magento\Catalog\Model;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
+
/**
* Product ID locator provides all product IDs by SKUs.
*/
-class ProductIdLocator implements \Magento\Catalog\Model\ProductIdLocatorInterface
+class ProductIdLocator implements \Magento\Catalog\Model\ProductIdLocatorInterface, ResetAfterRequestInterface
{
/**
* Limit values for array IDs by SKU.
@@ -126,4 +128,12 @@ private function truncateToLimit()
$this->idsBySku = array_slice($this->idsBySku, $this->idsLimit * -1, null, true);
}
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->idsBySku = [];
+ }
}
diff --git a/app/code/Magento/Catalog/Model/ProductRepository.php b/app/code/Magento/Catalog/Model/ProductRepository.php
index 5cf4ac1e64242..c586563759b54 100644
--- a/app/code/Magento/Catalog/Model/ProductRepository.php
+++ b/app/code/Magento/Catalog/Model/ProductRepository.php
@@ -31,6 +31,7 @@
use Magento\Framework\Exception\StateException;
use Magento\Framework\Exception\TemporaryState\CouldNotSaveException as TemporaryCouldNotSaveException;
use Magento\Framework\Exception\ValidatorException;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Store\Model\Store;
use Magento\Catalog\Api\Data\EavAttributeInterface;
@@ -40,8 +41,10 @@
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @SuppressWarnings(PHPMD.TooManyFields)
+ * phpcs:disable Magento2.Commenting.ClassPropertyPHPDocFormatting
+ * phpcs:disable Magento2.Annotation.MethodAnnotationStructure.InvalidDeprecatedTagUsage
*/
-class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterface
+class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterface, ResetAfterRequestInterface
{
/**
* @var \Magento\Catalog\Api\ProductCustomOptionRepositoryInterface
@@ -794,6 +797,7 @@ private function getMediaGalleryProcessor()
/**
* Retrieve collection processor
*
+ * phpcs:disable Magento2.Annotation.MethodAnnotationStructure
* @deprecated 102.0.0
* @return CollectionProcessorInterface
*/
@@ -952,4 +956,13 @@ private function joinPositionField(
);
}
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->instances = [];
+ $this->instancesById = [];
+ }
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Attribute/WebsiteAttributesSynchronizer.php b/app/code/Magento/Catalog/Model/ResourceModel/Attribute/WebsiteAttributesSynchronizer.php
index 61f2c1838a1c2..1c2a315490f36 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Attribute/WebsiteAttributesSynchronizer.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Attribute/WebsiteAttributesSynchronizer.php
@@ -14,25 +14,24 @@
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\FlagManager;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
-/**
- * Class WebsiteAttributesSynchronizer
- * @package Magento\Catalog\Cron
- */
-class WebsiteAttributesSynchronizer
+class WebsiteAttributesSynchronizer implements ResetAfterRequestInterface
{
- const FLAG_SYNCHRONIZED = 0;
- const FLAG_SYNCHRONIZATION_IN_PROGRESS = 1;
- const FLAG_REQUIRES_SYNCHRONIZATION = 2;
- const FLAG_NAME = 'catalog_website_attribute_is_sync_required';
+ public const FLAG_SYNCHRONIZED = 0;
+ public const FLAG_SYNCHRONIZATION_IN_PROGRESS = 1;
+ public const FLAG_REQUIRES_SYNCHRONIZATION = 2;
+ public const FLAG_NAME = 'catalog_website_attribute_is_sync_required';
- const ATTRIBUTE_WEBSITE = 2;
- const GLOBAL_STORE_VIEW_ID = 0;
+ public const ATTRIBUTE_WEBSITE = 2;
+ public const GLOBAL_STORE_VIEW_ID = 0;
- const MASK_ATTRIBUTE_VALUE = '%d_%d_%d';
+ public const MASK_ATTRIBUTE_VALUE = '%d_%d_%d';
/**
* Map table names to metadata classes where link field might be found
+ *
+ * @var string[]
*/
private $tableMetaDataClass = [
'catalog_category_entity_datetime' => CategoryInterface::class,
@@ -101,7 +100,7 @@ class WebsiteAttributesSynchronizer
* WebsiteAttributesSynchronizer constructor.
* @param ResourceConnection $resourceConnection
* @param FlagManager $flagManager
- * @param Generator $batchQueryGenerator,
+ * @param Generator $batchQueryGenerator
* @param MetadataPool $metadataPool
*/
public function __construct(
@@ -119,6 +118,7 @@ public function __construct(
/**
* Synchronizes attribute values between different store views on website level
+ *
* @return void
* @throws \Exception
*/
@@ -141,15 +141,18 @@ public function synchronize()
}
/**
+ * Check if synchronization required
+ *
* @return bool
*/
- public function isSynchronizationRequired()
+ public function isSynchronizationRequired(): bool
{
return self::FLAG_REQUIRES_SYNCHRONIZATION === $this->flagManager->getFlagData(self::FLAG_NAME);
}
/**
* Puts a flag that synchronization is required
+ *
* @return void
*/
public function scheduleSynchronization()
@@ -159,6 +162,7 @@ public function scheduleSynchronization()
/**
* Marks flag as in progress in case if several crons enabled, so sync. won't be duplicated
+ *
* @return void
*/
private function markSynchronizationInProgress()
@@ -168,6 +172,7 @@ private function markSynchronizationInProgress()
/**
* Turn off synchronization flag
+ *
* @return void
*/
private function markSynchronized()
@@ -176,10 +181,12 @@ private function markSynchronized()
}
/**
+ * Perform table synchronization
+ *
* @param string $tableName
* @return void
*/
- private function synchronizeTable($tableName)
+ private function synchronizeTable(string $tableName): void
{
foreach ($this->fetchAttributeValues($tableName) as $attributeValueItems) {
$this->processAttributeValues($attributeValueItems, $tableName);
@@ -188,6 +195,7 @@ private function synchronizeTable($tableName)
/**
* Aligns website attribute values
+ *
* @param array $attributeValueItems
* @param string $tableName
* @return void
@@ -215,7 +223,7 @@ private function processAttributeValues(array $attributeValueItems, $tableName)
*
* @param string $tableName
* @yield array
- * @return void
+ * @return \Generator
*/
private function fetchAttributeValues($tableName)
{
@@ -257,6 +265,8 @@ private function fetchAttributeValues($tableName)
}
/**
+ * Retrieve grouped store views
+ *
* @return array
*/
private function getGroupedStoreViews()
@@ -286,6 +296,8 @@ private function getGroupedStoreViews()
}
/**
+ * Check if attribute value processed
+ *
* @param array $attributeValue
* @param string $tableName
* @return bool
@@ -304,6 +316,7 @@ private function isAttributeValueProcessed(array $attributeValue, $tableName)
/**
* Resets processed attribute values
+ *
* @return void
*/
private function resetProcessedAttributeValues()
@@ -312,6 +325,8 @@ private function resetProcessedAttributeValues()
}
/**
+ * Mark processed attribute value
+ *
* @param array $attributeValue
* @param string $tableName
* @return void
@@ -326,6 +341,8 @@ private function markAttributeValueProcessed(array $attributeValue, $tableName)
}
/**
+ * Retrieve attribute value key
+ *
* @param int $entityId
* @param int $attributeId
* @param int $websiteId
@@ -342,6 +359,8 @@ private function getAttributeValueKey($entityId, $attributeId, $websiteId)
}
/**
+ * Generate insertions for attribute value
+ *
* @param array $attributeValue
* @param string $tableName
* @return array|null
@@ -369,6 +388,8 @@ private function generateAttributeValueInsertions(array $attributeValue, $tableN
}
/**
+ * Insert attribute values into table
+ *
* @param array $insertions
* @param string $tableName
* @return void
@@ -376,9 +397,9 @@ private function generateAttributeValueInsertions(array $attributeValue, $tableN
private function executeInsertions(array $insertions, $tableName)
{
$rawQuery = sprintf(
- 'INSERT INTO
+ 'INSERT INTO
%s(attribute_id, store_id, %s, `value`)
- VALUES
+ VALUES
%s
ON duplicate KEY UPDATE `value` = VALUES(`value`)',
$this->resourceConnection->getTableName($tableName),
@@ -399,13 +420,9 @@ private function getPlaceholderValues(array $insertions)
{
$placeholderValues = [];
foreach ($insertions as $insertion) {
- $placeholderValues = array_merge(
- $placeholderValues,
- $insertion
- );
+ $placeholderValues[] = $insertion;
}
-
- return $placeholderValues;
+ return array_merge(...$placeholderValues);
}
/**
@@ -426,6 +443,8 @@ private function prepareInsertValuesStatement(array $insertions)
}
/**
+ * Retrieve table link field
+ *
* @param string $tableName
* @return string
* @throws LocalizedException
@@ -449,4 +468,14 @@ private function getTableLinkField($tableName)
return $this->linkFields[$tableName];
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->groupedStoreViews = [];
+ $this->processedAttributeValues = [];
+ $this->linkFields = [];
+ }
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php
index 35828fc8ec117..765065bb5fded 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php
@@ -20,13 +20,14 @@
use Magento\Framework\DataObject;
use Magento\Framework\EntityManager\EntityManager;
use Magento\Framework\EntityManager\MetadataPool;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Resource model for category entity
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
-class Category extends AbstractResource
+class Category extends AbstractResource implements ResetAfterRequestInterface
{
/**
* Category tree object
@@ -1172,4 +1173,14 @@ public function getCategoryWithChildren(int $categoryId): array
return $connection->fetchAll($select);
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ parent::_resetState();
+ $this->entitiesWhereAttributesIs = [];
+ $this->_storeId = null;
+ }
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php
index 56fb7290b81a6..9df0a3a9b3831 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php
@@ -137,6 +137,18 @@ protected function _construct()
$this->_init(Category::class, \Magento\Catalog\Model\ResourceModel\Category::class);
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ parent::_resetState();
+ $this->_productTable = null;
+ $this->_productStoreId = null;
+ $this->_productWebsiteTable = null;
+ $this->_loadWithProductCount = false;
+ }
+
/**
* Add Id filter
*
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php
index a121648b7acba..89dacf5361a69 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php
@@ -76,6 +76,15 @@ public function __construct(
);
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ parent::_resetState();
+ $this->_storeId = null;
+ }
+
/**
* Retrieve Entity Primary Key
*
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product.php b/app/code/Magento/Catalog/Model/ResourceModel/Product.php
index c950b49348dc3..1256ab1caa93b 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product.php
@@ -16,6 +16,7 @@
use Magento\Framework\DataObject;
use Magento\Framework\EntityManager\EntityManager;
use Magento\Framework\Model\AbstractModel;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Product entity resource model
@@ -23,9 +24,10 @@
* @api
* @SuppressWarnings(PHPMD.LongVariable)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * phpcs:disable Magento2.Annotation.MethodAnnotationStructure
* @since 100.0.2
*/
-class Product extends AbstractResource
+class Product extends AbstractResource implements ResetAfterRequestInterface
{
/**
* Product to website linkage table
@@ -844,4 +846,13 @@ protected function _afterDelete(DataObject $object)
$this->mediaImageDeleteProcessor->execute($object);
return parent::_afterDelete($object);
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ parent::_resetState();
+ $this->availableCategoryIdsCache = [];
+ }
}
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php
index 79636c55c0f56..6756aac0786a9 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php
@@ -102,6 +102,11 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Collection\Abstrac
*/
protected $_productLimitationFilters;
+ /**
+ * @var ProductLimitationFactory
+ */
+ private $productLimitationFactory;
+
/**
* Category product count select
*
@@ -354,10 +359,10 @@ public function __construct(
$this->_resourceHelper = $resourceHelper;
$this->dateTime = $dateTime;
$this->_groupManagement = $groupManagement;
- $productLimitationFactory = $productLimitationFactory ?: ObjectManager::getInstance()->get(
+ $this->productLimitationFactory = $productLimitationFactory ?: ObjectManager::getInstance()->get(
\Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory::class
);
- $this->_productLimitationFilters = $productLimitationFactory->create();
+ $this->_productLimitationFilters = $this->productLimitationFactory->create();
$this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get(MetadataPool::class);
parent::__construct(
$entityFactory,
@@ -387,6 +392,36 @@ public function __construct(
->get(Gallery::class);
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ parent::_resetState();
+ $this->_flatEnabled = [];
+ $this->_addUrlRewrite = false;
+ $this->_urlRewriteCategory = '';
+ $this->_addFinalPrice = false;
+ $this->_allIdsCache = null;
+ $this->_addTaxPercents = false;
+ $this->_productLimitationFilters = $this->productLimitationFactory->create();
+ $this->_productCountSelect = null;
+ $this->_isWebsiteFilter = false;
+ $this->_priceDataFieldFilters = [];
+ $this->_priceExpression = null;
+ $this->_additionalPriceExpression = null;
+ $this->_maxPrice = null;
+ $this->_minPrice = null;
+ $this->_priceStandardDeviation = null;
+ $this->_pricesCount = null;
+ $this->_catalogPreparePriceSelect = null;
+ $this->needToAddWebsiteNamesToResult = null;
+ $this->linkField = null;
+ $this->backend = null;
+ $this->emptyItem = null;
+ $this->_construct();
+ }
+
/**
* Get cloned Select after dispatching 'catalog_prepare_price_select' event
*
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php
index 76f566a364769..7100f20ecd96f 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Compare/Item/Collection.php
@@ -46,15 +46,11 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection
protected $_comparableAttributes;
/**
- * Catalog product compare
- *
* @var \Magento\Catalog\Helper\Product\Compare
*/
protected $_catalogProductCompare = null;
/**
- * Catalog product compare item
- *
* @var \Magento\Catalog\Model\ResourceModel\Product\Compare\Item
*/
protected $_catalogProductCompareItem;
@@ -150,6 +146,18 @@ protected function _construct()
$this->_initTables();
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ parent::_resetState();
+ $this->_customerId = 0;
+ $this->_visitorId = 0;
+ $this->listId = 0;
+ $this->_comparableAttributes = null;
+ }
+
/**
* Set customer filter to collection
*
@@ -287,7 +295,6 @@ public function getProductsByListId(int $listId): array
return $this->getConnection()->fetchCol($select);
}
-
/**
* Set list_id for customer compare item
*
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php
index bca919e700364..cac549e0a17c9 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php
@@ -183,6 +183,21 @@ public function __construct(
}
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ parent::_resetState();
+ $this->_product = null;
+ $this->_linkModel = null;
+ $this->_linkTypeId = null;
+ $this->_isStrongMode = null;
+ $this->_hasLinkFilter = false;
+ $this->productIds = null;
+ $this->linkField = null;
+ }
+
/**
* Declare link model and initialize type attributes join
*
diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Url.php b/app/code/Magento/Catalog/Model/ResourceModel/Url.php
index 43762306b2b69..f7c02cc93bf97 100644
--- a/app/code/Magento/Catalog/Model/ResourceModel/Url.php
+++ b/app/code/Magento/Catalog/Model/ResourceModel/Url.php
@@ -10,16 +10,17 @@
*
* @author Magento Core Team
*/
-use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator;
use Magento\Catalog\Api\Data\CategoryInterface;
-use Magento\Framework\EntityManager\MetadataPool;
-use Magento\Framework\App\ObjectManager;
use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer;
+use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\EntityManager\MetadataPool;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
-class Url extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
+class Url extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb implements ResetAfterRequestInterface
{
/**
* Stores configuration array
@@ -727,4 +728,15 @@ private function getMetadataPool()
}
return $this->metadataPool;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->_categoryAttributes = [];
+ $this->_productAttributes = [];
+ $this->_rootChildrenIds = [];
+ $this->_stores = null;
+ }
}
diff --git a/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php b/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php
index a5e573caa381e..6945bf526cffb 100644
--- a/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php
+++ b/app/code/Magento/Catalog/Pricing/Price/MinimalTierPriceCalculator.php
@@ -6,9 +6,9 @@
namespace Magento\Catalog\Pricing\Price;
-use Magento\Framework\Pricing\SaleableInterface;
use Magento\Framework\Pricing\Adjustment\CalculatorInterface;
use Magento\Framework\Pricing\Amount\AmountInterface;
+use Magento\Framework\Pricing\SaleableInterface;
/**
* As Low As shows minimal value of Tier Prices
@@ -36,18 +36,7 @@ public function __construct(CalculatorInterface $calculator)
*/
public function getValue(SaleableInterface $saleableItem)
{
- /** @var TierPrice $price */
- $price = $saleableItem->getPriceInfo()->getPrice(TierPrice::PRICE_CODE);
- $tierPriceList = $price->getTierPriceList();
-
- $tierPrices = [];
- foreach ($tierPriceList as $tierPrice) {
- /** @var AmountInterface $price */
- $price = $tierPrice['price'];
- $tierPrices[] = $price->getValue();
- }
-
- return $tierPrices ? min($tierPrices) : null;
+ return $this->getAmount($saleableItem)?->getValue();
}
/**
@@ -58,10 +47,16 @@ public function getValue(SaleableInterface $saleableItem)
*/
public function getAmount(SaleableInterface $saleableItem)
{
- $value = $this->getValue($saleableItem);
+ $minPrice = null;
+ /** @var TierPrice $price */
+ $tierPrice = $saleableItem->getPriceInfo()->getPrice(TierPrice::PRICE_CODE);
+ $tierPriceList = $tierPrice->getTierPriceList();
+
+ if (count($tierPriceList)) {
+ usort($tierPriceList, fn ($tier1, $tier2) => $tier1['price']->getValue() <=> $tier2['price']->getValue());
+ $minPrice = array_shift($tierPriceList)['price'];
+ }
- return $value === null
- ? null
- : $this->calculator->getAmount($value, $saleableItem, 'tax');
+ return $minPrice;
}
}
diff --git a/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php b/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php
index 5fba207bdeb0c..6033e7deaeac9 100644
--- a/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php
+++ b/app/code/Magento/Catalog/Pricing/Render/FinalPriceBox.php
@@ -6,16 +6,17 @@
namespace Magento\Catalog\Pricing\Render;
-use Magento\Catalog\Pricing\Price;
-use Magento\Framework\Pricing\Render\PriceBox as BasePriceBox;
-use Magento\Msrp\Pricing\Price\MsrpPrice;
use Magento\Catalog\Model\Product\Pricing\Renderer\SalableResolverInterface;
-use Magento\Framework\View\Element\Template\Context;
-use Magento\Framework\Pricing\SaleableInterface;
+use Magento\Catalog\Pricing\Price;
+use Magento\Catalog\Pricing\Price\MinimalPriceCalculatorInterface;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Pricing\Adjustment\CalculatorInterface;
use Magento\Framework\Pricing\Price\PriceInterface;
+use Magento\Framework\Pricing\Render\PriceBox as BasePriceBox;
use Magento\Framework\Pricing\Render\RendererPool;
-use Magento\Framework\App\ObjectManager;
-use Magento\Catalog\Pricing\Price\MinimalPriceCalculatorInterface;
+use Magento\Framework\Pricing\SaleableInterface;
+use Magento\Framework\View\Element\Template\Context;
+use Magento\Msrp\Pricing\Price\MsrpPrice;
/**
* Class for final_price rendering
@@ -140,7 +141,7 @@ public function renderAmountMinimal()
'display_label' => __('As low as'),
'price_id' => $id,
'include_container' => false,
- 'skip_adjustments' => true
+ 'skip_adjustments' => false
]
);
}
@@ -183,7 +184,7 @@ public function showMinimalPrice()
public function getCacheKey()
{
return parent::getCacheKey()
- . ($this->getData('list_category_page') ? '-list-category-page': '')
+ . ($this->getData('list_category_page') ? '-list-category-page' : '')
. ($this->getSaleableItem()->getCustomerGroupId() ?? '');
}
diff --git a/app/code/Magento/Catalog/README.md b/app/code/Magento/Catalog/README.md
index 0e43661ba8cae..ef95c1effe0a5 100644
--- a/app/code/Magento/Catalog/README.md
+++ b/app/code/Magento/Catalog/README.md
@@ -1,7 +1,9 @@
-#Magento_Catalog
+# Magento_Catalog
+
Magento_Catalog module functionality is represented by the following sub-systems:
- - Products Management. It includes CRUD operation of product, product media, product attributes, etc...
- - Category Management. It includes CRUD operation of category, category attributes
+
+- Products Management. It includes CRUD operation of product, product media, product attributes, etc...
+- Category Management. It includes CRUD operation of category, category attributes
Catalog module provides mechanism for creating new product type in the system.
Catalog module provides API filtering that allows to limit product selection with advanced filters.
@@ -12,61 +14,61 @@ Catalog module provides API filtering that allows to limit product selection wit
(https://developer.adobe.com/commerce/php/development/build/component-file-structure/).
## Observer
+
This module observes the following events:
- `etc/events.xml`
- `magento_catalog_api_data_productinterface_save_before` event in
- `Magento\Framework\EntityManager\Observer\BeforeEntitySave` file.
- `magento_catalog_api_data_productinterface_save_after` event in
- `Magento\Framework\EntityManager\Observer\AfterEntitySave` file.
- `magento_catalog_api_data_productinterface_delete_before` event in
- `Magento\Framework\EntityManager\Observer\BeforeEntityDelete` file.
- `magento_catalog_api_data_productinterface_delete_after` event in
- `Magento\Framework\EntityManager\Observer\AfterEntityDelete` file.
- `magento_catalog_api_data_productinterface_load_after` event in
- `Magento\Framework\EntityManager\Observer\AfterEntityLoad` file.
- `magento_catalog_api_data_categoryinterface_save_before` event in
- `Magento\Framework\EntityManager\Observer\BeforeEntitySave` file.
- `magento_catalog_api_data_categoryinterface_save_after` event in
- `Magento\Framework\EntityManager\Observer\AfterEntitySave` file.
- `magento_catalog_api_data_categoryinterface_save_after` event in
- `Magento\Catalog\Observer\InvalidateCacheOnCategoryDesignChange` file.
- `magento_catalog_api_data_categoryinterface_delete_before` event in
- `Magento\Framework\EntityManager\Observer\BeforeEntityDelete` file.
- `magento_catalog_api_data_categoryinterface_delete_after` event in
- `Magento\Framework\EntityManager\Observer\AfterEntityDelete` file.
- `magento_catalog_api_data_categoryinterface_load_after` event in
- `Magento\Framework\EntityManager\Observer\AfterEntityLoad` file.
- `magento_catalog_api_data_categorytreeinterface_save_before` event in
- `Magento\Framework\EntityManager\Observer\BeforeEntitySave` file.
- `magento_catalog_api_data_categorytreeinterface_save_after` event in
- `Magento\Framework\EntityManager\Observer\AfterEntitySave` file.
- `magento_catalog_api_data_categorytreeinterface_delete_before` event in
- `Magento\Framework\EntityManager\Observer\BeforeEntityDelete` file.
- `magento_catalog_api_data_categorytreeinterface_delete_after` event in
- `Magento\Framework\EntityManager\Observer\AfterEntityDelete` file.
- `magento_catalog_api_data_categorytreeinterface_load_after` event in
- `Magento\Framework\EntityManager\Observer\AfterEntityLoad` file.
- `admin_system_config_changed_section_catalog` event in
- `Magento\Catalog\Observer\SwitchPriceAttributeScopeOnConfigChange` file.
- `catalog_product_save_before` event in
- `Magento\Catalog\Observer\SetSpecialPriceStartDate` file.
- `store_save_after` event in
- `Magento\Catalog\Observer\SynchronizeWebsiteAttributesOnStoreChange` file.
- `catalog_product_save_commit_after` event in
- `Magento\Catalog\Observer\ImageResizeAfterProductSave` file.
- `catalog_category_prepare_save` event in
- `Magento\Catalog\Observer\CategoryDesignAuthorization` file.
-
- `/etc/frontend/events.xml`
- `customer_login` event in
- `Magento\Catalog\Observer\Compare\BindCustomerLoginObserver` file.
- `customer_logout` event in
- `Magento\Catalog\Observer\Compare\BindCustomerLogoutObserver` file.
-
- `/etc/adminhtml/events.xml`
- `cms_wysiwyg_images_static_urls_allowed` event in
- `Magento\Catalog\Observer\CatalogCheckIsUsingStaticUrlsAllowedObserver` file.
- `catalog_category_change_products` event in
- `Magento\Catalog\Observer\CategoryProductIndexer` file.
- `category_move` event in
- `Magento\Catalog\Observer\FlushCategoryPagesCache`
\ No newline at end of file
+
+- `etc/events.xml`
+ - `magento_catalog_api_data_productinterface_save_before` event in
+ `Magento\Framework\EntityManager\Observer\BeforeEntitySave` file.
+ - `magento_catalog_api_data_productinterface_save_after` event in
+ `Magento\Framework\EntityManager\Observer\AfterEntitySave` file.
+ - `magento_catalog_api_data_productinterface_delete_before` event in
+ `Magento\Framework\EntityManager\Observer\BeforeEntityDelete` file.
+ - `magento_catalog_api_data_productinterface_delete_after` event in
+ `Magento\Framework\EntityManager\Observer\AfterEntityDelete` file.
+ - `magento_catalog_api_data_productinterface_load_after` event in
+ `Magento\Framework\EntityManager\Observer\AfterEntityLoad` file.
+ - `magento_catalog_api_data_categoryinterface_save_before` event in
+ `Magento\Framework\EntityManager\Observer\BeforeEntitySave` file.
+ - `magento_catalog_api_data_categoryinterface_save_after` event in
+ `Magento\Framework\EntityManager\Observer\AfterEntitySave` file.
+ - `magento_catalog_api_data_categoryinterface_save_after` event in
+ `Magento\Catalog\Observer\InvalidateCacheOnCategoryDesignChange` file.
+ - `magento_catalog_api_data_categoryinterface_delete_before` event in
+ `Magento\Framework\EntityManager\Observer\BeforeEntityDelete` file.
+ - `magento_catalog_api_data_categoryinterface_delete_after` event in
+ `Magento\Framework\EntityManager\Observer\AfterEntityDelete` file.
+ - `magento_catalog_api_data_categoryinterface_load_after` event in
+ `Magento\Framework\EntityManager\Observer\AfterEntityLoad` file.
+ - `magento_catalog_api_data_categorytreeinterface_save_before` event in
+ `Magento\Framework\EntityManager\Observer\BeforeEntitySave` file.
+ - `magento_catalog_api_data_categorytreeinterface_save_after` event in
+ `Magento\Framework\EntityManager\Observer\AfterEntitySave` file.
+ - `magento_catalog_api_data_categorytreeinterface_delete_before` event in
+ `Magento\Framework\EntityManager\Observer\BeforeEntityDelete` file.
+ - `magento_catalog_api_data_categorytreeinterface_delete_after` event in
+ `Magento\Framework\EntityManager\Observer\AfterEntityDelete` file.
+ - `magento_catalog_api_data_categorytreeinterface_load_after` event in
+ `Magento\Framework\EntityManager\Observer\AfterEntityLoad` file.
+ `admin_system_config_changed_section_catalog` event in
+ `Magento\Catalog\Observer\SwitchPriceAttributeScopeOnConfigChange` file.
+ - `catalog_product_save_before` event in
+ `Magento\Catalog\Observer\SetSpecialPriceStartDate` file.
+ `store_save_after` event in
+ `Magento\Catalog\Observer\SynchronizeWebsiteAttributesOnStoreChange` file.
+ - `catalog_product_save_commit_after` event in
+ `Magento\Catalog\Observer\ImageResizeAfterProductSave` file.
+ - `catalog_category_prepare_save` event in
+ `Magento\Catalog\Observer\CategoryDesignAuthorization` file.
+- `/etc/frontend/events.xml`
+ - `customer_login` event in
+ `Magento\Catalog\Observer\Compare\BindCustomerLoginObserver` file.
+ - `customer_logout` event in
+ `Magento\Catalog\Observer\Compare\BindCustomerLogoutObserver` file.
+- `/etc/adminhtml/events.xml`
+ `cms_wysiwyg_images_static_urls_allowed` event in
+ `Magento\Catalog\Observer\CatalogCheckIsUsingStaticUrlsAllowedObserver` file.
+ - `catalog_category_change_products` event in
+ `Magento\Catalog\Observer\CategoryProductIndexer` file.
+ - `category_move` event in
+ `Magento\Catalog\Observer\FlushCategoryPagesCache`
diff --git a/app/code/Magento/Catalog/Test/Fixture/Attribute.php b/app/code/Magento/Catalog/Test/Fixture/Attribute.php
index 1f68eb2b832d3..f4efdcf5038c8 100644
--- a/app/code/Magento/Catalog/Test/Fixture/Attribute.php
+++ b/app/code/Magento/Catalog/Test/Fixture/Attribute.php
@@ -11,8 +11,12 @@
use Magento\Catalog\Api\ProductAttributeManagementInterface;
use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
use Magento\Catalog\Model\Product;
+use Magento\Catalog\Model\ResourceModel\Attribute as ResourceModelAttribute;
+use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute;
+use Magento\Eav\Model\AttributeFactory;
use Magento\Eav\Setup\EavSetup;
use Magento\Framework\DataObject;
+use Magento\TestFramework\Fixture\Api\DataMerger;
use Magento\TestFramework\Fixture\Api\ServiceFactory;
use Magento\TestFramework\Fixture\RevertibleDataFixtureInterface;
use Magento\TestFramework\Fixture\Data\ProcessorInterface;
@@ -30,12 +34,12 @@ class Attribute implements RevertibleDataFixtureInterface
'is_filterable_in_grid' => true,
'position' => 0,
'apply_to' => [],
- 'is_searchable' => '0',
- 'is_visible_in_advanced_search' => '0',
- 'is_comparable' => '0',
- 'is_used_for_promo_rules' => '0',
- 'is_visible_on_front' => '0',
- 'used_in_product_listing' => '0',
+ 'is_searchable' => false,
+ 'is_visible_in_advanced_search' => false,
+ 'is_comparable' => false,
+ 'is_used_for_promo_rules' => false,
+ 'is_visible_on_front' => false,
+ 'used_in_product_listing' => false,
'is_visible' => true,
'scope' => 'store',
'attribute_code' => 'product_attribute%uniqid%',
@@ -49,7 +53,6 @@ class Attribute implements RevertibleDataFixtureInterface
'backend_type' => 'varchar',
'is_unique' => '0',
'validation_rules' => []
-
];
private const DEFAULT_ATTRIBUTE_SET_DATA = [
@@ -78,29 +81,59 @@ class Attribute implements RevertibleDataFixtureInterface
*/
private $productAttributeManagement;
+ /**
+ * @var AttributeFactory
+ */
+ private AttributeFactory $attributeFactory;
+
+ /**
+ * @var DataMerger
+ */
+ private DataMerger $dataMerger;
+
+ /**
+ * @var ResourceModelAttribute
+ */
+ private ResourceModelAttribute $resourceModelAttribute;
+
/**
* @param ServiceFactory $serviceFactory
* @param ProcessorInterface $dataProcessor
* @param EavSetup $eavSetup
+ * @param ProductAttributeManagementInterface $productAttributeManagement
+ * @param AttributeFactory $attributeFactory
+ * @param DataMerger $dataMerger
+ * @param ResourceModelAttribute $resourceModelAttribute
*/
public function __construct(
ServiceFactory $serviceFactory,
ProcessorInterface $dataProcessor,
EavSetup $eavSetup,
- ProductAttributeManagementInterface $productAttributeManagement
+ ProductAttributeManagementInterface $productAttributeManagement,
+ AttributeFactory $attributeFactory,
+ DataMerger $dataMerger,
+ ResourceModelAttribute $resourceModelAttribute
) {
$this->serviceFactory = $serviceFactory;
$this->dataProcessor = $dataProcessor;
$this->eavSetup = $eavSetup;
$this->productAttributeManagement = $productAttributeManagement;
+ $this->attributeFactory = $attributeFactory;
+ $this->dataMerger = $dataMerger;
+ $this->resourceModelAttribute = $resourceModelAttribute;
}
/**
* {@inheritdoc}
* @param array $data Parameters. Same format as Attribute::DEFAULT_DATA.
+ * @return DataObject|null
*/
public function apply(array $data = []): ?DataObject
{
+ if (array_key_exists('additional_data', $data)) {
+ return $this->applyAttributeWithAdditionalData($data);
+ }
+
$service = $this->serviceFactory->create(ProductAttributeRepositoryInterface::class, 'save');
/**
@@ -139,6 +172,26 @@ public function revert(DataObject $data): void
);
}
+ /**
+ * @param array $data Parameters. Same format as Attribute::DEFAULT_DATA.
+ * @return DataObject|null
+ */
+ private function applyAttributeWithAdditionalData(array $data = []): ?DataObject
+ {
+ $defaultData = array_merge(self::DEFAULT_DATA, ['additional_data' => null]);
+ /** @var EavAttribute $attr */
+ $attr = $this->attributeFactory->createAttribute(EavAttribute::class, $defaultData);
+ $mergedData = $this->dataProcessor->process($this, $this->dataMerger->merge($defaultData, $data));
+
+ $attributeSetData = $this->prepareAttributeSetData(
+ array_intersect_key($data, self::DEFAULT_ATTRIBUTE_SET_DATA)
+ );
+
+ $attr->setData(array_merge($mergedData, $attributeSetData));
+ $this->resourceModelAttribute->save($attr);
+ return $attr;
+ }
+
/**
* Prepare attribute data
*
diff --git a/app/code/Magento/Catalog/Test/Fixture/CategoryAttribute.php b/app/code/Magento/Catalog/Test/Fixture/CategoryAttribute.php
new file mode 100644
index 0000000000000..303ddd723d6c5
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Fixture/CategoryAttribute.php
@@ -0,0 +1,116 @@
+ false,
+ 'is_html_allowed_on_front' => true,
+ 'used_for_sort_by' => false,
+ 'is_filterable' => false,
+ 'is_filterable_in_search' => false,
+ 'is_used_in_grid' => true,
+ 'is_visible_in_grid' => true,
+ 'is_filterable_in_grid' => true,
+ 'position' => 0,
+ 'is_searchable' => '0',
+ 'is_visible_in_advanced_search' => '0',
+ 'is_comparable' => '0',
+ 'is_used_for_promo_rules' => '0',
+ 'is_visible_on_front' => '0',
+ 'used_in_product_listing' => '0',
+ 'is_visible' => true,
+ 'scope' => 'store',
+ 'attribute_code' => 'category_attribute%uniqid%',
+ 'frontend_input' => 'text',
+ 'entity_type_id' => '3',
+ 'is_required' => false,
+ 'is_user_defined' => true,
+ 'default_frontend_label' => 'Category Attribute%uniqid%',
+ 'backend_type' => 'varchar',
+ 'is_unique' => '0',
+ 'apply_to' => [],
+ ];
+
+ /**
+ * @var DataMerger
+ */
+ private DataMerger $dataMerger;
+
+ /**
+ * @var ProcessorInterface
+ */
+ private ProcessorInterface $processor;
+
+ /**
+ * @var AttributeFactory
+ */
+ private AttributeFactory $attributeFactory;
+
+ /**
+ * @var ResourceModelAttribute
+ */
+ private ResourceModelAttribute $resourceModelAttribute;
+
+ /**
+ * @var AttributeRepositoryInterface
+ */
+ private AttributeRepositoryInterface $attributeRepository;
+
+ /**
+ * @param DataMerger $dataMerger
+ * @param ProcessorInterface $processor
+ * @param AttributeRepositoryInterface $attributeRepository
+ * @param AttributeFactory $attributeFactory
+ * @param ResourceModelAttribute $resourceModelAttribute
+ */
+ public function __construct(
+ DataMerger $dataMerger,
+ ProcessorInterface $processor,
+ AttributeRepositoryInterface $attributeRepository,
+ AttributeFactory $attributeFactory,
+ ResourceModelAttribute $resourceModelAttribute
+ ) {
+ $this->dataMerger = $dataMerger;
+ $this->processor = $processor;
+ $this->attributeFactory = $attributeFactory;
+ $this->resourceModelAttribute = $resourceModelAttribute;
+ $this->attributeRepository = $attributeRepository;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function apply(array $data = []): ?DataObject
+ {
+ /** @var Attribute $attr */
+ $attr = $this->attributeFactory->createAttribute(Attribute::class, self::DEFAULT_DATA);
+ $mergedData = $this->processor->process($this, $this->dataMerger->merge(self::DEFAULT_DATA, $data));
+ $attr->setData($mergedData);
+ $this->resourceModelAttribute->save($attr);
+ return $attr;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function revert(DataObject $data): void
+ {
+ $this->attributeRepository->deleteById($data['attribute_id']);
+ }
+}
diff --git a/app/code/Magento/Catalog/Test/Fixture/Product.php b/app/code/Magento/Catalog/Test/Fixture/Product.php
index c6d0905c539ed..f856bff65a1b1 100644
--- a/app/code/Magento/Catalog/Test/Fixture/Product.php
+++ b/app/code/Magento/Catalog/Test/Fixture/Product.php
@@ -120,11 +120,7 @@ public function apply(array $data = []): ?DataObject
public function revert(DataObject $data): void
{
$service = $this->serviceFactory->create(ProductRepositoryInterface::class, 'deleteById');
- $service->execute(
- [
- 'sku' => $data->getSku()
- ]
- );
+ $service->execute(['sku' => $data->getSku()]);
}
/**
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductByIdOnProductGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductByIdOnProductGridActionGroup.xml
new file mode 100644
index 0000000000000..86b3e942f3f46
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCheckProductByIdOnProductGridActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Check the checkbox for the product on the Product Grid using Product ID
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteCreatedColorSpecificAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteCreatedColorSpecificAttributeActionGroup.xml
new file mode 100644
index 0000000000000..2907f88446ec2
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminDeleteCreatedColorSpecificAttributeActionGroup.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ Delete the created new colors in color attribute
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SortProductsByIdDescendingActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SortProductsByIdDescendingActionGroup.xml
index 635e36c458519..7ca4d177ae4fa 100644
--- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SortProductsByIdDescendingActionGroup.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SortProductsByIdDescendingActionGroup.xml
@@ -15,5 +15,6 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml
index e5b6efbd6373a..57d5103a87a7e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml
@@ -31,6 +31,13 @@
true
true
+
+ SubCat
+ simplesubcategory
+ simplesubcategory
+ true
+ true
+
NewRootCategory
newrootcategory
@@ -309,4 +316,4 @@
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ImageData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ImageData.xml
index e1072001b56e5..ebde9601149e0 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/ImageData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ImageData.xml
@@ -43,6 +43,12 @@
jpg
jpg
+
+ GifImageWithUnusedTransparencyIndex
+ transparency_index.gif
+ transparency_index
+ gif
+
largeimage
large.jpg
diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml
index 98085011dbc1c..6791c9ad7787f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeData.xml
@@ -31,6 +31,29 @@
true
ProductAttributeFrontendLabel
+
+ attribute
+ textarea
+ global
+ false
+ false
+ true
+ true
+ text
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+ ProductAttributeFrontendLabel
+
attribute
select
@@ -134,7 +157,7 @@
ProductAttributeFrontendLabel
- testattribute
+ testattribute
select
global
false
@@ -299,7 +322,7 @@
date
No
-
+
select
Dropdown
No
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml
index c22677f0e0f5f..8b1a25adb267c 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml
@@ -23,7 +23,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection/AttributePropertiesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection/AttributePropertiesSection.xml
index aa86573044279..021057e06e7ef 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection/AttributePropertiesSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateProductAttributeSection/AttributePropertiesSection.xml
@@ -25,7 +25,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml
index e4b33ac795559..8e2877b47b64a 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml
@@ -23,6 +23,8 @@
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml
index 0a3c67bc00d5b..f0a1b0f2e9452 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml
@@ -83,5 +83,9 @@
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml
index 05391d9babce5..28cd0ad14e2d5 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml
@@ -11,6 +11,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StoreFrontRecentProductSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StoreFrontRecentProductSection.xml
index 387e252ae93d4..acc88d0001775 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/StoreFrontRecentProductSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StoreFrontRecentProductSection.xml
@@ -11,5 +11,6 @@
-
\ No newline at end of file
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml
index 526ac700a0b5a..744b279e4c77d 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryMainSection.xml
@@ -44,5 +44,6 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml
index 61e6a345b9ba5..de1c010797b6a 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryProductSection.xml
@@ -35,5 +35,6 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml
index 26a5452ee018c..6edef36fd98f4 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml
@@ -27,5 +27,9 @@
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml
index 6ea8102a035d3..370487075c3c0 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductInfoMainSection.xml
@@ -30,7 +30,10 @@
+
+
+
@@ -106,7 +109,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddExistingProductAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddExistingProductAttributeFromProductPageTest.xml
index 38d8b572ac62d..34603cbc6b3c9 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AddExistingProductAttributeFromProductPageTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddExistingProductAttributeFromProductPageTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddNewProductAttributeInProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddNewProductAttributeInProductPageTest.xml
index f5dec88789bf0..cffeb9006d448 100755
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AddNewProductAttributeInProductPageTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddNewProductAttributeInProductPageTest.xml
@@ -21,7 +21,10 @@
-
+
+
+
+
@@ -33,7 +36,8 @@
-
+
+
@@ -97,7 +101,11 @@
-
+
+
+
+
+
@@ -106,6 +114,9 @@
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml
index b677fae5e58ea..b5068e5b0b37e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddAndUpdateCustomGroupInAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddAndUpdateCustomGroupInAttributeSetTest.xml
index 35f17f1dcb32a..af67fc0e9b82e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddAndUpdateCustomGroupInAttributeSetTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddAndUpdateCustomGroupInAttributeSetTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml
index bed5297041dd1..ef4b1879dde52 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml
index d713660d7ee63..412fa19a295a5 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageForCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageForCategoryTest.xml
index e4cf255a03e05..b62aa752449a0 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageForCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageForCategoryTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml
index 5786eabf9c840..4f2dd66e4d4ff 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAlertDoseNotAppearOnProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAlertDoseNotAppearOnProductPageTest.xml
index 471880b5f6ea5..c53234ab3f56b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAlertDoseNotAppearOnProductPageTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAlertDoseNotAppearOnProductPageTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/StoreFrontDeleteProductImagesAssignedDifferentRolesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/StoreFrontDeleteProductImagesAssignedDifferentRolesTest.xml
index a3d50a9c361b8..31ff50c0a0ef1 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/StoreFrontDeleteProductImagesAssignedDifferentRolesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/StoreFrontDeleteProductImagesAssignedDifferentRolesTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml
index f5cf4cd3f2417..5acd0fa09b4e5 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAssignProductAttributeToAttributeSetTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogCategoriesNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogCategoriesNavigateMenuTest.xml
index 8d59e475ca10c..6d3eddbfed5b5 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogCategoriesNavigateMenuTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogCategoriesNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogProductsNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogProductsNavigateMenuTest.xml
index 1dec1073f56ee..40b6ddeaff506 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogProductsNavigateMenuTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCatalogProductsNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeArrangementOfAttributesInAnAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeArrangementOfAttributesInAnAttributeSetTest.xml
index c682c7ab4001e..67ceaf89c01db 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeArrangementOfAttributesInAnAttributeSetTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeArrangementOfAttributesInAnAttributeSetTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeGroupTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeGroupTest.xml
index 68e6040277247..8aea1a0c1cd39 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeGroupTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeGroupTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml
index f2413a1523394..b1911645cf00d 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml
index 75f805bb99e04..3bf36ca9e9486 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml
index 97992c35b7316..a30209128e7d3 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml
index 88d24540b11f8..dac214f03d456 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckMediaRolesForFirstAddedImageViaApiTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckMediaRolesForFirstAddedImageViaApiTest.xml
index ce4cb250796bd..901b8973c8e2e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckMediaRolesForFirstAddedImageViaApiTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckMediaRolesForFirstAddedImageViaApiTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckNewCategoryLevelAddedViaApiTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckNewCategoryLevelAddedViaApiTest.xml
index 92a3b298aa6b6..b460c8125c221 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckNewCategoryLevelAddedViaApiTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckNewCategoryLevelAddedViaApiTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml
index c15cedadb4460..9892b13b36984 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml
index f956c73319425..45506ee1cc8a2 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml
index b23ce827d5d69..668796ff95808 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml
@@ -16,6 +16,7 @@
Product List page filter grid by created product, add mentioned columns to grid, check values."/>
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesWithDifferentCurrencyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesWithDifferentCurrencyTest.xml
index 3fdd278a6bacd..fe7426bad856c 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesWithDifferentCurrencyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckProductListPriceAttributesWithDifferentCurrencyTest.xml
@@ -20,6 +20,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml
index 2cdec1405e9f9..26faf7bb42a48 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChecksIfOnlyOneQuantityConfigurationIsDisplayedForBundleProductWhileCreatingAnOrderTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChecksIfOnlyOneQuantityConfigurationIsDisplayedForBundleProductWhileCreatingAnOrderTest.xml
new file mode 100644
index 0000000000000..d326204999c7d
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChecksIfOnlyOneQuantityConfigurationIsDisplayedForBundleProductWhileCreatingAnOrderTest.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml
index a865cbfdef22c..384e3d9362c77 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndEditSimpleProductSettingsTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml
index c42569385c59a..bf1835c03fb7e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToSimpleTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToSimpleTest.xml
index 7191f1971b319..6406c914f40f0 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToSimpleTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToSimpleTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml
index 19064458ae2a4..472d2a826a8f6 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoriesWithTheSameCategoryNamesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoriesWithTheSameCategoryNamesTest.xml
index f2fcc7a3e8905..3408ba9827397 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoriesWithTheSameCategoryNamesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoriesWithTheSameCategoryNamesTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCategoryFormDisplaySettingsUIValidationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCategoryFormDisplaySettingsUIValidationTest.xml
index 9ec19ee97eed0..3ab36faf8d01f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCategoryFormDisplaySettingsUIValidationTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCategoryFormDisplaySettingsUIValidationTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminConfigDefaultCategoryLayoutFromConfigurationSettingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminConfigDefaultCategoryLayoutFromConfigurationSettingTest.xml
index 852353300d090..4190adbf6b3b6 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminConfigDefaultCategoryLayoutFromConfigurationSettingTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminConfigDefaultCategoryLayoutFromConfigurationSettingTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml
index 83404391abca9..fe722f73e8508 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminUploadCategoryImageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminUploadCategoryImageTest.xml
index 6f183a44d8277..2958a76f4836e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminUploadCategoryImageTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminUploadCategoryImageTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAPIForMultiStoresTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAPIForMultiStoresTest.xml
new file mode 100644
index 0000000000000..3d640eb8e1be6
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAPIForMultiStoresTest.xml
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml
index adb9d9bd824f0..68c2e80e5dd95 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml
index a711228e659b1..9812f64660816 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml
index 6d7d56861b731..440d0b5ee0486 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml
index f60312f19a7e0..d8775fd307003 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml
index 31ad92afb9d4f..135d4a7ac3c3b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml
index 3f51fa2296219..877a084aa24c9 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml
index 6f97cc7abe71f..ea5209dce6f33 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml
index 5931193dbe7ca..75cab8bd71851 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml
index 5c00028ee69ba..d14f1c981537e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDuplicateCategoryTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml
index 45b776a6c8713..e8ea05c240c18 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml
index 52cac23574b53..d8af1d3194d12 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductPageTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml
index e5251b5fee406..aef4c7497de64 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewAttributeFromProductTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml
index fc5fa60f754c4..b603c1a1252e6 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml
index 90730a6516d39..41604104003ec 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml
index da87880477de5..2e50911f76559 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeTextSwatchFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeTextSwatchFromProductPageTest.xml
index 7a087f02a3fff..290a1991c825f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeTextSwatchFromProductPageTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeTextSwatchFromProductPageTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeVisualSwatchFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeVisualSwatchFromProductPageTest.xml
index 686f8aa865c22..1fdc7e1563823 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeVisualSwatchFromProductPageTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeVisualSwatchFromProductPageTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSetTest.xml
index d129ad3a04d0f..160e7e8908d97 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSetTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductCustomAttributeSetTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateProductTest.xml
index c18f1c69f87ff..7ca613ec00118 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateProductTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml
index e61684b91c082..580f30e3548af 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml
index 7df525f5f9f2f..e7df46287282d 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml
index f7f68b9f85c04..2f724aab7877e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminConfigDefaultProductLayoutFromConfigurationSettingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminConfigDefaultProductLayoutFromConfigurationSettingTest.xml
index 819835dead304..1bfe63748bf0a 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminConfigDefaultProductLayoutFromConfigurationSettingTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminConfigDefaultProductLayoutFromConfigurationSettingTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductCommaSeparatedPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductCommaSeparatedPriceTest.xml
index a18754f0ecb18..501a6dbb1e240 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductCommaSeparatedPriceTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductCommaSeparatedPriceTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml
index 4b40f04f098e0..d3174a11d6346 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductTest.xml
index 4ef9e2ec1fc62..d303dbb63c91c 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml
index b1f18a770ea0b..e7c53a72bb1ca 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateTwoSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateTwoSimpleProductTest.xml
index d74b15b01ea3b..39d6b45e4e595 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateTwoSimpleProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateTwoSimpleProductTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml
index 494ff1008e6ef..a4cab3cad7b5f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSubcategoryWithEmptyRequiredFieldsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSubcategoryWithEmptyRequiredFieldsTest.xml
index 68b9c6b32c981..0ad6d399bdb73 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSubcategoryWithEmptyRequiredFieldsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSubcategoryWithEmptyRequiredFieldsTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSwatchAttributeWithSpecialCharactersTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSwatchAttributeWithSpecialCharactersTest.xml
index 18fb840202f47..f181092030c22 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSwatchAttributeWithSpecialCharactersTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSwatchAttributeWithSpecialCharactersTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml
index 4c02c57dae535..23f3ff0566eec 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml
index 71665e4064d55..b425968b36ed2 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml
index 81d897d4836a5..faed5ea16810d 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml
index 7aeb1a1397952..d71247647caa2 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml
@@ -24,6 +24,8 @@
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml
index b82c6ba13550c..3c01e43e85d43 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml
index e26a42006b0ac..034af7e8b2147 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteCustomGroupInAnAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteCustomGroupInAnAttributeSetTest.xml
index d4c7e20223a68..ed9104bf08594 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteCustomGroupInAnAttributeSetTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteCustomGroupInAnAttributeSetTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml
index 841b08e70fb4f..bf11030cce59f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteDropdownProductAttributeFromAttributeSetTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml
index abbc541fbbcf3..1489df34ac3d9 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml
index f43048f00e6b1..e71289b59eaa1 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml
index 6712bf90c4700..740aa54aab502 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml
index ae92e997e0aa0..a7c63e15789d0 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml
index 92b190efc6210..8f6ce45bfd538 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml
index 900c40dec14da..b88b6d5d75757 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml
index 2034ea8ec8211..3038f1fe468f9 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml
index b7e037b323ee2..bbb6d73d23219 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml
index a6cd3c8b52b23..ed88c80619a6a 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml
index cb2e6b8e483a0..efbc198b05a82 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeletedCategoryNotShownAsAvailableOnProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeletedCategoryNotShownAsAvailableOnProductPageTest.xml
index 744dbcc32e7fd..6d88ea477a883 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeletedCategoryNotShownAsAvailableOnProductPageTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeletedCategoryNotShownAsAvailableOnProductPageTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDisableProductOnChangingAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDisableProductOnChangingAttributeSetTest.xml
index db932e5d4751f..11f97edf5019d 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDisableProductOnChangingAttributeSetTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDisableProductOnChangingAttributeSetTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml
index 83e9a70ad285f..3f180ed0369dc 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml
index 2b1ba0894ecd0..ce56fa18dee2b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml
index 9f757ff72d067..56e13e84eee58 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilteringCategoryProductsUsingScopeSelectorTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml
index f10288bea36d9..997c66d19098a 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml
index 82a9a610e32c9..492310d457bf2 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml
index fe13603fa6154..f6ec18e70a8ed 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml
index c8aba75838f52..08d7c5744acbd 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml
@@ -45,12 +45,12 @@
-
-
+
+
-
-
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryTest.xml
index 2bcabbd54f49c..d73d13eafd77d 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml
index 96a8f711ea569..256768848efb6 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml
index efd2a54fc5133..e22b20cdad263 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml
index 14636d8b8ae3d..1cf398d852c50 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml
index 9eba952c1a3b2..18d2b8bed5799 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml
index 203ed2c530fbf..d272aed83f9cd 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml
index 4f3feba01a92c..84770061c12c8 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductAttributeLabelDontAllowHtmlTagsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductAttributeLabelDontAllowHtmlTagsTest.xml
index f3981e7b8f76a..d3b8113891d09 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductAttributeLabelDontAllowHtmlTagsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductAttributeLabelDontAllowHtmlTagsTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml
index 85fec54de2f0c..25a18074689da 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml
@@ -94,8 +94,8 @@
-
-
+
+
@@ -124,8 +124,8 @@
-
-
+
+
@@ -180,8 +180,8 @@
-
-
+
+
@@ -240,8 +240,8 @@
-
-
+
+
@@ -302,8 +302,8 @@
-
-
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml
index ef44d0b418b44..ca77574edfccd 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml
index d677eda5b0920..29819f4151fca 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml
index 449d201393206..6268bc4e65ca8 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridSwitchViewBookmarkTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridSwitchViewBookmarkTest.xml
index 30c1b9296553a..d3008c2fbb397 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridSwitchViewBookmarkTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridSwitchViewBookmarkTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml
index bfa80c2e24b48..3d6a246b27b45 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml
index 410e945cea7e5..a3f95eaeba5d3 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml
index 99fe4dd0c135d..33a6a873d444e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml
index 521256cf57dd5..5720723ae4466 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml
index 4a544b60f15b6..3854d34f56951 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml
index c5b475f616b7b..4759f97a9d47b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageFromCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageFromCategoryTest.xml
index 8033a2dffec7c..aced4c8786745 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageFromCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageFromCategoryTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveProductAttributeFromAttributeSetUsingDragAndDropTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveProductAttributeFromAttributeSetUsingDragAndDropTest.xml
index de3e2a9b9dada..2fdf988c48d6c 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveProductAttributeFromAttributeSetUsingDragAndDropTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveProductAttributeFromAttributeSetUsingDragAndDropTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRenameCategoryOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRenameCategoryOnStoreViewLevelTest.xml
index f647492775b2f..8ece4af9eaab8 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRenameCategoryOnStoreViewLevelTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRenameCategoryOnStoreViewLevelTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml
index 49add85f76806..041f690e627c6 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml
index 92818c846fcf1..3a0b78a0fa81c 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSaveProductByCustomDateWithCustomDateAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSaveProductByCustomDateWithCustomDateAttributeTest.xml
new file mode 100644
index 0000000000000..c4df428371db2
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSaveProductByCustomDateWithCustomDateAttributeTest.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminScopeSelectionShouldBeDisabledOnMediaGalleryProductAttributeEditTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminScopeSelectionShouldBeDisabledOnMediaGalleryProductAttributeEditTest.xml
index 6e0ad56f0d5df..35e34fd8cb2ec 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminScopeSelectionShouldBeDisabledOnMediaGalleryProductAttributeEditTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminScopeSelectionShouldBeDisabledOnMediaGalleryProductAttributeEditTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductGifWithUnusedTransparencyImageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductGifWithUnusedTransparencyImageTest.xml
new file mode 100644
index 0000000000000..2877313e744ce
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductGifWithUnusedTransparencyImageTest.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml
index 38ba4f4331c1d..99f8c3d9b78f4 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresAttributeSetNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresAttributeSetNavigateMenuTest.xml
index 96a6dc7b70a7a..17a8285ca5a4a 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresAttributeSetNavigateMenuTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresAttributeSetNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresProductNavigateMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresProductNavigateMenuTest.xml
index a8eddeb8b613f..55dcb87390b43 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresProductNavigateMenuTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminStoresProductNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminTestForRelatedProductsPriceBoxIsNotBeingUpdatedWhenNotNeeded.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminTestForRelatedProductsPriceBoxIsNotBeingUpdatedWhenNotNeeded.xml
new file mode 100644
index 0000000000000..554f8e2448b89
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminTestForRelatedProductsPriceBoxIsNotBeingUpdatedWhenNotNeeded.xml
@@ -0,0 +1,186 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ grabProductPrice
+ As low as
+
+
+
+ grabProductPrice
+ As low as
+
+
+
+
+
+
+
+ grabProductPrice
+ As low as
+
+
+
+ grabProductPrice
+ As low as
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml
index 7989de271b3ad..6d9b91815034a 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminTierPriceNotAvailableForProductOptionsWithoutTierPriceTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml
index bb6098f55cf96..367dba6500d3b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUnassignProductAttributeFromAttributeSetTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml
index bb7aca5ed7706..a1063bbc2402a 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml
index ea50a17b47b44..360f3aa14a7a3 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml
index f8c3857fb5442..6532136985352 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml
index 4389bf4bd6383..d166d22b804de 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml
index c04212a220f4a..0c40a411a5af0 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsDefaultSortingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsDefaultSortingTest.xml
index 051495b257012..e3026fcee6c27 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsDefaultSortingTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsDefaultSortingTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateOfSystemProductAttributeIsNotPossibleTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateOfSystemProductAttributeIsNotPossibleTest.xml
index 3c4dd60785614..c880e232b8fc2 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateOfSystemProductAttributeIsNotPossibleTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateOfSystemProductAttributeIsNotPossibleTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml
index 8b2d447d297d0..2e8007946b866 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml
index 0fd564d86f03f..edf1ab7910b14 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml
index ad14bc274a52d..93f041f172720 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml
index 2e72bb734fe00..067453be59978 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml
index 669e5cd040c5e..511cb4af97d72 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml
index fa9aea7683200..adf294c954ba3 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml
index 4431991fdbb78..4759ed35181ce 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml
index 01feac998060b..c41c00c6665b3 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml
index 214ca0e9b8576..bea04c1714de6 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml
index b436601356b31..44a074166189a 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml
index 607ebd1a626a2..cab3f33e516b4 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml
index 27b65c53b835b..a3050e015a930 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml
index b0c14bcb79e18..3df499905ea2b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml
index 71cb86e765fd5..7c066343ee7f3 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml
@@ -17,6 +17,7 @@
+
@@ -28,6 +29,9 @@
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateAllNestedCategoryInWidgetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateAllNestedCategoryInWidgetTest.xml
index c79567d7f674f..b59531810d594 100755
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateAllNestedCategoryInWidgetTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminValidateAllNestedCategoryInWidgetTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCloseCreateCustomProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCloseCreateCustomProductAttributeTest.xml
index b989450ea2288..f921187cda63b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCloseCreateCustomProductAttributeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCloseCreateCustomProductAttributeTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml
index 30771fcfd947b..3d912906b4cbf 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml
index 5115399db9e3b..a85dbe696bad1 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml
index cacf4f3f4c9f5..8a8ef7fdf6d7a 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml
index 6ca81ee494730..7754bfe14e805 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml
index dc6409043f67f..3d892c2b16213 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByDescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByDescriptionTest.xml
index 64da7e8599d07..e23f3472d8dc7 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByDescriptionTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByDescriptionTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByNameTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByNameTest.xml
index 12056962bac23..972616cad853e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByNameTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByNameTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByPriceTest.xml
index 68a69644d3d7b..cbedba0127ebc 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByPriceTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByPriceTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByShortDescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByShortDescriptionTest.xml
index f6cfb58bf71df..a2db74321df2e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByShortDescriptionTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductByShortDescriptionTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductBySkuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductBySkuTest.xml
index 132e82d49085e..cbc39a7e80bea 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductBySkuTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdvanceCatalogSearchVirtualProductTest/AdvanceCatalogSearchVirtualProductBySkuTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml
index ebc7bcd542a65..fbdfc3df7b6b0 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml
@@ -147,8 +147,10 @@
-
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHintTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHintTest.xml
index bc9da6efcbf42..887894619706f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHintTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/ConfigurableOptionTextInputLengthValidationHintTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateAnchorCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateAnchorCategoryTest.xml
index b983112d91e4b..5e1da0f77eb8a 100755
--- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateAnchorCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateAnchorCategoryTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDateTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDateTest.xml
index 87dfca735cc0a..a7923f49d3f85 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDateTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDateTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownWithSingleQuoteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownWithSingleQuoteTest.xml
index b756df331d0c5..af637bf1bba58 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownWithSingleQuoteTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownWithSingleQuoteTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityMultiSelectTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityMultiSelectTest.xml
index 72d3fa04591c2..2c357d120e11e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityMultiSelectTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityMultiSelectTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityPriceTest.xml
index c7b9613e1ee48..7eaec8bab78fd 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityPriceTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityPriceTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityTextFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityTextFieldTest.xml
index 629d084b2617c..29073927dcba5 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityTextFieldTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityTextFieldTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityWithReservedKeysTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityWithReservedKeysTest.xml
index 8acb0bef4c438..c101d4bae0773 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityWithReservedKeysTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityWithReservedKeysTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml
index 18869e670f62f..c9b221f414e94 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml
index c062b9bc2f949..2e44ad45edf67 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DisplayingCustomAttributesInProductGridTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DisplayingCustomAttributesInProductGridTest.xml
index 123a686e421c1..87d5f01d093f9 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/DisplayingCustomAttributesInProductGridTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/DisplayingCustomAttributesInProductGridTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml
index 3b0fad592fed8..7a10c0e949e31 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml
@@ -18,13 +18,16 @@
+
+
+
+
-
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAttributeWithoutValueInCompareListTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAttributeWithoutValueInCompareListTest.xml
index 9c18ba6cd654b..3b14122289d2b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAttributeWithoutValueInCompareListTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAttributeWithoutValueInCompareListTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsAdditionalWebsiteTest.xml
similarity index 98%
rename from app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml
rename to app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsAdditionalWebsiteTest.xml
index f32ba620732fc..6ed898d5dee49 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsAdditionalWebsiteTest.xml
@@ -17,6 +17,8 @@
+
+
@@ -100,6 +102,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SavingCustomAttributeValuesUsingUITest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SavingCustomAttributeValuesUsingUITest.xml
new file mode 100644
index 0000000000000..2c8d91d997177
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/SavingCustomAttributeValuesUsingUITest.xml
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml
index 902c4339cf208..2752aeeadd3b2 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontSimpleProductWithSpecialAndTierDiscountPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontSimpleProductWithSpecialAndTierDiscountPriceTest.xml
index 90d432b70f794..d8b5af6694d75 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontSimpleProductWithSpecialAndTierDiscountPriceTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontSimpleProductWithSpecialAndTierDiscountPriceTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAddProductWithBackordersAllowedOnProductLevelToCartTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAddProductWithBackordersAllowedOnProductLevelToCartTest.xml
index ef569a56a3fed..c4e64a563f065 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAddProductWithBackordersAllowedOnProductLevelToCartTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontAddProductWithBackordersAllowedOnProductLevelToCartTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml
index fb4bd4d1dcb74..bacdf5c8f695f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryHighlightedAndProductDisplayedTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryHighlightedAndProductDisplayedTest.xml
index acceb6662d59e..4a28581f22840 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryHighlightedAndProductDisplayedTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryHighlightedAndProductDisplayedTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml
index 973fcb68b63e9..992a87016619b 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml
index c9be526e095aa..11a6228d81f45 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontConfigurableOptionsThumbImagesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontConfigurableOptionsThumbImagesTest.xml
index d0c6c4fe86aee..6300d26fe2383 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontConfigurableOptionsThumbImagesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontConfigurableOptionsThumbImagesTest.xml
@@ -17,6 +17,7 @@
(visible and active) for each selected option for the configurable product"/>
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontEnsureThatAccordionAnchorIsVisibleOnViewportOnceClickedTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontEnsureThatAccordionAnchorIsVisibleOnViewportOnceClickedTest.xml
index 66f900293dd1c..c6ee3aee3c29d 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontEnsureThatAccordionAnchorIsVisibleOnViewportOnceClickedTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontEnsureThatAccordionAnchorIsVisibleOnViewportOnceClickedTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml
index 58e6ee43ce744..6f71809249332 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontFotoramaArrowsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontFotoramaArrowsTest.xml
index f9ad2d69264f4..367fb8143c471 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontFotoramaArrowsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontFotoramaArrowsTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductImageSlideTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductImageSlideTest.xml
new file mode 100644
index 0000000000000..80919fd06c4f2
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductImageSlideTest.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductImageWithDotTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductImageWithDotTest.xml
index a711a585a81b0..e16e079afd1f4 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductImageWithDotTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductImageWithDotTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml
index e6aea3e7b3321..64705ff725922 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml
index 8bbe9b137abbb..c86331eba9631 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithEmptyAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithEmptyAttributeTest.xml
index 95072f81e02b8..79c110775f2e5 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithEmptyAttributeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithEmptyAttributeTest.xml
@@ -17,6 +17,7 @@
+
@@ -38,7 +39,7 @@
-
+
@@ -49,6 +50,6 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithMediaThumbGallerySliderTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithMediaThumbGallerySliderTest.xml
index 5f5279cc483c1..c7c4bb096c79e 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithMediaThumbGallerySliderTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductWithMediaThumbGallerySliderTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml
index d56faf9d5dec4..e96290b326ed1 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductsCompareWithEmptyAttributeTest.xml
@@ -17,6 +17,7 @@
+
@@ -41,7 +42,7 @@
-
+
@@ -81,6 +82,6 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml
index deafab6a95258..2481e8aad9f42 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml
index ba7388ebb1ccc..e62c83e6666f0 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml
index 9e01caa0f1d32..7e23dc67d2f06 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml
index e19446c157605..5a62352e8d50c 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSelectedByQueryParamsConfigurableOptionsThumbImagesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSelectedByQueryParamsConfigurableOptionsThumbImagesTest.xml
index 9821121d8c171..57de0d2c9a95f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSelectedByQueryParamsConfigurableOptionsThumbImagesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSelectedByQueryParamsConfigurableOptionsThumbImagesTest.xml
@@ -20,6 +20,7 @@
to selected needed option."/>
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml
index 5ff0a002e11ed..8ba09a80f0c21 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCategoryPageNotCachedTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCategoryPageNotCachedTest.xml
new file mode 100644
index 0000000000000..6c79b40b3ff94
--- /dev/null
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCategoryPageNotCachedTest.xml
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 100
+
+
+
+ 200
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCompareListVisibilityForMultiWebsiteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCompareListVisibilityForMultiWebsiteTest.xml
index 945ebe153a991..e778e3e7067ca 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCompareListVisibilityForMultiWebsiteTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCompareListVisibilityForMultiWebsiteTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyProductAfterPartialReindexOnSeveralWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyProductAfterPartialReindexOnSeveralWebsitesTest.xml
index e5f464920c3ee..1369bcf7fd34f 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyProductAfterPartialReindexOnSeveralWebsitesTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyProductAfterPartialReindexOnSeveralWebsitesTest.xml
@@ -82,7 +82,8 @@
-
+
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyThatRecentlyOrderedWidgetShowOnlyFiveProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyThatRecentlyOrderedWidgetShowOnlyFiveProductTest.xml
index 64901a541a779..cf92289cf8c6d 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyThatRecentlyOrderedWidgetShowOnlyFiveProductTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyThatRecentlyOrderedWidgetShowOnlyFiveProductTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml
index 9c68c08064081..4f9f17ba7e017 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml
index 59e3700acf5c3..a9fa033ffd4c9 100644
--- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml
+++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyChildCategoriesShouldNotIncludeInMenuTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php
index 886b03e0f3c1e..73478b80091f0 100644
--- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php
@@ -388,8 +388,8 @@ public function initializeDataProvider()
return [
[
'single_store' => false,
- 'website_ids' => ['1' => 1, '2' => 1],
- 'expected_website_ids' => ['1' => 1, '2' => 1],
+ 'website_ids' => ['1' => 1, '2' => 2],
+ 'expected_website_ids' => ['1' => 1, '2' => 2],
'links' => [],
'linkTypes' => ['related', 'upsell', 'crosssell'],
'expected_links' => [],
@@ -423,8 +423,8 @@ public function initializeDataProvider()
// Related links
[
'single_store' => false,
- 'website_ids' => ['1' => 1, '2' => 1],
- 'expected_website_ids' => ['1' => 1, '2' => 1],
+ 'website_ids' => ['1' => 1, '2' => 2],
+ 'expected_website_ids' => ['1' => 1, '2' => 2],
'links' => [
'related' => [
0 => [
@@ -449,8 +449,8 @@ public function initializeDataProvider()
// Custom link
[
'single_store' => false,
- 'website_ids' => ['1' => 1, '2' => 1],
- 'expected_website_ids' => ['1' => 1, '2' => 1],
+ 'website_ids' => ['1' => 1, '2' => 2],
+ 'expected_website_ids' => ['1' => 1, '2' => 2],
'links' => [
'customlink' => [
0 => [
@@ -475,8 +475,8 @@ public function initializeDataProvider()
// Both links
[
'single_store' => false,
- 'website_ids' => ['1' => 1, '2' => 1],
- 'expected_website_ids' => ['1' => 1, '2' => 1],
+ 'website_ids' => ['1' => 1, '2' => 2],
+ 'expected_website_ids' => ['1' => 1, '2' => 2],
'links' => [
'related' => [
0 => [
@@ -515,8 +515,8 @@ public function initializeDataProvider()
// Undefined link type
[
'single_store' => false,
- 'website_ids' => ['1' => 1, '2' => 1],
- 'expected_website_ids' => ['1' => 1, '2' => 1],
+ 'website_ids' => ['1' => 1, '2' => 2],
+ 'expected_website_ids' => ['1' => 1, '2' => 2],
'links' => [
'related' => [
0 => [
diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/NewActionTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/NewActionTest.php
old mode 100644
new mode 100755
index 974c85b2b5c98..cad43f39f0261
--- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/NewActionTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/NewActionTest.php
@@ -16,6 +16,9 @@
use Magento\Catalog\Controller\Adminhtml\Product\NewAction;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Test\Unit\Controller\Adminhtml\ProductTest;
+use Magento\Framework\RegexValidator;
+use Magento\Framework\Validator\Regex;
+use Magento\Framework\Validator\RegexFactory;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
use Magento\Framework\View\Result\PageFactory;
use PHPUnit\Framework\MockObject\MockObject;
@@ -42,6 +45,26 @@ class NewActionTest extends ProductTest
*/
protected $initializationHelper;
+ /**
+ * @var RegexValidator|MockObject
+ */
+ private $regexValidator;
+
+ /**
+ * @var RegexFactory
+ */
+ private $regexValidatorFactoryMock;
+
+ /**
+ * @var Regex|MockObject
+ */
+ private $regexValidatorMock;
+
+ /**
+ * @var ForwardFactory&MockObject|MockObject
+ */
+ private $resultForwardFactory;
+
protected function setUp(): void
{
$this->productBuilder = $this->createPartialMock(
@@ -63,37 +86,78 @@ protected function setUp(): void
->disableOriginalConstructor()
->setMethods(['create'])
->getMock();
- $resultPageFactory->expects($this->atLeastOnce())
- ->method('create')
- ->willReturn($this->resultPage);
$this->resultForward = $this->getMockBuilder(Forward::class)
->disableOriginalConstructor()
->getMock();
- $resultForwardFactory = $this->getMockBuilder(ForwardFactory::class)
+ $this->resultForwardFactory = $this->getMockBuilder(ForwardFactory::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['create'])
+ ->getMock();
+
+ $this->regexValidatorFactoryMock = $this->getMockBuilder(RegexFactory::class)
->disableOriginalConstructor()
->setMethods(['create'])
->getMock();
- $resultForwardFactory->expects($this->any())
- ->method('create')
- ->willReturn($this->resultForward);
+ $this->regexValidatorMock = $this->createMock(Regex::class);
+ $this->regexValidatorFactoryMock->method('create')
+ ->willReturn($this->regexValidatorMock);
+ $this->regexValidator = new regexValidator($this->regexValidatorFactoryMock);
$this->action = (new ObjectManager($this))->getObject(
NewAction::class,
[
'context' => $this->initContext(),
'productBuilder' => $this->productBuilder,
'resultPageFactory' => $resultPageFactory,
- 'resultForwardFactory' => $resultForwardFactory,
+ 'resultForwardFactory' => $this->resultForwardFactory,
+ 'regexValidator' => $this->regexValidator,
]
);
}
- public function testExecute()
+ /**
+ * Test execute method input validation.
+ *
+ * @param string $value
+ * @param bool $exceptionThrown
+ * @dataProvider validationCases
+ */
+ public function testExecute(string $value, bool $exceptionThrown): void
+ {
+ if ($exceptionThrown) {
+ $this->action->getRequest()->expects($this->any())
+ ->method('getParam')
+ ->willReturn($value);
+ $this->resultForwardFactory->expects($this->any())
+ ->method('create')
+ ->willReturn($this->resultForward);
+ $this->resultForward->expects($this->once())
+ ->method('forward')
+ ->with('noroute')
+ ->willReturn(true);
+ $this->assertTrue($this->action->execute());
+ } else {
+ $this->action->getRequest()->expects($this->any())->method('getParam')->willReturn($value);
+ $this->regexValidatorMock->expects($this->any())
+ ->method('isValid')
+ ->with($value)
+ ->willReturn(true);
+
+ $this->assertEquals(true, $this->regexValidator->validateParamRegex($value));
+ }
+ }
+
+ /**
+ * Validation cases.
+ *
+ * @return array
+ */
+ public function validationCases(): array
{
- $this->action->getRequest()->expects($this->any())->method('getParam')->willReturn(true);
- $this->action->getRequest()->expects($this->any())->method('getFullActionName')
- ->willReturn('catalog_product_new');
- $this->action->execute();
+ return [
+ 'execute-with-exception' => ['simple\' and true()]|*[self%3a%3ahandle%20or%20self%3a%3alayout',true],
+ 'execute-without-exception' => ['catalog_product_new',false]
+ ];
}
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php
index 4317607fd661e..239e19c84dbf8 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryTest.php
@@ -214,7 +214,7 @@ public function testMoveWhenCannotFindParentCategory(): void
{
$this->expectException('Magento\Framework\Exception\LocalizedException');
$this->expectExceptionMessage('Sorry, but we can\'t find the new parent category you selected.');
- $this->markTestIncomplete('MAGETWO-31165');
+ $this->markTestSkipped('MAGETWO-31165');
$parentCategory = $this->createPartialMock(
Category::class,
['getId', 'setStoreId', 'load']
@@ -260,7 +260,7 @@ public function testMoveWhenParentCategoryIsSameAsChildCategory(): void
$this->expectExceptionMessage(
'We can\'t move the category because the parent category name matches the child category name.'
);
- $this->markTestIncomplete('MAGETWO-31165');
+ $this->markTestSkipped('MAGETWO-31165');
$parentCategory = $this->createPartialMock(
Category::class,
['getId', 'setStoreId', 'load']
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/ImportTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/ImportTest.php
deleted file mode 100644
index 0dc0e23ccb3c2..0000000000000
--- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/ImportTest.php
+++ /dev/null
@@ -1,37 +0,0 @@
-getMockBuilder(Processor::class)
- ->disableOriginalConstructor()
- ->getMock();
- $processorMock->expects($this->once())
- ->method('markIndexerAsInvalid');
-
- $subjectMock = $this->getMockBuilder(Import::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $import = true;
-
- $model = new \Magento\CatalogImportExport\Model\Indexer\Category\Product\Plugin\Import($processorMock);
-
- $this->assertEquals(
- $import,
- $model->afterImportSource($subjectMock, $import)
- );
- }
-}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Plugin/ImportTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Plugin/ImportTest.php
deleted file mode 100644
index 2dafc07ffee15..0000000000000
--- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Plugin/ImportTest.php
+++ /dev/null
@@ -1,37 +0,0 @@
-getMockBuilder(Processor::class)
- ->disableOriginalConstructor()
- ->getMock();
- $processorMock->expects($this->once())
- ->method('markIndexerAsInvalid');
-
- $subjectMock = $this->getMockBuilder(Import::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $import = true;
-
- $model = new \Magento\CatalogImportExport\Model\Indexer\Product\Category\Plugin\Import($processorMock);
-
- $this->assertEquals(
- $import,
- $model->afterImportSource($subjectMock, $import)
- );
- }
-}
diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Source/CountryofmanufactureTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Source/CountryofmanufactureTest.php
index 799424f2557c4..0ec5a48e68aab 100644
--- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Source/CountryofmanufactureTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/Source/CountryofmanufactureTest.php
@@ -9,6 +9,7 @@
use Magento\Catalog\Model\Product\Attribute\Source\Countryofmanufacture;
use Magento\Framework\App\Cache\Type\Config;
+use Magento\Framework\Locale\ResolverInterface;
use Magento\Framework\Serialize\SerializerInterface;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
use Magento\Store\Model\Store;
@@ -46,17 +47,24 @@ class CountryofmanufactureTest extends TestCase
*/
private $serializerMock;
+ /**
+ * @var ResolverInterface
+ */
+ private $localeResolverMock;
+
protected function setUp(): void
{
$this->storeManagerMock = $this->getMockForAbstractClass(StoreManagerInterface::class);
$this->storeMock = $this->createMock(Store::class);
$this->cacheConfig = $this->createMock(Config::class);
+ $this->localeResolverMock = $this->getMockForAbstractClass(ResolverInterface::class);
$this->objectManagerHelper = new ObjectManager($this);
$this->countryOfManufacture = $this->objectManagerHelper->getObject(
Countryofmanufacture::class,
[
'storeManager' => $this->storeManagerMock,
'configCacheType' => $this->cacheConfig,
+ 'localeResolver' => $this->localeResolverMock,
]
);
@@ -80,9 +88,10 @@ public function testGetAllOptions($cachedDataSrl, $cachedDataUnsrl)
{
$this->storeMock->expects($this->once())->method('getCode')->willReturn('store_code');
$this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($this->storeMock);
+ $this->localeResolverMock->expects($this->once())->method('getLocale')->willReturn('en_US');
$this->cacheConfig->expects($this->once())
->method('load')
- ->with($this->equalTo('COUNTRYOFMANUFACTURE_SELECT_STORE_store_code'))
+ ->with($this->equalTo('COUNTRYOFMANUFACTURE_SELECT_STORE_store_code_LOCALE_en_US'))
->willReturn($cachedDataSrl);
$this->serializerMock->expects($this->once())
->method('unserialize')
diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php
index 305f4acd40d83..1144ce1bbe6c3 100644
--- a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/MinimalTierPriceCalculatorTest.php
@@ -7,6 +7,7 @@
namespace Magento\Catalog\Test\Unit\Pricing\Price;
+use Magento\Catalog\Pricing\Price\FinalPrice;
use Magento\Catalog\Pricing\Price\MinimalTierPriceCalculator;
use Magento\Catalog\Pricing\Price\TierPrice;
use Magento\Framework\Pricing\Adjustment\CalculatorInterface;
@@ -73,10 +74,10 @@ private function getValueTierPricesExistShouldReturnMinTierPrice()
$notMinPrice = 10;
$minAmount = $this->getMockForAbstractClass(AmountInterface::class);
- $minAmount->expects($this->once())->method('getValue')->willReturn($minPrice);
+ $minAmount->expects($this->atLeastOnce())->method('getValue')->willReturn($minPrice);
$notMinAmount = $this->getMockForAbstractClass(AmountInterface::class);
- $notMinAmount->expects($this->once())->method('getValue')->willReturn($notMinPrice);
+ $notMinAmount->expects($this->atLeastOnce())->method('getValue')->willReturn($notMinPrice);
$tierPriceList = [
[
@@ -89,10 +90,12 @@ private function getValueTierPricesExistShouldReturnMinTierPrice()
$this->price->expects($this->once())->method('getTierPriceList')->willReturn($tierPriceList);
- $this->priceInfo->expects($this->once())->method('getPrice')->with(TierPrice::PRICE_CODE)
- ->willReturn($this->price);
+ $this->priceInfo->expects($this->atLeastOnce())
+ ->method('getPrice')
+ ->withConsecutive([TierPrice::PRICE_CODE], [FinalPrice::PRICE_CODE])
+ ->willReturnOnConsecutiveCalls($this->price, $notMinAmount);
- $this->saleable->expects($this->once())->method('getPriceInfo')->willReturn($this->priceInfo);
+ $this->saleable->expects($this->atLeastOnce())->method('getPriceInfo')->willReturn($this->priceInfo);
return $minPrice;
}
@@ -107,12 +110,8 @@ public function testGetGetAmountMinTierPriceExistShouldReturnAmountObject()
$minPrice = $this->getValueTierPricesExistShouldReturnMinTierPrice();
$amount = $this->getMockForAbstractClass(AmountInterface::class);
+ $amount->method('getValue')->willReturn($minPrice);
- $this->calculator->expects($this->once())
- ->method('getAmount')
- ->with($minPrice, $this->saleable)
- ->willReturn($amount);
-
- $this->assertSame($amount, $this->object->getAmount($this->saleable));
+ $this->assertEquals($amount, $this->object->getAmount($this->saleable));
}
}
diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php
index f3831e50ef3d9..eadf47601585d 100644
--- a/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php
+++ b/app/code/Magento/Catalog/Test/Unit/Pricing/Render/FinalPriceBoxTest.php
@@ -15,8 +15,11 @@
use Magento\Catalog\Pricing\Render\FinalPriceBox;
use Magento\Framework\App\Cache\StateInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
+use Magento\Framework\App\DeploymentConfig;
use Magento\Framework\App\State;
+use Magento\Framework\Config\ConfigOptionsListConstants;
use Magento\Framework\Event\Test\Unit\ManagerStub;
+use Magento\Framework\ObjectManagerInterface;
use Magento\Framework\Pricing\Amount\AmountInterface;
use Magento\Framework\Pricing\Price\PriceInterface;
use Magento\Framework\Pricing\PriceInfoInterface;
@@ -96,11 +99,27 @@ class FinalPriceBoxTest extends TestCase
*/
private $minimalPriceCalculator;
+ /**
+ * @var DeploymentConfig|MockObject
+ */
+ private $deploymentConfig;
+
+ /**
+ * @var ObjectManagerInterface|MockObject
+ */
+ private $objectManagerMock;
+
/**
* @inheritDoc
+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
protected function setUp(): void
{
+ $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['get'])
+ ->getMockForAbstractClass();
+ \Magento\Framework\App\ObjectManager::setInstance($this->objectManagerMock);
$this->product = $this->getMockBuilder(Product::class)
->addMethods(['getCanShowPrice'])
->onlyMethods(['getPriceInfo', 'isSalable', 'getId'])
@@ -183,6 +202,11 @@ protected function setUp(): void
->disableOriginalConstructor()
->getMockForAbstractClass();
+ $this->deploymentConfig = $this->createPartialMock(
+ DeploymentConfig::class,
+ ['get']
+ );
+
$this->minimalPriceCalculator = $this->getMockForAbstractClass(MinimalPriceCalculatorInterface::class);
$this->object = $objectManager->getObject(
FinalPriceBox::class,
@@ -339,10 +363,10 @@ public function testRenderAmountMinimal(): void
$arguments = [
'zone' => 'test_zone',
'list_category_page' => true,
- 'display_label' => 'As low as',
+ 'display_label' => __('As low as'),
'price_id' => $priceId,
'include_container' => false,
- 'skip_adjustments' => true
+ 'skip_adjustments' => false
];
$amountRender = $this->createPartialMock(Amount::class, ['toHtml']);
@@ -455,6 +479,15 @@ public function testHidePrice(): void
*/
public function testGetCacheKey(): void
{
+ $this->objectManagerMock->expects($this->any())
+ ->method('get')
+ ->with(DeploymentConfig::class)
+ ->willReturn($this->deploymentConfig);
+
+ $this->deploymentConfig->expects($this->any())
+ ->method('get')
+ ->with(ConfigOptionsListConstants::CONFIG_PATH_CRYPT_KEY)
+ ->willReturn('448198e08af35844a42d3c93c1ef4e03');
$result = $this->object->getCacheKey();
$this->assertStringEndsWith('list-category-page', $result);
}
diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Price.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Price.php
index 337182abf084c..3f8e6f699d84c 100644
--- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Price.php
+++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Price.php
@@ -23,16 +23,16 @@
class Price implements ProductRenderCollectorInterface
{
/** FInal Price key */
- const KEY_FINAL_PRICE = "final_price";
+ public const KEY_FINAL_PRICE = "final_price";
/** Minimal Price key */
- const KEY_MINIMAL_PRICE = "minimal_price";
+ public const KEY_MINIMAL_PRICE = "minimal_price";
/** Regular Price key */
- const KEY_REGULAR_PRICE = "regular_price";
+ public const KEY_REGULAR_PRICE = "regular_price";
/** Max Price key */
- const KEY_MAX_PRICE = "max_price";
+ public const KEY_MAX_PRICE = "max_price";
/**
* @var PriceCurrencyInterface
diff --git a/app/code/Magento/Catalog/composer.json b/app/code/Magento/Catalog/composer.json
index 4421b2991266b..73f8d988bf270 100644
--- a/app/code/Magento/Catalog/composer.json
+++ b/app/code/Magento/Catalog/composer.json
@@ -31,7 +31,8 @@
"magento/module-ui": "*",
"magento/module-url-rewrite": "*",
"magento/module-widget": "*",
- "magento/module-wishlist": "*"
+ "magento/module-wishlist": "*",
+ "magento/module-aws-s3": "*"
},
"suggest": {
"magento/module-cookie": "*",
diff --git a/app/code/Magento/Catalog/etc/adminhtml/di.xml b/app/code/Magento/Catalog/etc/adminhtml/di.xml
index eeacd0f0970f0..9edd98e24468a 100644
--- a/app/code/Magento/Catalog/etc/adminhtml/di.xml
+++ b/app/code/Magento/Catalog/etc/adminhtml/di.xml
@@ -291,4 +291,9 @@
+
+
+ true
+
+
diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml
index e817bcbb42d25..0805d1e48b2d7 100644
--- a/app/code/Magento/Catalog/etc/di.xml
+++ b/app/code/Magento/Catalog/etc/di.xml
@@ -1181,7 +1181,7 @@
- - Magento\Catalog\Model\Indexer\Product\Price\ModeSwitcher
+ - Magento\Catalog\Model\Indexer\Product\Price\ModeSwitcher\Proxy
diff --git a/app/code/Magento/Catalog/i18n/en_US.csv b/app/code/Magento/Catalog/i18n/en_US.csv
index defbf31a6b8f8..81e059adb3bb0 100644
--- a/app/code/Magento/Catalog/i18n/en_US.csv
+++ b/app/code/Magento/Catalog/i18n/en_US.csv
@@ -819,4 +819,5 @@ Details,Details
"Failed to retrieve product links for ""%1""","Failed to retrieve product links for ""%1"""
"The linked product SKU is invalid. Verify the data and try again.","The linked product SKU is invalid. Verify the data and try again."
"The linked products data is invalid. Verify the data and try again.","The linked products data is invalid. Verify the data and try again."
+"The url has invalid characters. Please correct and try again.","The url has invalid characters. Please correct and try again."
diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/category-tree.js b/app/code/Magento/Catalog/view/adminhtml/web/js/category-tree.js
index d292bd126593c..b4d4ed12d20ba 100644
--- a/app/code/Magento/Catalog/view/adminhtml/web/js/category-tree.js
+++ b/app/code/Magento/Catalog/view/adminhtml/web/js/category-tree.js
@@ -5,10 +5,9 @@
define([
'jquery',
- 'mageUtils',
'jquery/ui',
'jquery/jstree/jquery.jstree'
-], function ($, utils) {
+], function ($) {
'use strict';
$.widget('mage.categoryTree', {
@@ -87,7 +86,7 @@ define([
// jscs:disable requireCamelCaseOrUpperCaseIdentifiers
result = {
id: node.id,
- text: utils.unescape(node.name) + ' (' + node.product_count + ')',
+ text: node.name + ' (' + node.product_count + ')',
li_attr: {
class: node.cls + (!!node.disabled ? ' disabled' : '') //eslint-disable-line no-extra-boolean-cast
},
diff --git a/app/code/Magento/Catalog/view/base/templates/product/price/final_price.phtml b/app/code/Magento/Catalog/view/base/templates/product/price/final_price.phtml
index e56804a06de22..18a2bab2a31bf 100644
--- a/app/code/Magento/Catalog/view/base/templates/product/price/final_price.phtml
+++ b/app/code/Magento/Catalog/view/base/templates/product/price/final_price.phtml
@@ -6,6 +6,7 @@
?>
getZone() == 'item_view') ? true : false;
'price_id' => $block->getPriceId('old-price-' . $idSuffix),
'price_type' => 'oldPrice',
'include_container' => true,
- 'skip_adjustments' => true
+ 'skip_adjustments' => false
]); ?>
diff --git a/app/code/Magento/CatalogCmsGraphQl/README.md b/app/code/Magento/CatalogCmsGraphQl/README.md
index f3b36e515ac6e..5e506e4cb6aed 100644
--- a/app/code/Magento/CatalogCmsGraphQl/README.md
+++ b/app/code/Magento/CatalogCmsGraphQl/README.md
@@ -1,3 +1,3 @@
# CatalogCmsGraphQl
-**CatalogCmsGraphQl** provides type and resolver information for GraphQL attributes that have dependencies on the Catalog and Cms modules.
\ No newline at end of file
+**CatalogCmsGraphQl** provides type and resolver information for GraphQL attributes that have dependencies on the Catalog and Cms modules.
diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php
index 3c6cc849081ee..fba1f7f8cbcc4 100644
--- a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php
+++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php
@@ -18,12 +18,13 @@
use Magento\Framework\GraphQl\Query\Resolver\ValueFactory;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Framework\Pricing\PriceCurrencyInterface;
/**
* Resolver for price_tiers
*/
-class PriceTiers implements ResolverInterface
+class PriceTiers implements ResolverInterface, ResetAfterRequestInterface
{
/**
* @var TiersFactory
@@ -185,7 +186,7 @@ private function formatTierPrices(float $productPrice, string $currencyCode, $ti
"discount" => $discount,
"quantity" => $tierPrice->getQty(),
"final_price" => [
- "value" => $tierPrice->getValue(),
+ "value" => $tierPrice->getValue() * $tierPrice->getQty(),
"currency" => $currencyCode
]
];
@@ -216,4 +217,15 @@ private function filterTierPrices(
$this->tierPricesQty[$qty] = $key;
}
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->tierPricesQty = [];
+ $this->formatAndFilterTierPrices = [];
+ $this->customerGroupId = null;
+ $this->tiers = null;
+ }
}
diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Product/Price/Tiers.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Product/Price/Tiers.php
index a1ad456dc5208..954f076295290 100644
--- a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Product/Price/Tiers.php
+++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/Product/Price/Tiers.php
@@ -13,11 +13,12 @@
use Magento\Customer\Model\GroupManagement;
use Magento\Catalog\Api\Data\ProductTierPriceInterface;
use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool as PriceProviderPool;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Get product tier price information
*/
-class Tiers
+class Tiers implements ResetAfterRequestInterface
{
/**
* @var CollectionFactory
@@ -173,4 +174,13 @@ private function setProducts(Collection $productCollection): void
$this->products[$missingProductId] = null;
}
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->products = [];
+ $this->filterProductIds = [];
+ }
}
diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php
index bd05e48e23384..3481796323a78 100644
--- a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php
+++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php
@@ -16,11 +16,12 @@
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Query\Resolver\ValueFactory;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* @inheritdoc
*/
-class TierPrices implements ResolverInterface
+class TierPrices implements ResolverInterface, ResetAfterRequestInterface
{
/**
* @var ValueFactory
@@ -94,4 +95,13 @@ function () use ($productId) {
}
);
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->customerGroupId = null;
+ $this->tiers = null;
+ }
}
diff --git a/app/code/Magento/CatalogCustomerGraphQl/README.md b/app/code/Magento/CatalogCustomerGraphQl/README.md
index 525a1a4f76433..eb1a190e87bc0 100644
--- a/app/code/Magento/CatalogCustomerGraphQl/README.md
+++ b/app/code/Magento/CatalogCustomerGraphQl/README.md
@@ -1,3 +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
+**CatalogCustomerGraphQl** provides type and resolver information for GraphQL attributes that have dependences on the Catalog and Customer modules.
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php
index 09342ceb2f602..9171214a1137a 100644
--- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php
@@ -63,6 +63,7 @@ public function getOptions(array $optionIds, ?int $storeId, array $attributeCode
'attribute_id' => 'a.attribute_id',
'attribute_code' => 'a.attribute_code',
'attribute_label' => 'a.frontend_label',
+ 'attribute_type' => 'a.frontend_input',
'position' => 'attribute_configuration.position'
]
)
@@ -137,6 +138,7 @@ private function formatResult(Select $select): array
'attribute_code' => $option['attribute_code'],
'attribute_label' => $option['attribute_store_label']
? $option['attribute_store_label'] : $option['attribute_label'],
+ 'attribute_type' => $option['attribute_type'],
'position' => $option['position'],
'options' => [],
];
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Aggregations/Category/IncludeDirectChildrenOnly.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Aggregations/Category/IncludeDirectChildrenOnly.php
index e22843573d9d6..5fc50452bb70c 100644
--- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Aggregations/Category/IncludeDirectChildrenOnly.php
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Aggregations/Category/IncludeDirectChildrenOnly.php
@@ -9,6 +9,7 @@
use Magento\Catalog\Api\CategoryListInterface;
use Magento\Framework\Api\SearchCriteriaBuilder;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Framework\Search\Response\Aggregation;
use Magento\Framework\Search\Response\AggregationFactory;
use Magento\Framework\Search\Response\BucketFactory;
@@ -18,7 +19,7 @@
/**
* Class to include only direct subcategories of category in aggregation
*/
-class IncludeDirectChildrenOnly
+class IncludeDirectChildrenOnly implements ResetAfterRequestInterface
{
/**
* @var string
@@ -160,4 +161,12 @@ private function filterBucketValues(
}
return array_values($categoryBucketValues);
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->filter = [];
+ }
}
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php
index afef26aad6046..d1e66613c35f2 100644
--- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php
@@ -13,6 +13,7 @@
use Magento\Framework\Api\Search\AggregationValueInterface;
use Magento\Framework\Api\Search\BucketInterface;
use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Formatter\LayerFormatter;
+use Magento\Config\Model\Config\Source\Yesno;
/**
* @inheritdoc
@@ -49,18 +50,26 @@ class Attribute implements LayerBuilderInterface
self::CATEGORY_BUCKET
];
+ /**
+ * @var Yesno
+ */
+ private Yesno $YesNo;
+
/**
* @param AttributeOptionProvider $attributeOptionProvider
* @param LayerFormatter $layerFormatter
+ * @param Yesno $YesNo
* @param array $bucketNameFilter
*/
public function __construct(
AttributeOptionProvider $attributeOptionProvider,
LayerFormatter $layerFormatter,
+ Yesno $YesNo,
$bucketNameFilter = []
) {
$this->attributeOptionProvider = $attributeOptionProvider;
$this->layerFormatter = $layerFormatter;
+ $this->YesNo = $YesNo;
$this->bucketNameFilter = \array_merge($this->bucketNameFilter, $bucketNameFilter);
}
@@ -87,7 +96,11 @@ public function build(AggregationInterface $aggregation, ?int $storeId): array
isset($attribute['position']) ? $attribute['position'] : null
);
- $options = $this->getSortedOptions($bucket, isset($attribute['options']) ? $attribute['options'] : []);
+ $options = $this->getSortedOptions(
+ $bucket,
+ isset($attribute['options']) ? $attribute['options'] : [],
+ ($attribute['attribute_type']) ? $attribute['attribute_type']: ''
+ );
foreach ($options as $option) {
$result[$bucketName]['options'][] = $this->layerFormatter->buildItem(
$option['label'],
@@ -168,9 +181,11 @@ function (AggregationValueInterface $value) {
*
* @param BucketInterface $bucket
* @param array $optionLabels
+ * @param string $attributeType
* @return array
+ * @SuppressWarnings(PHPMD.UnusedLocalVariable)
*/
- private function getSortedOptions(BucketInterface $bucket, array $optionLabels): array
+ private function getSortedOptions(BucketInterface $bucket, array $optionLabels, string $attributeType): array
{
/**
* Option labels array has been sorted
@@ -179,7 +194,16 @@ private function getSortedOptions(BucketInterface $bucket, array $optionLabels):
foreach ($bucket->getValues() as $value) {
$metrics = $value->getMetrics();
$optionValue = $metrics['value'];
- $optionLabel = $optionLabels[$optionValue] ?? $optionValue;
+ if (isset($optionLabels[$optionValue])) {
+ $optionLabel = $optionLabels[$optionValue];
+ } else {
+ if ($attributeType === 'boolean') {
+ $yesNoOptions = $this->YesNo->toArray();
+ $optionLabel = $yesNoOptions[$optionValue];
+ } else {
+ $optionLabel = $optionValue;
+ }
+ }
$options[$optionValue] = $metrics + ['label' => $optionLabel];
}
@@ -188,7 +212,7 @@ private function getSortedOptions(BucketInterface $bucket, array $optionLabels):
*/
foreach ($options as $optionId => $option) {
if (!is_array($options[$optionId])) {
- unset($options[$optionId]);
+ unset($options[$optionId]);
}
}
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php
index b8689cc8868d7..c65c0872d0873 100644
--- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php
@@ -18,13 +18,14 @@
use Magento\Framework\Api\Search\BucketInterface;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\GraphQl\Query\Uid;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Category layer builder
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
-class Category implements LayerBuilderInterface
+class Category implements LayerBuilderInterface, ResetAfterRequestInterface
{
/**
* @var string
@@ -201,4 +202,17 @@ private function getStoreCategoryIds(int $storeId): array
);
return $collection->getAllIds();
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ self::$bucketMap = [
+ self::CATEGORY_BUCKET => [
+ 'request_name' => 'category_uid',
+ 'label' => 'Category'
+ ],
+ ];
+ }
}
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php
index 6df29fa256923..d3b4e31366525 100644
--- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Formatter/LayerFormatter.php
@@ -24,7 +24,7 @@ class LayerFormatter
public function buildLayer($layerName, $itemsCount, $requestName, $position = null): array
{
return [
- 'label' => $layerName,
+ 'label' => __($layerName),
'count' => $itemsCount,
'attribute_code' => $requestName,
'position' => isset($position) ? (int)$position : null
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php
index d67a50875b81d..70520c31a7724 100644
--- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php
@@ -98,7 +98,7 @@ public function __construct(
public function build(array $args, bool $includeAggregation): SearchCriteriaInterface
{
$searchCriteria = $this->builder->build('products', $args);
- $isSearch = !empty($args['search']);
+ $isSearch = isset($args['search']);
$this->updateRangeFilters($searchCriteria);
if ($includeAggregation) {
$attributeData = $this->eavConfig->getAttribute(Product::ENTITY, 'price');
@@ -122,7 +122,7 @@ public function build(array $args, bool $includeAggregation): SearchCriteriaInte
}
$this->addEntityIdSort($searchCriteria);
- $this->addVisibilityFilter($searchCriteria, $isSearch, !empty($args['filter']));
+ $this->addVisibilityFilter($searchCriteria, $isSearch, !empty($args['filter']['category_id']));
$searchCriteria->setCurrentPage($args['currentPage']);
$searchCriteria->setPageSize($args['pageSize']);
diff --git a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php
index 34f5dd831686c..63a998022df80 100644
--- a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php
+++ b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php
@@ -10,13 +10,15 @@
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\NodeKind;
+use GraphQL\Language\AST\NodeList;
use Magento\Eav\Model\Entity\Collection\AbstractCollection;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Joins attributes for provided field node field names.
*/
-class AttributesJoiner
+class AttributesJoiner implements ResetAfterRequestInterface
{
/**
* @var array
@@ -61,39 +63,56 @@ public function join(FieldNode $fieldNode, AbstractCollection $collection, Resol
*
* @param FieldNode $fieldNode
* @param ResolveInfo $resolveInfo
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @return string[]
*/
public function getQueryFields(FieldNode $fieldNode, ResolveInfo $resolveInfo): array
{
if (null === $this->getFieldNodeSelections($fieldNode)) {
$query = $fieldNode->selectionSet->selections;
- $selectedFields = [];
- $fragmentFields = [];
/** @var FieldNode $field */
- foreach ($query as $field) {
- if ($field->kind === NodeKind::INLINE_FRAGMENT) {
- $fragmentFields[] = $this->addInlineFragmentFields($resolveInfo, $field);
- } elseif ($field->kind === NodeKind::FRAGMENT_SPREAD &&
- ($spreadFragmentNode = $resolveInfo->fragments[$field->name->value])) {
-
- foreach ($spreadFragmentNode->selectionSet->selections as $spreadNode) {
- if (isset($spreadNode->selectionSet->selections)) {
- $fragmentFields[] = $this->getQueryFields($spreadNode, $resolveInfo);
- } else {
+ $result = $this->getQueryData($query, $resolveInfo);
+ if ($result['fragmentFields']) {
+ $result['selectedFields'] = array_merge([], $result['selectedFields'], ...$result['fragmentFields']);
+ }
+ $this->setSelectionsForFieldNode($fieldNode, array_unique($result['selectedFields']));
+ }
+ return $this->getFieldNodeSelections($fieldNode);
+ }
+
+ /**
+ * Get an array of queried data.
+ *
+ * @param NodeList $query
+ * @param ResolveInfo $resolveInfo
+ * @return array
+ */
+ public function getQueryData(NodeList $query, ResolveInfo $resolveInfo): array
+ {
+ $selectedFields = $fragmentFields = $data = [];
+ foreach ($query as $field) {
+ if ($field->kind === NodeKind::INLINE_FRAGMENT) {
+ $fragmentFields[] = $this->addInlineFragmentFields($resolveInfo, $field);
+ } elseif ($field->kind === NodeKind::FRAGMENT_SPREAD &&
+ ($spreadFragmentNode = $resolveInfo->fragments[$field->name->value])) {
+ foreach ($spreadFragmentNode->selectionSet->selections as $spreadNode) {
+ if (isset($spreadNode->selectionSet->selections)) {
+ if ($spreadNode->kind === NodeKind::FIELD && isset($spreadNode->name)) {
$selectedFields[] = $spreadNode->name->value;
}
+ $fragmentFields[] = $this->getQueryFields($spreadNode, $resolveInfo);
+ } else {
+ $selectedFields[] = $spreadNode->name->value;
}
- } else {
- $selectedFields[] = $field->name->value;
}
+ } else {
+ $selectedFields[] = $field->name->value;
}
- if ($fragmentFields) {
- $selectedFields = array_merge([], $selectedFields, ...$fragmentFields);
- }
- $this->setSelectionsForFieldNode($fieldNode, array_unique($selectedFields));
}
+ $data['selectedFields'] = $selectedFields;
+ $data['fragmentFields'] = $fragmentFields;
- return $this->getFieldNodeSelections($fieldNode);
+ return $data;
}
/**
@@ -111,15 +130,22 @@ private function addInlineFragmentFields(
): array {
$query = $inlineFragmentField->selectionSet->selections;
/** @var FieldNode $field */
+ $fragmentFields = [];
foreach ($query as $field) {
if ($field->kind === NodeKind::INLINE_FRAGMENT) {
$this->addInlineFragmentFields($resolveInfo, $field, $inlineFragmentFields);
} elseif (isset($field->selectionSet->selections)) {
- continue;
+ if ($field->kind === NodeKind::FIELD && isset($field->name)) {
+ $inlineFragmentFields[] = $field->name->value;
+ }
+ $fragmentFields[] = $this->getQueryFields($field, $resolveInfo);
} else {
$inlineFragmentFields[] = $field->name->value;
}
}
+ if ($fragmentFields) {
+ $inlineFragmentFields = array_merge([], $inlineFragmentFields, ...$fragmentFields);
+ }
return array_unique($inlineFragmentFields);
}
@@ -172,4 +198,12 @@ private function setSelectionsForFieldNode(FieldNode $fieldNode, array $selected
{
$this->queryFields[$fieldNode->name->value][$fieldNode->name->loc->start] = $selectedFields;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->queryFields = [];
+ }
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php
index 356ff17183a57..2bd1714537a0a 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php
@@ -11,6 +11,7 @@
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\NodeKind;
+use GraphQL\Language\AST\SelectionNode;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
/**
@@ -66,13 +67,13 @@ private function calculateRecursive(ResolveInfo $resolveInfo, Node $node) : int
* Add inline fragment fields into calculating of category depth
*
* @param ResolveInfo $resolveInfo
- * @param InlineFragmentNode $inlineFragmentField
+ * @param SelectionNode $inlineFragmentField
* @param array $depth
* @return int
*/
private function addInlineFragmentDepth(
ResolveInfo $resolveInfo,
- InlineFragmentNode $inlineFragmentField,
+ SelectionNode $inlineFragmentField,
$depth = []
): int {
$selections = $inlineFragmentField->selectionSet->selections;
@@ -80,7 +81,7 @@ private function addInlineFragmentDepth(
foreach ($selections as $field) {
if ($field->kind === NodeKind::INLINE_FRAGMENT) {
$depth[] = $this->addInlineFragmentDepth($resolveInfo, $field, $depth);
- } elseif ($field->selectionSet && $field->selectionSet->selections) {
+ } elseif (!empty($field->selectionSet) && $field->selectionSet->selections) {
$depth[] = $this->calculate($resolveInfo, $field);
}
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php b/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php
index 215b28be0579c..2f16e9ccb318f 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Config/SortAttributeReader.php
@@ -6,9 +6,11 @@
namespace Magento\CatalogGraphQl\Model\Config;
+use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection as AttributesCollection;
+use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory as AttributesCollectionFactory;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Config\ReaderInterface;
use Magento\Framework\GraphQl\Schema\Type\Entity\MapperInterface;
-use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection as AttributesCollection;
/**
* Adds custom/eav attribute to catalog products sorting in the GraphQL config.
@@ -31,20 +33,24 @@ class SortAttributeReader implements ReaderInterface
private $mapper;
/**
- * @var AttributesCollection
+ * @var AttributesCollectionFactory
*/
- private $attributesCollection;
+ private $attributesCollectionFactory;
/**
* @param MapperInterface $mapper
- * @param AttributesCollection $attributesCollection
+ * @param AttributesCollection $attributesCollection @deprecated @see $attributesCollectionFactory
+ * @param AttributesCollectionFactory|null $attributesCollectionFactory
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __construct(
MapperInterface $mapper,
- AttributesCollection $attributesCollection
+ AttributesCollection $attributesCollection,
+ ?AttributesCollectionFactory $attributesCollectionFactory = null
) {
$this->mapper = $mapper;
- $this->attributesCollection = $attributesCollection;
+ $this->attributesCollectionFactory = $attributesCollectionFactory
+ ?? ObjectManager::getInstance()->get(AttributesCollectionFactory::class);
}
/**
@@ -58,7 +64,8 @@ public function read($scope = null) : array
{
$map = $this->mapper->getMappedTypes(self::ENTITY_TYPE);
$config =[];
- $attributes = $this->attributesCollection->addSearchableAttributeFilter()->addFilter('used_for_sort_by', 1);
+ $attributes = $this->attributesCollectionFactory->create()
+ ->addSearchableAttributeFilter()->addFilter('used_for_sort_by', 1);
/** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */
foreach ($attributes as $attribute) {
$attributeCode = $attribute->getAttributeCode();
@@ -73,7 +80,6 @@ public function read($scope = null) : array
];
}
}
-
return $config;
}
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Output/AttributeMetadata.php b/app/code/Magento/CatalogGraphQl/Model/Output/AttributeMetadata.php
new file mode 100644
index 0000000000000..7930c597adeae
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Output/AttributeMetadata.php
@@ -0,0 +1,83 @@
+entityType = $entityType;
+ }
+
+ /**
+ * Retrieve formatted attribute data
+ *
+ * @param Attribute $attribute
+ * @param string $entityType
+ * @param int $storeId
+ * @return array
+ * @throws LocalizedException
+ * @throws NoSuchEntityException
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function execute(
+ AttributeInterface $attribute,
+ string $entityType,
+ int $storeId
+ ): array {
+ if ($entityType !== $this->entityType) {
+ return [];
+ }
+
+ $metadata = [
+ 'is_comparable' => $attribute->getIsComparable() === "1",
+ 'is_filterable' => $attribute->getIsFilterable() === "1",
+ 'is_filterable_in_search' => $attribute->getIsFilterableInSearch() === "1",
+ 'is_searchable' => $attribute->getIsSearchable() === "1",
+ 'is_html_allowed_on_front' => $attribute->getIsHtmlAllowedOnFront() === "1",
+ 'is_used_for_price_rules' => $attribute->getIsUsedForPriceRules() === "1",
+ 'is_used_for_promo_rules' => $attribute->getIsUsedForPromoRules() === "1",
+ 'is_visible_in_advanced_search' => $attribute->getIsVisibleInAdvancedSearch() === "1",
+ 'is_visible_on_front' => $attribute->getIsVisibleOnFront() === "1",
+ 'is_wysiwyg_enabled' => $attribute->getIsWysiwygEnabled() === "1",
+ 'used_in_product_listing' => $attribute->getUsedInProductListing() === "1",
+ 'apply_to' => null
+ ];
+
+ if (!empty($attribute->getApplyTo())) {
+ $metadata['apply_to'] = array_map('strtoupper', $attribute->getApplyTo());
+ }
+
+ if (!empty($attribute->getAdditionalData())) {
+ $additionalData = json_decode($attribute->getAdditionalData(), true);
+ $metadata = array_merge(
+ $metadata,
+ array_map('strtoupper', $additionalData)
+ );
+ }
+
+ return $metadata;
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php
index d7118d71db89b..68b051f39c3a2 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php
@@ -8,7 +8,6 @@
namespace Magento\CatalogGraphQl\Model\Resolver;
use Magento\Catalog\Api\Data\CategoryInterface;
-use Magento\Catalog\Model\ResourceModel\Category\Collection;
use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory;
use Magento\CatalogGraphQl\Model\AttributesJoiner;
use Magento\CatalogGraphQl\Model\Category\Hydrator as CategoryHydrator;
@@ -27,46 +26,39 @@
class Categories implements ResolverInterface
{
/**
- * @var Collection
+ * @var CollectionFactory
*/
- private $collection;
-
- /**
- * Accumulated category ids
- *
- * @var array
- */
- private $categoryIds = [];
+ private CollectionFactory $collectionFactory;
/**
* @var AttributesJoiner
*/
- private $attributesJoiner;
+ private AttributesJoiner $attributesJoiner;
/**
* @var CustomAttributesFlattener
*/
- private $customAttributesFlattener;
+ private CustomAttributesFlattener $customAttributesFlattener;
/**
* @var ValueFactory
*/
- private $valueFactory;
+ private ValueFactory $valueFactory;
/**
* @var CategoryHydrator
*/
- private $categoryHydrator;
+ private CategoryHydrator $categoryHydrator;
/**
* @var ProductCategories
*/
- private $productCategories;
+ private ProductCategories $productCategories;
/**
* @var StoreManagerInterface
*/
- private $storeManager;
+ private StoreManagerInterface $storeManager;
/**
* @param CollectionFactory $collectionFactory
@@ -86,7 +78,7 @@ public function __construct(
ProductCategories $productCategories,
StoreManagerInterface $storeManager
) {
- $this->collection = $collectionFactory->create();
+ $this->collectionFactory = $collectionFactory;
$this->attributesJoiner = $attributesJoiner;
$this->customAttributesFlattener = $customAttributesFlattener;
$this->valueFactory = $valueFactory;
@@ -105,43 +97,37 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
if (!isset($value['model'])) {
throw new LocalizedException(__('"model" value should be specified'));
}
-
/** @var \Magento\Catalog\Model\Product $product */
$product = $value['model'];
$storeId = $this->storeManager->getStore()->getId();
$categoryIds = $this->productCategories->getCategoryIdsByProduct((int)$product->getId(), (int)$storeId);
- $this->categoryIds = array_merge($this->categoryIds, $categoryIds);
- $that = $this;
-
+ $collection = $this->collectionFactory->create();
return $this->valueFactory->create(
- function () use ($that, $categoryIds, $info) {
+ function () use ($categoryIds, $info, $collection) {
$categories = [];
- if (empty($that->categoryIds)) {
+ if (empty($categoryIds)) {
return [];
}
-
- if (!$this->collection->isLoaded()) {
- $that->attributesJoiner->join($info->fieldNodes[0], $this->collection, $info);
- $this->collection->addIdFilter($this->categoryIds);
+ if (!$collection->isLoaded()) {
+ $this->attributesJoiner->join($info->fieldNodes[0], $collection, $info);
+ $collection->addIdFilter($categoryIds);
}
/** @var CategoryInterface | \Magento\Catalog\Model\Category $item */
- foreach ($this->collection as $item) {
+ foreach ($collection as $item) {
if (in_array($item->getId(), $categoryIds)) {
// Try to extract all requested fields from the loaded collection data
$categories[$item->getId()] = $this->categoryHydrator->hydrateCategory($item, true);
$categories[$item->getId()]['model'] = $item;
- $requestedFields = $that->attributesJoiner->getQueryFields($info->fieldNodes[0], $info);
+ $requestedFields = $this->attributesJoiner->getQueryFields($info->fieldNodes[0], $info);
$extractedFields = array_keys($categories[$item->getId()]);
$foundFields = array_intersect($requestedFields, $extractedFields);
if (count($requestedFields) === count($foundFields)) {
continue;
}
-
// If not all requested fields were extracted from the collection, start more complex extraction
$categories[$item->getId()] = $this->categoryHydrator->hydrateCategory($item);
}
}
-
return $categories;
}
);
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php
index df725c02eb5bd..1a52916a85c01 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product.php
@@ -9,12 +9,14 @@
use Magento\CatalogGraphQl\Model\Resolver\Product\ProductFieldsSelector;
use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\Product as ProductDataProvider;
+use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Deferred\ProductFactory as ProductDataProviderFactory;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\Framework\GraphQl\Query\Resolver\ValueFactory;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
-use Magento\Framework\Exception\LocalizedException;
/**
* @inheritdoc
@@ -22,31 +24,35 @@
class Product implements ResolverInterface
{
/**
- * @var ProductDataProvider
+ * @var ProductDataProviderFactory
*/
- private $productDataProvider;
+ private ProductDataProviderFactory $productDataProviderFactory;
/**
* @var ValueFactory
*/
- private $valueFactory;
+ private ValueFactory $valueFactory;
/**
* @var ProductFieldsSelector
*/
- private $productFieldsSelector;
+ private ProductFieldsSelector $productFieldsSelector;
/**
- * @param ProductDataProvider $productDataProvider
+ * @param ProductDataProvider $productDataProvider Deprecated. Use $productDataProviderFactory
* @param ValueFactory $valueFactory
* @param ProductFieldsSelector $productFieldsSelector
+ * @param ProductDataProviderFactory|null $productDataProviderFactory
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __construct(
ProductDataProvider $productDataProvider,
ValueFactory $valueFactory,
- ProductFieldsSelector $productFieldsSelector
+ ProductFieldsSelector $productFieldsSelector,
+ ProductDataProviderFactory $productDataProviderFactory = null
) {
- $this->productDataProvider = $productDataProvider;
+ $this->productDataProviderFactory = $productDataProviderFactory
+ ?: ObjectManager::getInstance()->get(ProductDataProviderFactory::class);
$this->valueFactory = $valueFactory;
$this->productFieldsSelector = $productFieldsSelector;
}
@@ -59,12 +65,12 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
if (!isset($value['sku'])) {
throw new GraphQlInputException(__('No child sku found for product link.'));
}
- $this->productDataProvider->addProductSku($value['sku']);
+ $productDataProvider = $this->productDataProviderFactory->create();
+ $productDataProvider->addProductSku($value['sku']);
$fields = $this->productFieldsSelector->getProductFieldsFromInfo($info);
- $this->productDataProvider->addEavAttributes($fields);
-
- $result = function () use ($value, $context) {
- $data = $value['product'] ?? $this->productDataProvider->getProductBySku($value['sku'], $context);
+ $productDataProvider->addEavAttributes($fields);
+ $result = function () use ($value, $context, $productDataProvider) {
+ $data = $value['product'] ?? $productDataProvider->getProductBySku($value['sku'], $context);
if (empty($data)) {
return null;
}
@@ -75,7 +81,6 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
/** @var \Magento\Catalog\Model\Product $productModel */
$data = $productModel->getData();
$data['model'] = $productModel;
-
if (!empty($productModel->getCustomAttributes())) {
foreach ($productModel->getCustomAttributes() as $customAttribute) {
if (!isset($data[$customAttribute->getAttributeCode()])) {
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php
index 359d295095667..79dd1450125ed 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGallery/Url.php
@@ -14,11 +14,12 @@
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Returns media url
*/
-class Url implements ResolverInterface
+class Url implements ResolverInterface, ResetAfterRequestInterface
{
/**
* @var ImageFactory
@@ -100,4 +101,12 @@ private function getImageUrl(string $imageType, ?string $imagePath): string
return $image->getUrl();
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->placeholderCache = [];
+ }
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php
index 25db5207af285..938f6c359b060 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php
@@ -8,8 +8,6 @@
namespace Magento\CatalogGraphQl\Model\Resolver\Product;
use Magento\CatalogGraphQl\Model\PriceRangeDataProvider;
-use Magento\CatalogGraphQl\Model\Resolver\Product\Price\Discount;
-use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool as PriceProviderPool;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Config\Element\Field;
@@ -20,33 +18,17 @@
*/
class PriceRange implements ResolverInterface
{
- /**
- * @var Discount
- */
- private Discount $discount;
-
- /**
- * @var PriceProviderPool
- */
- private PriceProviderPool $priceProviderPool;
-
/**
* @var PriceRangeDataProvider
*/
private PriceRangeDataProvider $priceRangeDataProvider;
/**
- * @param PriceProviderPool $priceProviderPool
- * @param Discount $discount
* @param PriceRangeDataProvider|null $priceRangeDataProvider
*/
public function __construct(
- PriceProviderPool $priceProviderPool,
- Discount $discount,
PriceRangeDataProvider $priceRangeDataProvider = null
) {
- $this->priceProviderPool = $priceProviderPool;
- $this->discount = $discount;
$this->priceRangeDataProvider = $priceRangeDataProvider
?? ObjectManager::getInstance()->get(PriceRangeDataProvider::class);
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductCustomAttributes.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductCustomAttributes.php
new file mode 100644
index 0000000000000..367724891026a
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductCustomAttributes.php
@@ -0,0 +1,133 @@
+getAttributeValue = $getAttributeValue;
+ $this->productDataProvider = $productDataProvider;
+ $this->getFilteredAttributes = $getFilteredAttributes;
+ $this->filterCustomAttribute = $filterCustomAttribute;
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @param Field $field
+ * @param ContextInterface $context
+ * @param ResolveInfo $info
+ * @param array|null $value
+ * @param array|null $args
+ * @throws \Exception
+ * @return array
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ $filtersArgs = $args['filters'] ?? [];
+
+ $productCustomAttributes = $this->getFilteredAttributes->execute(
+ $filtersArgs,
+ ProductAttributeInterface::ENTITY_TYPE_CODE
+ );
+
+ $attributeCodes = array_map(
+ function (AttributeInterface $customAttribute) {
+ return $customAttribute->getAttributeCode();
+ },
+ $productCustomAttributes['items']
+ );
+
+ $filteredAttributeCodes = $this->filterCustomAttribute->execute(array_flip($attributeCodes));
+
+ /** @var Product $product */
+ $product = $value['model'];
+ $productData = $this->productDataProvider->getProductDataById((int)$product->getId());
+
+ $customAttributes = [];
+ foreach ($filteredAttributeCodes as $attributeCode => $value) {
+ if (!array_key_exists($attributeCode, $productData)) {
+ continue;
+ }
+ $attributeValue = $productData[$attributeCode];
+ if (is_array($attributeValue)) {
+ $attributeValue = implode(',', $attributeValue);
+ }
+ $customAttributes[] = [
+ 'attribute_code' => $attributeCode,
+ 'value' => $attributeValue
+ ];
+ }
+
+ return [
+ 'items' => array_map(
+ function (array $customAttribute) {
+ return $this->getAttributeValue->execute(
+ ProductAttributeInterface::ENTITY_TYPE_CODE,
+ $customAttribute['attribute_code'],
+ $customAttribute['value']
+ );
+ },
+ $customAttributes
+ ),
+ 'errors' => $productCustomAttributes['errors']
+ ];
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php
index 3139c35774008..ab9fed035cc35 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php
@@ -7,7 +7,7 @@
namespace Magento\CatalogGraphQl\Model\Resolver\Product;
-use GraphQL\Language\AST\NodeKind;
+use Magento\CatalogGraphQl\Model\AttributesJoiner;
use Magento\Framework\GraphQl\Query\FieldTranslator;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
@@ -19,14 +19,23 @@ class ProductFieldsSelector
/**
* @var FieldTranslator
*/
- private $fieldTranslator;
+ private FieldTranslator $fieldTranslator;
+
+ /**
+ * @var AttributesJoiner
+ */
+ private AttributesJoiner $attributesJoiner;
/**
* @param FieldTranslator $fieldTranslator
+ * @param AttributesJoiner $attributesJoiner
*/
- public function __construct(FieldTranslator $fieldTranslator)
- {
+ public function __construct(
+ FieldTranslator $fieldTranslator,
+ AttributesJoiner $attributesJoiner
+ ) {
$this->fieldTranslator = $fieldTranslator;
+ $this->attributesJoiner = $attributesJoiner;
}
/**
@@ -36,27 +45,17 @@ public function __construct(FieldTranslator $fieldTranslator)
* @param string $productNodeName
* @return string[]
*/
- public function getProductFieldsFromInfo(ResolveInfo $info, string $productNodeName = 'product') : array
+ public function getProductFieldsFromInfo(ResolveInfo $info, string $productNodeName = 'product'): array
{
$fieldNames = [];
foreach ($info->fieldNodes as $node) {
if ($node->name->value !== $productNodeName) {
continue;
}
- foreach ($node->selectionSet->selections as $selectionNode) {
- if ($selectionNode->kind === NodeKind::INLINE_FRAGMENT) {
- foreach ($selectionNode->selectionSet->selections as $inlineSelection) {
- if ($inlineSelection->kind === NodeKind::INLINE_FRAGMENT) {
- continue;
- }
- $fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value);
- }
- continue;
- }
- $fieldNames[] = $this->fieldTranslator->translate($selectionNode->name->value);
- }
+ $queryFields = $this->attributesJoiner->getQueryFields($node, $info);
+ $fieldNames[] = $queryFields;
}
- return $fieldNames;
+ return array_merge(...$fieldNames);
}
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Websites/Collection.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Websites/Collection.php
index 3091cffb619c2..d5afac28354bf 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Websites/Collection.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Websites/Collection.php
@@ -9,13 +9,14 @@
use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Store\Model\ResourceModel\Website\Collection as WebsiteCollection;
use Magento\Store\Model\ResourceModel\Website\CollectionFactory as WebsiteCollectionFactory;
/**
* Collection to fetch websites data at resolution time.
*/
-class Collection
+class Collection implements ResetAfterRequestInterface
{
/**
* @var WebsiteCollection
@@ -133,4 +134,13 @@ private function fetch() : array
}
return $this->websites;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->productIds = [];
+ $this->websites = [];
+ }
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Attributes/Collection.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Attributes/Collection.php
index ab0531ad09513..63cd205fd87f9 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Attributes/Collection.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Attributes/Collection.php
@@ -11,11 +11,12 @@
use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory as AttributeCollectionFactory;
use Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection as AttributeCollection;
use Magento\Eav\Model\Attribute;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Gather all eav and custom attributes to use in a GraphQL schema for products
*/
-class Collection
+class Collection implements ResetAfterRequestInterface
{
/**
* @var AttributeCollectionFactory
@@ -95,4 +96,12 @@ public function getRequestAttributes(array $fieldNames) : array
return $matchedAttributes;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->collection = null;
+ }
}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php
index a528efcb4a81a..d3e30dd48f280 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php
@@ -10,12 +10,13 @@
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product as ProductDataProvider;
use Magento\Framework\Api\SearchCriteriaBuilder;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\GraphQl\Model\Query\ContextInterface;
/**
* Deferred resolver for product data.
*/
-class Product
+class Product implements ResetAfterRequestInterface
{
/**
* @var ProductDataProvider
@@ -144,4 +145,14 @@ private function fetch(ContextInterface $context = null): void
$this->productList[$product->getSku()] = ['model' => $product];
}
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->productList = [];
+ $this->productSkus = [];
+ $this->attributeCodes = [];
+ }
}
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 30be41072242b..3e955ae303453 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php
@@ -89,7 +89,7 @@ public function getList(
$this->collectionPreProcessor->process($collection, $searchCriteria, $attributes, $context);
- if ($isChildSearch) {
+ if (!$isChildSearch) {
$visibilityIds = $isSearch
? $this->visibility->getVisibleInSearchIds()
: $this->visibility->getVisibleInCatalogIds();
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/CategoryUrlPathArgsProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/CategoryUrlPathArgsProcessor.php
new file mode 100644
index 0000000000000..9941c5a9cbb4a
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/CategoryUrlPathArgsProcessor.php
@@ -0,0 +1,90 @@
+collectionFactory = $collectionFactory;
+ }
+
+ /**
+ * Composite processor that loops through available processors for arguments that come from graphql input
+ *
+ * @param string $fieldName
+ * @param array $args
+ * @return array
+ * @throws GraphQlInputException
+ */
+ public function process(
+ string $fieldName,
+ array $args
+ ): array {
+ $idFilter = $args['filter'][self::ID] ?? [];
+ $uidFilter = $args['filter'][self::UID] ?? [];
+ $pathFilter = $args['filter'][self::URL_PATH] ?? [];
+
+ if (!empty($pathFilter) && $fieldName === 'products') {
+ if (!empty($idFilter)) {
+ throw new GraphQlInputException(
+ __('`%1` and `%2` can\'t be used at the same time.', [self::ID, self::URL_PATH])
+ );
+ } elseif (!empty($uidFilter)) {
+ throw new GraphQlInputException(
+ __('`%1` and `%2` can\'t be used at the same time.', [self::UID, self::URL_PATH])
+ );
+ }
+
+ /** @var Collection $collection */
+ $collection = $this->collectionFactory->create();
+ $collection->addAttributeToSelect('entity_id');
+ $collection->addAttributeToFilter('url_path', $pathFilter);
+
+ if ($collection->count() === 0) {
+ throw new GraphQlInputException(
+ __('No category with the provided `%1` was found', [self::URL_PATH])
+ );
+ } elseif ($collection->count() === 1) {
+ $category = $collection->getFirstItem();
+ $args['filter'][self::ID]['eq'] = $category->getId();
+ } else {
+ $categoryIds = [];
+ foreach ($collection as $category) {
+ $categoryIds[] = $category->getId();
+ }
+ $args['filter'][self::ID]['in'] = $categoryIds;
+ }
+
+ unset($args['filter'][self::URL_PATH]);
+ }
+ return $args;
+ }
+}
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 2c612da4f433a..c4d189cd7cb0c 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php
@@ -186,7 +186,8 @@ private function buildSearchCriteria(array $args, ResolveInfo $info): SearchCrit
{
$productFields = (array)$info->getFieldSelection(1);
$includeAggregations = isset($productFields['filters']) || isset($productFields['aggregations']);
- $processedArgs = $this->argsSelection->process((string) $info->fieldName, $args);
+ $fieldName = $info->fieldName ?? "";
+ $processedArgs = $this->argsSelection->process((string) $fieldName, $args);
$searchCriteria = $this->searchCriteriaBuilder->build($processedArgs, $includeAggregations);
return $searchCriteria;
diff --git a/app/code/Magento/CatalogGraphQl/Test/Unit/DataProvider/Product/SearchCriteriaBuilderTest.php b/app/code/Magento/CatalogGraphQl/Test/Unit/DataProvider/Product/SearchCriteriaBuilderTest.php
index 59970335d3d10..b406c053cd207 100644
--- a/app/code/Magento/CatalogGraphQl/Test/Unit/DataProvider/Product/SearchCriteriaBuilderTest.php
+++ b/app/code/Magento/CatalogGraphQl/Test/Unit/DataProvider/Product/SearchCriteriaBuilderTest.php
@@ -97,50 +97,60 @@ public function testBuild(): void
$filter = $this->createMock(Filter::class);
$searchCriteria = $this->getMockBuilder(SearchCriteriaInterface::class)
- ->disableOriginalConstructor()
- ->getMockForAbstractClass();
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
$attributeInterface = $this->getMockBuilder(Attribute::class)
- ->disableOriginalConstructor()
- ->getMockForAbstractClass();
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
$attributeInterface->setData(['is_filterable' => 0]);
$this->builder->expects($this->any())
- ->method('build')
- ->with('products', $args)
- ->willReturn($searchCriteria);
+ ->method('build')
+ ->with('products', $args)
+ ->willReturn($searchCriteria);
$searchCriteria->expects($this->any())->method('getFilterGroups')->willReturn([]);
$this->eavConfig->expects($this->any())
- ->method('getAttribute')
- ->with(Product::ENTITY, 'price')
- ->willReturn($attributeInterface);
-
- $this->sortOrderBuilder->expects($this->once())
- ->method('setField')
- ->with('_id')
- ->willReturnSelf();
- $this->sortOrderBuilder->expects($this->once())
- ->method('setDirection')
- ->with('DESC')
- ->willReturnSelf();
- $this->sortOrderBuilder->expects($this->any())
- ->method('create')
- ->willReturn([]);
-
- $this->filterBuilder->expects($this->once())
- ->method('setField')
- ->with('visibility')
- ->willReturnSelf();
- $this->filterBuilder->expects($this->once())
- ->method('setValue')
- ->with("")
- ->willReturnSelf();
- $this->filterBuilder->expects($this->once())
- ->method('setConditionType')
- ->with('in')
- ->willReturnSelf();
-
- $this->filterBuilder->expects($this->once())->method('create')->willReturn($filter);
+ ->method('getAttribute')
+ ->with(Product::ENTITY, 'price')
+ ->willReturn($attributeInterface);
+ $sortOrderList = ['relevance', '_id'];
+
+ $this->sortOrderBuilder->expects($this->exactly(2))
+ ->method('setField')
+ ->withConsecutive([$sortOrderList[0]], [$sortOrderList[1]])
+ ->willReturnSelf();
+
+ $this->sortOrderBuilder->expects($this->exactly(2))
+ ->method('setDirection')
+ ->with('DESC')
+ ->willReturnSelf();
+
+ $this->sortOrderBuilder->expects($this->exactly(2))
+ ->method('create')
+ ->willReturn([]);
+
+ $filterOrderList = ['search_term', 'visibility'];
+
+ $this->filterBuilder->expects($this->exactly(2))
+ ->method('setField')
+ ->withConsecutive([$filterOrderList[0]], [$filterOrderList[1]])
+ ->willReturnSelf();
+
+ $this->filterBuilder->expects($this->exactly(2))
+ ->method('setValue')
+ ->with('')
+ ->willReturnSelf();
+
+ $this->filterBuilder->expects($this->exactly(2))
+ ->method('setConditionType')
+ ->withConsecutive([''], ['in'])
+ ->willReturnSelf();
+
+ $this->filterBuilder
+ ->expects($this->exactly(2))
+ ->method('create')
+ ->willReturn($filter);
$this->filterGroupBuilder->expects($this->any())
->method('addFilter')
diff --git a/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Category/DepthCalculatorTest.php b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Category/DepthCalculatorTest.php
new file mode 100644
index 0000000000000..489742db45f79
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Category/DepthCalculatorTest.php
@@ -0,0 +1,143 @@
+fieldNodeMock->kind = NodeKind::FIELD;
+ /** @var SelectionSetNode $selectionSetNode */
+ $selectionSetNode = new SelectionSetNode([]);
+ $selectionSetNode->selections = $this->getSelectionsArrayForNullCase();
+ $this->fieldNodeMock->selectionSet = $selectionSetNode;
+ $result = $this->depthCalculator->calculate($this->resolveInfoMock, $this->fieldNodeMock);
+ $this->assertSame(1, $result);
+ }
+
+ /**
+ * Await for depth of '2' if selectionSet is not null
+ * @return void
+ */
+ public function testCalculateNonNullAsSelectionSet(): void
+ {
+ $this->fieldNodeMock->kind = NodeKind::FIELD;
+ $selectionSetNode = $this->getSelectionSetNode();
+ $selectionSetNode->selections = $this->getSelectionsArrayForNonNullCase();
+ $this->fieldNodeMock->selectionSet = $selectionSetNode;
+ $result = $this->depthCalculator->calculate($this->resolveInfoMock, $this->fieldNodeMock);
+ $this->assertEquals(2, $result);
+ }
+
+ /**
+ * @return NodeList
+ */
+ private function getSelectionsArrayForNullCase()
+ {
+ $selectionSetNode = $this->getSelectionSetNode();
+ $selectionSetNode->selections = $this->getNodeList();
+ $inlineFragmentNode = $this->getNewInlineFragmentNode();
+ $inlineFragmentNode->selectionSet = $selectionSetNode;
+ return new NodeList([
+ $this->getNewFieldNode(),
+ $inlineFragmentNode
+ ]);
+ }
+
+ /**
+ * @return FieldNode
+ */
+ private function getNewFieldNode()
+ {
+ return new FieldNode([]);
+ }
+
+ /**
+ * @return InlineFragmentNode
+ */
+ private function getNewInlineFragmentNode()
+ {
+ return new InlineFragmentNode([]);
+ }
+
+ /**
+ * @return NodeList
+ */
+ private function getSelectionsArrayForNonNullCase()
+ {
+ $newFieldNode = $this->getNewFieldNode();
+ $newFieldNode->selectionSet = $this->getSelectionSetNode();
+ $newFieldNode->selectionSet->selections = $this->getNodeList();
+ $newFieldNode->selectionSet->selections[] = $this->getNewFieldNode();
+ $selectionSetNode = $this->getSelectionSetNode();
+ $selectionSetNode->selections = new NodeList([$newFieldNode]);
+ $inlineFragmentNode = $this->getNewInlineFragmentNode();
+ $inlineFragmentNode->selectionSet = $selectionSetNode;
+ return new NodeList([
+ $newFieldNode,
+ $inlineFragmentNode
+ ]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function setUp(): void
+ {
+ $this->depthCalculator = new DepthCalculator();
+ $this->resolveInfoMock = $this->createMock(ResolveInfo::class);
+ $this->fieldNodeMock = $this->getMockBuilder(FieldNode::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ }
+
+ /**
+ * @return \GraphQL\Language\AST\SelectionSetNode
+ */
+ protected function getSelectionSetNode($nodes = []): SelectionSetNode
+ {
+ return new SelectionSetNode($nodes);
+ }
+
+ /**
+ * @return \GraphQL\Language\AST\NodeList
+ */
+ protected function getNodeList(): NodeList
+ {
+ return new NodeList([]);
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/composer.json b/app/code/Magento/CatalogGraphQl/composer.json
index fbc4172226c58..f51d069ec0684 100644
--- a/app/code/Magento/CatalogGraphQl/composer.json
+++ b/app/code/Magento/CatalogGraphQl/composer.json
@@ -14,6 +14,7 @@
"magento/module-catalog-search": "*",
"magento/framework": "*",
"magento/module-graph-ql": "*",
+ "magento/module-config": "*",
"magento/module-advanced-search": "*"
},
"suggest": {
diff --git a/app/code/Magento/CatalogGraphQl/etc/di.xml b/app/code/Magento/CatalogGraphQl/etc/di.xml
index de72c4a559c4d..692dd5679182c 100644
--- a/app/code/Magento/CatalogGraphQl/etc/di.xml
+++ b/app/code/Magento/CatalogGraphQl/etc/di.xml
@@ -78,6 +78,7 @@
+ - Magento\CatalogGraphQl\Model\Resolver\Products\Query\CategoryUrlPathArgsProcessor
- Magento\CatalogGraphQl\Model\Resolver\Products\Query\CategoryUidArgsProcessor
- Magento\CatalogGraphQl\Model\Category\CategoryUidsArgsProcessor
- Magento\CatalogGraphQl\Model\Category\ParentCategoryUidsArgsProcessor
diff --git a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml
index d66ee50ba03a5..9fc1a47594458 100644
--- a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml
+++ b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml
@@ -208,4 +208,40 @@
+
+
+
+ - CatalogAttributeMetadata
+ - CatalogAttributeMetadata
+
+
+
+
+
+
+ -
+
- catalog_product
+ - catalog_category
+
+
+
+
+
+
+
+ - GetCatalogProductAttributesMetadata
+ - GetCatalogCategoryAttributesMetadata
+
+
+
+
+
+ catalog_product
+
+
+
+
+ catalog_category
+
+
diff --git a/app/code/Magento/CatalogGraphQl/etc/module.xml b/app/code/Magento/CatalogGraphQl/etc/module.xml
index 87696c129a714..1837648d1ca4d 100644
--- a/app/code/Magento/CatalogGraphQl/etc/module.xml
+++ b/app/code/Magento/CatalogGraphQl/etc/module.xml
@@ -13,6 +13,7 @@
+
diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls
index 0b42655ed73cc..3d3875bb5c588 100644
--- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls
@@ -125,6 +125,7 @@ interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\
categories: [CategoryInterface] @doc(description: "The categories assigned to a product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Categories") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoriesIdentity")
canonical_url: String @doc(description: "The relative canonical URL. This value is returned only if the system setting 'Use Canonical Link Meta Tag For Products' is enabled.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CanonicalUrl")
media_gallery: [MediaGalleryInterface] @doc(description: "An array of media gallery objects.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\MediaGallery")
+ custom_attributesV2(filters: AttributeFilterInput): ProductCustomAttributes @doc(description: "Product custom attributes.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductCustomAttributes")
}
interface PhysicalProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\ProductInterfaceTypeResolverComposite") @doc(description: "Contains attributes specific to tangible products.") {
@@ -344,6 +345,7 @@ type CategoryProducts @doc(description: "Contains details about the products ass
input ProductAttributeFilterInput @doc(description: "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 @deprecated(reason: "Use `category_uid` instead.") @doc(description: "Deprecated: use `category_uid` to filter product by category ID.")
category_uid: FilterEqualTypeInput @doc(description: "Filter product by the unique ID for a `CategoryInterface` object.")
+ category_url_path: FilterEqualTypeInput @doc(description: "Filter product by category URL path.")
}
input CategoryFilterInput @doc(description: "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.")
@@ -531,3 +533,38 @@ type SimpleWishlistItem implements WishlistItemInterface @doc(description: "Cont
type VirtualWishlistItem implements WishlistItemInterface @doc(description: "Contains a virtual product wish list item.") {
}
+
+enum AttributeEntityTypeEnum {
+ CATALOG_PRODUCT
+ CATALOG_CATEGORY
+}
+
+type CatalogAttributeMetadata implements CustomAttributeMetadataInterface @doc(description: "Catalog attribute metadata.") {
+ apply_to: [CatalogAttributeApplyToEnum] @doc(description: "To which catalog types an attribute can be applied.")
+ is_comparable: Boolean @doc(description: "Whether a product or category attribute can be compared against another or not.")
+ is_filterable: Boolean @doc(description: "Whether a product or category attribute can be filtered or not.")
+ is_filterable_in_search: Boolean @doc(description: "Whether a product or category attribute can be filtered in search or not.")
+ is_html_allowed_on_front: Boolean @doc(description: "Whether a product or category attribute can use HTML on front or not.")
+ is_searchable: Boolean @doc(description: "Whether a product or category attribute can be searched or not.")
+ is_used_for_price_rules: Boolean @doc(description: "Whether a product or category attribute can be used for price rules or not.")
+ is_used_for_promo_rules: Boolean @doc(description: "Whether a product or category attribute is used for promo rules or not.")
+ is_visible_in_advanced_search: Boolean @doc(description: "Whether a product or category attribute is visible in advanced search or not.")
+ is_visible_on_front: Boolean @doc(description: "Whether a product or category attribute is visible on front or not.")
+ is_wysiwyg_enabled: Boolean @doc(description: "Whether a product or category attribute has WYSIWYG enabled or not.")
+ used_in_product_listing: Boolean @doc(description: "Whether a product or category attribute is used in product listing or not.")
+}
+
+enum CatalogAttributeApplyToEnum {
+ SIMPLE
+ VIRTUAL
+ BUNDLE
+ DOWNLOADABLE
+ CONFIGURABLE
+ GROUPED
+ CATEGORY
+}
+
+type ProductCustomAttributes @doc(description: "Product custom attributes") {
+ items: [AttributeValueInterface!]! @doc(description: "Requested custom attributes")
+ errors: [AttributeMetadataError!]! @doc(description: "Errors when retrieving custom attributes metadata.")
+}
diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php
index 2c0d7f45af19a..2784530566bb0 100644
--- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php
+++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php
@@ -640,10 +640,13 @@ protected function prepareCatalogInventory(array $productIds)
if ($stockItemRow['use_config_max_sale_qty']) {
$stockItemRow['max_sale_qty'] = $this->stockConfiguration->getMaxSaleQty();
}
-
if ($stockItemRow['use_config_min_sale_qty']) {
$stockItemRow['min_sale_qty'] = $this->stockConfiguration->getMinSaleQty();
}
+ if ($stockItemRow['use_config_manage_stock']) {
+ $stockItemRow['manage_stock'] = $this->stockConfiguration->getManageStock();
+ }
+
$stockItemRows[$productId] = $stockItemRow;
}
return $stockItemRows;
diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php
index 3058c6f6619d4..a0ee8d77d69f3 100644
--- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php
+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php
@@ -8,6 +8,8 @@
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\Config as CatalogConfig;
+use Magento\Catalog\Model\Indexer\Product\Category as ProductCategoryIndexer;
+use Magento\Catalog\Model\Indexer\Product\Price\Processor as ProductPriceIndexer;
use Magento\Catalog\Model\Product\Visibility;
use Magento\CatalogImportExport\Model\Import\Product\ImageTypeProcessor;
use Magento\CatalogImportExport\Model\Import\Product\LinkProcessor;
@@ -16,6 +18,7 @@
use Magento\CatalogImportExport\Model\Import\Product\Skip;
use Magento\CatalogImportExport\Model\Import\Product\StatusProcessor;
use Magento\CatalogImportExport\Model\Import\Product\StockProcessor;
+use Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType;
use Magento\CatalogImportExport\Model\StockItemImporterInterface;
use Magento\CatalogImportExport\Model\StockItemProcessorInterface;
use Magento\CatalogInventory\Api\Data\StockItemInterface;
@@ -48,6 +51,7 @@
*/
class Product extends AbstractEntity
{
+ private const COL_NAME_FORMAT = '/[\x00-\x1F\x7F]/';
private const DEFAULT_GLOBAL_MULTIPLE_VALUE_SEPARATOR = ',';
public const CONFIG_KEY_PRODUCT_TYPES = 'global/importexport/import_product_types';
@@ -461,7 +465,7 @@ class Product extends AbstractEntity
/**
* Array of supported product types as keys with appropriate model object as value.
*
- * @var \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType[]
+ * @var AbstractType[]
*/
protected $_productTypeModels = [];
@@ -1102,6 +1106,7 @@ protected function _deleteProducts()
'catalog_product_import_bunch_delete_after',
['adapter' => $this, 'bunch' => $bunch]
);
+ $this->reindexProducts($idsToDelete);
}
}
return $this;
@@ -1222,6 +1227,11 @@ private function initImagesArrayKeys()
*/
protected function _initTypeModels()
{
+ // When multiple imports are processed in a single php process,
+ // these memory caches may interfere with the import result.
+ AbstractType::$commonAttributesCache = [];
+ AbstractType::$invAttributesCache = [];
+ AbstractType::$attributeCodeToId = [];
$productTypes = $this->_importConfig->getEntityTypes($this->getEntityTypeCode());
$fieldsMap = [];
$specialAttributes = [];
@@ -1233,11 +1243,11 @@ protected function _initTypeModels()
__('Entity type model \'%1\' is not found', $productTypeConfig['model'])
);
}
- if (!$model instanceof \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType) {
+ if (!$model instanceof AbstractType) {
throw new LocalizedException(
__(
'Entity type model must be an instance of '
- . \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType::class
+ . AbstractType::class
)
);
}
@@ -1624,6 +1634,10 @@ protected function _saveProducts()
// the bunch of products will pass for the event with url_key column.
$bunch[$rowNum][self::URL_KEY] = $rowData[self::URL_KEY] = $urlKey;
}
+ if (!empty($rowData[self::COL_NAME])) {
+ // remove null byte character
+ $rowData[self::COL_NAME] = preg_replace(self::COL_NAME_FORMAT, '', $rowData[self::COL_NAME]);
+ }
$rowSku = $rowData[self::COL_SKU];
if (null === $rowSku) {
$this->getErrorAggregator()->addRowToSkip($rowNum);
@@ -1660,7 +1674,7 @@ protected function _saveProducts()
$prevAttributeSet,
$attributes
);
- // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch
+ // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch
} catch (Skip $skip) {
// Product is skipped. Go on to the next one.
}
@@ -2034,10 +2048,7 @@ private function saveProductAttributesPhase(
}
if (self::SCOPE_STORE == $rowScope) {
if (self::SCOPE_WEBSITE == $attribute->getIsGlobal()) {
- // check website defaults already set
- if (!isset($attributes[$attrTable][$rowSku][$attrId][$rowStore])) {
- $storeIds = $this->storeResolver->getStoreIdToWebsiteStoreIds($rowStore);
- }
+ $storeIds = $this->storeResolver->getStoreIdToWebsiteStoreIds($rowStore);
} elseif (self::SCOPE_STORE == $attribute->getIsGlobal()) {
$storeIds = [$rowStore];
}
@@ -2466,9 +2477,17 @@ private function reindexStockStatus(array $productIds): void
*/
private function reindexProducts($productIdsToReindex = [])
{
- $indexer = $this->indexerRegistry->get('catalog_product_category');
- if (is_array($productIdsToReindex) && count($productIdsToReindex) > 0 && !$indexer->isScheduled()) {
- $indexer->reindexList($productIdsToReindex);
+ if (is_array($productIdsToReindex) && !empty($productIdsToReindex)) {
+ $indexersToReindex = [
+ ProductCategoryIndexer::INDEXER_ID,
+ ProductPriceIndexer::INDEXER_ID
+ ];
+ foreach ($indexersToReindex as $id) {
+ $indexer = $this->indexerRegistry->get($id);
+ if (!$indexer->isScheduled()) {
+ $indexer->reindexList($productIdsToReindex);
+ }
+ }
}
}
@@ -2678,7 +2697,7 @@ public function validateRow(array $rowData, $rowNum)
// set attribute set code into row data for followed attribute validation in type model
$rowData[self::COL_ATTR_SET] = $newSku['attr_set_code'];
- /** @var \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType $productTypeValidator */
+ /** @var AbstractType $productTypeValidator */
// isRowValid can add error to general errors pull if row is invalid
$productTypeValidator = $this->_productTypeModels[$newSku['type_id']];
$productTypeValidator->isRowValid(
diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php
index 3a6d8d2533e85..856a985014ff7 100644
--- a/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php
+++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/CategoryProcessor.php
@@ -5,6 +5,8 @@
*/
namespace Magento\CatalogImportExport\Model\Import\Product;
+use Magento\Store\Model\Store;
+
/**
* @api
* @since 100.0.2
@@ -119,6 +121,7 @@ protected function createCategory($name, $parentId)
$category->setIsActive(true);
$category->setIncludeInMenu(true);
$category->setAttributeSetId($category->getDefaultAttributeSetId());
+ $category->setStoreId(Store::DEFAULT_STORE_ID);
$category->save();
$this->categoriesCache[$category->getId()] = $category;
return $category->getId();
diff --git a/app/code/Magento/CatalogImportExport/Model/Indexer/Category/Product/Plugin/Import.php b/app/code/Magento/CatalogImportExport/Model/Indexer/Category/Product/Plugin/Import.php
deleted file mode 100644
index d8a926f7cfe31..0000000000000
--- a/app/code/Magento/CatalogImportExport/Model/Indexer/Category/Product/Plugin/Import.php
+++ /dev/null
@@ -1,41 +0,0 @@
-_indexerCategoryProductProcessor = $indexerCategoryProductProcessor;
- }
-
- /**
- * After import handler
- *
- * @param \Magento\ImportExport\Model\Import $subject
- * @param boolean $import
- *
- * @return mixed
- * @SuppressWarnings(PHPMD.UnusedFormalParameter)
- */
- public function afterImportSource(\Magento\ImportExport\Model\Import $subject, $import)
- {
- if (!$this->_indexerCategoryProductProcessor->isIndexerScheduled()) {
- $this->_indexerCategoryProductProcessor->markIndexerAsInvalid();
- }
- return $import;
- }
-}
diff --git a/app/code/Magento/CatalogImportExport/Model/Indexer/Product/Category/Plugin/Import.php b/app/code/Magento/CatalogImportExport/Model/Indexer/Product/Category/Plugin/Import.php
deleted file mode 100644
index 0d0d4ea80530a..0000000000000
--- a/app/code/Magento/CatalogImportExport/Model/Indexer/Product/Category/Plugin/Import.php
+++ /dev/null
@@ -1,41 +0,0 @@
-_indexerProductCategoryProcessor = $indexerProductCategoryProcessor;
- }
-
- /**
- * After import handler
- *
- * @param \Magento\ImportExport\Model\Import $subject
- * @param boolean $import
- *
- * @return mixed
- * @SuppressWarnings(PHPMD.UnusedFormalParameter)
- */
- public function afterImportSource(\Magento\ImportExport\Model\Import $subject, $import)
- {
- if (!$this->_indexerProductCategoryProcessor->isIndexerScheduled()) {
- $this->_indexerProductCategoryProcessor->markIndexerAsInvalid();
- }
- return $import;
- }
-}
diff --git a/app/code/Magento/CatalogImportExport/Model/Indexer/Product/Price/Plugin/Import.php b/app/code/Magento/CatalogImportExport/Model/Indexer/Product/Price/Plugin/Import.php
deleted file mode 100644
index 87020be7cd30d..0000000000000
--- a/app/code/Magento/CatalogImportExport/Model/Indexer/Product/Price/Plugin/Import.php
+++ /dev/null
@@ -1,39 +0,0 @@
-indexerRegistry = $indexerRegistry;
- }
-
- /**
- * After import handler
- *
- * @param \Magento\ImportExport\Model\Import $subject
- * @param bool $result
- * @return bool
- * @SuppressWarnings(PHPMD.UnusedFormalParameter)
- */
- public function afterImportSource(\Magento\ImportExport\Model\Import $subject, $result)
- {
- $priceIndexer = $this->indexerRegistry->get(\Magento\Catalog\Model\Indexer\Product\Price\Processor::INDEXER_ID);
- if (!$priceIndexer->isScheduled()) {
- $priceIndexer->invalidate();
- }
- return $result;
- }
-}
diff --git a/app/code/Magento/CatalogImportExport/Model/Indexer/Stock/Plugin/Import.php b/app/code/Magento/CatalogImportExport/Model/Indexer/Stock/Plugin/Import.php
deleted file mode 100644
index c83045b2062cb..0000000000000
--- a/app/code/Magento/CatalogImportExport/Model/Indexer/Stock/Plugin/Import.php
+++ /dev/null
@@ -1,39 +0,0 @@
-_stockndexerProcessor = $stockndexerProcessor;
- }
-
- /**
- * After import handler
- *
- * @param \Magento\ImportExport\Model\Import $subject
- * @param Object $import
- *
- * @return mixed
- * @SuppressWarnings(PHPMD.UnusedFormalParameter)
- */
- public function afterImportSource(\Magento\ImportExport\Model\Import $subject, $import)
- {
- if (!$this->_stockndexerProcessor->isIndexerScheduled()) {
- $this->_stockndexerProcessor->markIndexerAsInvalid();
- }
- return $import;
- }
-}
diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php
index c2ce4c6499ecc..9e3a2f220f73d 100644
--- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php
+++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/CategoryProcessorTest.php
@@ -13,16 +13,17 @@
use Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType;
use Magento\Framework\Exception\AlreadyExistsException;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper;
+use Magento\Store\Model\Store;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
class CategoryProcessorTest extends TestCase
{
- const PARENT_CATEGORY_ID = 1;
+ public const PARENT_CATEGORY_ID = 1;
- const CHILD_CATEGORY_ID = 2;
+ public const CHILD_CATEGORY_ID = 2;
- const CHILD_CATEGORY_NAME = 'Child';
+ public const CHILD_CATEGORY_NAME = 'Child';
/**
* @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager
@@ -48,7 +49,7 @@ class CategoryProcessorTest extends TestCase
private $childCategory;
/**
- * \Magento\Catalog\Model\Category
+ * @var \Magento\Catalog\Model\Category
*/
private $parentCategory;
@@ -200,4 +201,19 @@ protected function setPropertyValue(&$object, $property, $value)
$reflectionProperty->setValue($object, $value);
return $object;
}
+
+ /**
+ * @throws \ReflectionException
+ */
+ public function testCategoriesCreatedForGlobalScope()
+ {
+ $this->childCategory->expects($this->once())
+ ->method('setStoreId')
+ ->with(Store::DEFAULT_STORE_ID);
+
+ $reflection = new \ReflectionClass($this->categoryProcessor);
+ $createCategoryReflection = $reflection->getMethod('createCategory');
+ $createCategoryReflection->setAccessible(true);
+ $createCategoryReflection->invokeArgs($this->categoryProcessor, ['testCategory', 2]);
+ }
}
diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Indexer/Product/Price/Plugin/ImportTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Indexer/Product/Price/Plugin/ImportTest.php
deleted file mode 100644
index d5ae17d5c392f..0000000000000
--- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Indexer/Product/Price/Plugin/ImportTest.php
+++ /dev/null
@@ -1,77 +0,0 @@
-_objectManager = new ObjectManager($this);
-
- $this->_indexerMock = $this->getMockBuilder(Indexer::class)
- ->addMethods(['getPriceIndexer'])
- ->onlyMethods(['getId', 'invalidate', 'isScheduled'])
- ->disableOriginalConstructor()
- ->getMock();
- $this->indexerRegistryMock = $this->createPartialMock(
- IndexerRegistry::class,
- ['get']
- );
-
- $this->_model = $this->_objectManager->getObject(
- Import::class,
- ['indexerRegistry' => $this->indexerRegistryMock]
- );
- }
-
- /**
- * Test AfterImportSource()
- */
- public function testAfterImportSource()
- {
- $this->_indexerMock->expects($this->once())->method('invalidate');
- $this->indexerRegistryMock->expects($this->any())
- ->method('get')
- ->with(Processor::INDEXER_ID)
- ->willReturn($this->_indexerMock);
- $this->_indexerMock->expects($this->any())
- ->method('isScheduled')
- ->willReturn(false);
-
- $importMock = $this->createMock(\Magento\ImportExport\Model\Import::class);
- $this->assertEquals('return_value', $this->_model->afterImportSource($importMock, 'return_value'));
- }
-}
diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Indexer/Stock/Plugin/ImportTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Indexer/Stock/Plugin/ImportTest.php
deleted file mode 100644
index 3659cde191b54..0000000000000
--- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Indexer/Stock/Plugin/ImportTest.php
+++ /dev/null
@@ -1,36 +0,0 @@
-createPartialMock(
- Processor::class,
- ['markIndexerAsInvalid', 'isIndexerScheduled']
- );
-
- $subjectMock = $this->createMock(Import::class);
- $processorMock->expects($this->any())->method('markIndexerAsInvalid');
- $processorMock->expects($this->any())->method('isIndexerScheduled')->willReturn(false);
-
- $someData = [1, 2, 3];
-
- $model = new \Magento\CatalogImportExport\Model\Indexer\Stock\Plugin\Import($processorMock);
- $this->assertEquals($someData, $model->afterImportSource($subjectMock, $someData));
- }
-}
diff --git a/app/code/Magento/CatalogImportExport/etc/di.xml b/app/code/Magento/CatalogImportExport/etc/di.xml
index 43fdda6227ac7..4150fca46fa6a 100644
--- a/app/code/Magento/CatalogImportExport/etc/di.xml
+++ b/app/code/Magento/CatalogImportExport/etc/di.xml
@@ -12,11 +12,7 @@
-
-
-
-
diff --git a/app/code/Magento/CatalogImportExport/etc/import.xml b/app/code/Magento/CatalogImportExport/etc/import.xml
index 522b478752f01..05f8ceb5425d4 100644
--- a/app/code/Magento/CatalogImportExport/etc/import.xml
+++ b/app/code/Magento/CatalogImportExport/etc/import.xml
@@ -9,7 +9,5 @@
-
-
diff --git a/app/code/Magento/CatalogInventory/Helper/Minsaleqty.php b/app/code/Magento/CatalogInventory/Helper/Minsaleqty.php
index 96bf5bd965355..1ee8e1a97e89f 100644
--- a/app/code/Magento/CatalogInventory/Helper/Minsaleqty.php
+++ b/app/code/Magento/CatalogInventory/Helper/Minsaleqty.php
@@ -8,13 +8,14 @@
use Magento\Customer\Api\GroupManagementInterface;
use Magento\Framework\App\ObjectManager;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Framework\Serialize\Serializer\Json;
use Magento\Store\Model\Store;
/**
* MinSaleQty value manipulation helper
*/
-class Minsaleqty
+class Minsaleqty implements ResetAfterRequestInterface
{
/**
* Core store config
@@ -61,6 +62,14 @@ public function __construct(
$this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class);
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->minSaleQtyCache = [];
+ }
+
/**
* Retrieve fixed qty value
*
diff --git a/app/code/Magento/CatalogInventory/Model/Configuration.php b/app/code/Magento/CatalogInventory/Model/Configuration.php
index 8b0849c8874bc..9df634c225d71 100644
--- a/app/code/Magento/CatalogInventory/Model/Configuration.php
+++ b/app/code/Magento/CatalogInventory/Model/Configuration.php
@@ -9,98 +9,96 @@
use Magento\CatalogInventory\Helper\Minsaleqty as MinsaleqtyHelper;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Catalog\Model\ProductTypes\ConfigInterface;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Store\Model\ScopeInterface;
use Magento\Store\Model\StoreManagerInterface;
-/**
- * Class Configuration
- */
-class Configuration implements StockConfigurationInterface
+class Configuration implements StockConfigurationInterface, ResetAfterRequestInterface
{
/**
* Default website id
*/
- const DEFAULT_WEBSITE_ID = 1;
+ public const DEFAULT_WEBSITE_ID = 1;
/**
* Inventory options config path
*/
- const XML_PATH_GLOBAL = 'cataloginventory/options/';
+ public const XML_PATH_GLOBAL = 'cataloginventory/options/';
/**
* Subtract config path
*/
- const XML_PATH_CAN_SUBTRACT = 'cataloginventory/options/can_subtract';
+ public const XML_PATH_CAN_SUBTRACT = 'cataloginventory/options/can_subtract';
/**
* Back in stock config path
*/
- const XML_PATH_CAN_BACK_IN_STOCK = 'cataloginventory/options/can_back_in_stock';
+ public const XML_PATH_CAN_BACK_IN_STOCK = 'cataloginventory/options/can_back_in_stock';
/**
* Item options config path
*/
- const XML_PATH_ITEM = 'cataloginventory/item_options/';
+ public const XML_PATH_ITEM = 'cataloginventory/item_options/';
/**
* Max qty config path
*/
- const XML_PATH_MIN_QTY = 'cataloginventory/item_options/min_qty';
+ public const XML_PATH_MIN_QTY = 'cataloginventory/item_options/min_qty';
/**
* Min sale qty config path
*/
- const XML_PATH_MIN_SALE_QTY = 'cataloginventory/item_options/min_sale_qty';
+ public const XML_PATH_MIN_SALE_QTY = 'cataloginventory/item_options/min_sale_qty';
/**
* Max sale qty config path
*/
- const XML_PATH_MAX_SALE_QTY = 'cataloginventory/item_options/max_sale_qty';
+ public const XML_PATH_MAX_SALE_QTY = 'cataloginventory/item_options/max_sale_qty';
/**
* Back orders config path
*/
- const XML_PATH_BACKORDERS = 'cataloginventory/item_options/backorders';
+ public const XML_PATH_BACKORDERS = 'cataloginventory/item_options/backorders';
/**
* Notify stock config path
*/
- const XML_PATH_NOTIFY_STOCK_QTY = 'cataloginventory/item_options/notify_stock_qty';
+ public const XML_PATH_NOTIFY_STOCK_QTY = 'cataloginventory/item_options/notify_stock_qty';
/**
* Manage stock config path
*/
- const XML_PATH_MANAGE_STOCK = 'cataloginventory/item_options/manage_stock';
+ public const XML_PATH_MANAGE_STOCK = 'cataloginventory/item_options/manage_stock';
/**
* Enable qty increments config path
*/
- const XML_PATH_ENABLE_QTY_INCREMENTS = 'cataloginventory/item_options/enable_qty_increments';
+ public const XML_PATH_ENABLE_QTY_INCREMENTS = 'cataloginventory/item_options/enable_qty_increments';
/**
* Qty increments config path
*/
- const XML_PATH_QTY_INCREMENTS = 'cataloginventory/item_options/qty_increments';
+ public const XML_PATH_QTY_INCREMENTS = 'cataloginventory/item_options/qty_increments';
/**
* Show out of stock config path
*/
- const XML_PATH_SHOW_OUT_OF_STOCK = 'cataloginventory/options/show_out_of_stock';
+ public const XML_PATH_SHOW_OUT_OF_STOCK = 'cataloginventory/options/show_out_of_stock';
/**
* Auto return config path
*/
- const XML_PATH_ITEM_AUTO_RETURN = 'cataloginventory/item_options/auto_return';
+ public const XML_PATH_ITEM_AUTO_RETURN = 'cataloginventory/item_options/auto_return';
/**
* Path to configuration option 'Display product stock status'
*/
- const XML_PATH_DISPLAY_PRODUCT_STOCK_STATUS = 'cataloginventory/options/display_product_stock_status';
+ public const XML_PATH_DISPLAY_PRODUCT_STOCK_STATUS = 'cataloginventory/options/display_product_stock_status';
/**
* Threshold qty config path
*/
- const XML_PATH_STOCK_THRESHOLD_QTY = 'cataloginventory/options/stock_threshold_qty';
+ public const XML_PATH_STOCK_THRESHOLD_QTY = 'cataloginventory/options/stock_threshold_qty';
/**
* @var ConfigInterface
@@ -122,7 +120,7 @@ class Configuration implements StockConfigurationInterface
/**
* All product types registry in scope of quantity availability
*
- * @var array
+ * @var array|null
*/
protected $isQtyTypeIds;
@@ -151,6 +149,14 @@ public function __construct(
$this->storeManager = $storeManager;
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->isQtyTypeIds = null;
+ }
+
/**
* @inheritdoc
*/
diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php
index c871a8dee65f4..c5b71d9345804 100644
--- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php
+++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php
@@ -90,7 +90,7 @@ public function clean(array $productIds, callable $reindex)
$this->cacheContext->registerEntities(Product::CACHE_TAG, array_unique($productIds));
$this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]);
$categoryIds = $this->getCategoryIdsByProductIds($productIds);
- if ($categoryIds){
+ if ($categoryIds) {
$this->cacheContext->registerEntities(Category::CACHE_TAG, array_unique($categoryIds));
$this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]);
}
@@ -162,7 +162,7 @@ private function getProductIdsForCacheClean(array $productStatusesBefore, array
}
}
- return $productIds;
+ return array_map('intval', $productIds);
}
/**
@@ -176,7 +176,7 @@ private function getCategoryIdsByProductIds(array $productIds): array
$categoryProductTable = $this->resource->getTableName('catalog_category_product');
$select = $this->getConnection()->select()
->from(['catalog_category_product' => $categoryProductTable], ['category_id'])
- ->where('product_id IN (?)', $productIds);
+ ->where('product_id IN (?)', $productIds, \Zend_Db::INT_TYPE);
return $this->getConnection()->fetchCol($select);
}
diff --git a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/StockItem.php b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/StockItem.php
index f104552b4e0fc..ea4c35de053b2 100644
--- a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/StockItem.php
+++ b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/Initializer/StockItem.php
@@ -35,6 +35,8 @@ class StockItem
/**
* @var StockStateProviderInterface
+ * @deprecated
+ * @see was overriding ItemBackorders value with the Default Scope value; caused discrepancy in multistock config
*/
private $stockStateProvider;
@@ -122,11 +124,6 @@ public function initialize(
$quoteItem->setHasError(true);
}
- /* We need to ensure that any possible plugin will not erase the data */
- $backOrdersQty = $this->stockStateProvider->checkQuoteItemQty($stockItem, $rowQty, $qtyForCheck, $qty)
- ->getItemBackorders();
- $result->setItemBackorders($backOrdersQty);
-
if ($stockItem->hasIsChildItem()) {
$stockItem->unsIsChildItem();
}
diff --git a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/QuoteItemQtyList.php b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/QuoteItemQtyList.php
index 600bf9897a036..363f91916fb75 100644
--- a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/QuoteItemQtyList.php
+++ b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator/QuoteItemQtyList.php
@@ -33,7 +33,7 @@ class QuoteItemQtyList
public function getQty($productId, $quoteItemId, $quoteId, $itemQty)
{
$qty = $itemQty;
- if (isset($this->_checkedQuoteItems[$quoteId][$productId]['qty']) && !in_array(
+ if (isset($this->_checkedQuoteItems[$quoteId][$productId]['qty']) && $quoteItemId !== null && !in_array(
$quoteItemId,
$this->_checkedQuoteItems[$quoteId][$productId]['items']
)
diff --git a/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php b/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php
index 8238c1e8f6b21..50e16ad5ed04a 100644
--- a/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php
+++ b/app/code/Magento/CatalogInventory/Model/StockRegistryStorage.php
@@ -8,11 +8,9 @@
use Magento\CatalogInventory\Api\Data\StockInterface;
use Magento\CatalogInventory\Api\Data\StockItemInterface;
use Magento\CatalogInventory\Api\Data\StockStatusInterface;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
-/**
- * Class StockRegistryStorage
- */
-class StockRegistryStorage
+class StockRegistryStorage implements ResetAfterRequestInterface
{
/**
* @var array
@@ -30,6 +28,8 @@ class StockRegistryStorage
private $stockStatuses = [];
/**
+ * Get Stock Data
+ *
* @param int $scopeId
* @return StockInterface
*/
@@ -39,6 +39,8 @@ public function getStock($scopeId)
}
/**
+ * Set Stock cache
+ *
* @param int $scopeId
* @param StockInterface $value
* @return void
@@ -49,6 +51,8 @@ public function setStock($scopeId, StockInterface $value)
}
/**
+ * Delete cached Stock based on scopeId
+ *
* @param int|null $scopeId
* @return void
*/
@@ -62,6 +66,8 @@ public function removeStock($scopeId = null)
}
/**
+ * Retrieve Stock Item
+ *
* @param int $productId
* @param int $scopeId
* @return StockItemInterface
@@ -72,6 +78,8 @@ public function getStockItem($productId, $scopeId)
}
/**
+ * Update Stock Item
+ *
* @param int $productId
* @param int $scopeId
* @param StockItemInterface $value
@@ -83,6 +91,8 @@ public function setStockItem($productId, $scopeId, StockItemInterface $value)
}
/**
+ * Remove stock Item based on productId & scopeId
+ *
* @param int $productId
* @param int|null $scopeId
* @return void
@@ -97,6 +107,8 @@ public function removeStockItem($productId, $scopeId = null)
}
/**
+ * Retrieve stock status
+ *
* @param int $productId
* @param int $scopeId
* @return StockStatusInterface
@@ -107,6 +119,8 @@ public function getStockStatus($productId, $scopeId)
}
/**
+ * Update stock Status
+ *
* @param int $productId
* @param int $scopeId
* @param StockStatusInterface $value
@@ -118,6 +132,8 @@ public function setStockStatus($productId, $scopeId, StockStatusInterface $value
}
/**
+ * Clear stock status
+ *
* @param int $productId
* @param int|null $scopeId
* @return void
@@ -142,4 +158,12 @@ public function clean()
$this->stocks = [];
$this->stockStatuses = [];
}
+
+ /**
+ * @inheritdoc
+ */
+ public function _resetState(): void
+ {
+ $this->clean();
+ }
}
diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml
index 6f388c3e6c6d1..9a198dd571def 100644
--- a/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml
+++ b/app/code/Magento/CatalogInventory/Test/Mftf/Section/AdminProductFormAdvancedInventorySection.xml
@@ -36,5 +36,7 @@
+
+
diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest.xml
index 26dd08be0a8c6..75eb122118ef9 100644
--- a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest.xml
+++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AdminCreateProductWithZeroMaximumQtyAllowedInShoppingCartTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml
index cd1931cf7fb78..60e903f59225c 100644
--- a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml
+++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/StoreFrontAddOutOfStockProductToShoppingCartTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/StoreFrontAddOutOfStockProductToShoppingCartTest.xml
index 951ca2b0ee80b..0242673fb0034 100644
--- a/app/code/Magento/CatalogInventory/Test/Mftf/Test/StoreFrontAddOutOfStockProductToShoppingCartTest.xml
+++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/StoreFrontAddOutOfStockProductToShoppingCartTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontValidateQuantityIncrementsWithDecimalInventoryTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontValidateQuantityIncrementsWithDecimalInventoryTest.xml
index e17c8fe65d4cf..c7beaa3c6835d 100644
--- a/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontValidateQuantityIncrementsWithDecimalInventoryTest.xml
+++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontValidateQuantityIncrementsWithDecimalInventoryTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php
index 794f5d92da1e8..22dce1a600601 100644
--- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php
+++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/CacheCleanerTest.php
@@ -122,9 +122,6 @@ public function testClean($stockStatusBefore, $stockStatusAfter, $qtyAfter, $sto
$this->selectMock->expects($this->any())
->method('from')
->willReturnSelf();
- $this->selectMock->expects($this->any())
- ->method('where')
- ->willReturnSelf();
$this->selectMock->expects($this->any())
->method('joinLeft')
->willReturnSelf();
@@ -141,6 +138,21 @@ public function testClean($stockStatusBefore, $stockStatusAfter, $qtyAfter, $sto
['product_id' => $productId, 'stock_status' => $stockStatusAfter, 'qty' => $qtyAfter],
]
);
+ $this->connectionMock->expects($this->exactly(3))
+ ->method('select')
+ ->willReturn($this->selectMock);
+ $this->selectMock->expects($this->exactly(7))
+ ->method('where')
+ ->withConsecutive(
+ ['product_id IN (?)'],
+ ['stock_id = ?'],
+ ['website_id = ?'],
+ ['product_id IN (?)'],
+ ['stock_id = ?'],
+ ['website_id = ?'],
+ ['product_id IN (?)', [123], \Zend_Db::INT_TYPE]
+ )
+ ->willReturnSelf();
$this->connectionMock->expects($this->exactly(1))
->method('fetchCol')
->willReturn([$categoryId]);
diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/StockItemTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/StockItemTest.php
index 24f46c2414f35..9591b84b4c8d5 100644
--- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/StockItemTest.php
+++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/Initializer/StockItemTest.php
@@ -173,10 +173,7 @@ public function testInitializeWithSubitem()
->method('checkQuoteItemQty')
->withAnyParameters()
->willReturn($result);
- $this->stockStateProviderMock->expects($this->once())
- ->method('checkQuoteItemQty')
- ->withAnyParameters()
- ->willReturn($result);
+ $this->stockStateProviderMock->expects($this->never())->method('checkQuoteItemQty');
$product->expects($this->once())
->method('getCustomOption')
->with('product_type')
@@ -213,7 +210,7 @@ public function testInitializeWithSubitem()
$quoteItem->expects($this->once())->method('setUseOldQty')->with('item')->willReturnSelf();
$result->expects($this->exactly(2))->method('getMessage')->willReturn('message');
$quoteItem->expects($this->once())->method('setMessage')->with('message')->willReturnSelf();
- $result->expects($this->exactly(3))->method('getItemBackorders')->willReturn('backorders');
+ $result->expects($this->exactly(2))->method('getItemBackorders')->willReturn('backorders');
$quoteItem->expects($this->once())->method('setBackorders')->with('backorders')->willReturnSelf();
$quoteItem->expects($this->once())->method('setStockStateResult')->with($result)->willReturnSelf();
@@ -276,10 +273,7 @@ public function testInitializeWithoutSubitem()
->method('checkQuoteItemQty')
->withAnyParameters()
->willReturn($result);
- $this->stockStateProviderMock->expects($this->once())
- ->method('checkQuoteItemQty')
- ->withAnyParameters()
- ->willReturn($result);
+ $this->stockStateProviderMock->expects($this->never())->method('checkQuoteItemQty');
$product->expects($this->once())
->method('getCustomOption')
->with('product_type')
@@ -299,7 +293,7 @@ public function testInitializeWithoutSubitem()
$result->expects($this->once())->method('getHasQtyOptionUpdate')->willReturn(false);
$result->expects($this->once())->method('getItemUseOldQty')->willReturn(null);
$result->expects($this->once())->method('getMessage')->willReturn(null);
- $result->expects($this->exactly(2))->method('getItemBackorders')->willReturn(null);
+ $result->expects($this->exactly(1))->method('getItemBackorders')->willReturn(null);
$this->model->initialize($stockItem, $quoteItem, $qty);
}
diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/QuoteItemQtyListTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/QuoteItemQtyListTest.php
index 44ce1fe6a3451..df9d3ee94dbbd 100644
--- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/QuoteItemQtyListTest.php
+++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Quote/Item/QuantityValidator/QuoteItemQtyListTest.php
@@ -49,6 +49,10 @@ public function testSingleQuoteItemQty()
$qty = $this->quoteItemQtyList->getQty(125, 1, 11232, 1);
$this->assertEquals($this->itemQtyTestValue, $qty);
+
+ $this->itemQtyTestValue = 2;
+ $qty = $this->quoteItemQtyList->getQty(125, null, 11232, 1);
+ $this->assertNotEquals($this->itemQtyTestValue, $qty);
}
/**
diff --git a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php
index e19757401621a..5d1f91f962834 100644
--- a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php
+++ b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php
@@ -9,6 +9,8 @@
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\ProductFactory;
use Magento\Catalog\Model\ResourceModel\Indexer\ActiveTableSwitcher;
+use Magento\Catalog\Model\Indexer\Product\Price\Processor as PriceIndexProcessor;
+use Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory;
use Magento\CatalogRule\Model\Indexer\IndexBuilder\ProductLoader;
use Magento\CatalogRule\Model\Indexer\IndexerTableSwapperInterface as TableSwapper;
@@ -19,6 +21,7 @@
use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\Indexer\IndexerRegistry;
use Magento\Framework\Pricing\PriceCurrencyInterface;
use Magento\Framework\Stdlib\DateTime;
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
@@ -168,6 +171,11 @@ class IndexBuilder
*/
private $productLoader;
+ /**
+ * @var IndexerRegistry
+ */
+ private $indexerRegistry;
+
/**
* @var ProductCollectionFactory
*/
@@ -195,6 +203,7 @@ class IndexBuilder
* @param TableSwapper|null $tableSwapper
* @param TimezoneInterface|null $localeDate
* @param ProductCollectionFactory|null $productCollectionFactory
+ * @param IndexerRegistry|null $indexerRegistry
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
@@ -219,7 +228,8 @@ public function __construct(
ProductLoader $productLoader = null,
TableSwapper $tableSwapper = null,
TimezoneInterface $localeDate = null,
- ProductCollectionFactory $productCollectionFactory = null
+ ProductCollectionFactory $productCollectionFactory = null,
+ IndexerRegistry $indexerRegistry = null
) {
$this->resource = $resource;
$this->connection = $resource->getConnection();
@@ -261,6 +271,8 @@ public function __construct(
ObjectManager::getInstance()->get(TableSwapper::class);
$this->localeDate = $localeDate ??
ObjectManager::getInstance()->get(TimezoneInterface::class);
+ $this->indexerRegistry = $indexerRegistry ??
+ ObjectManager::getInstance()->get(IndexerRegistry::class);
$this->productCollectionFactory = $productCollectionFactory ??
ObjectManager::getInstance()->get(ProductCollectionFactory::class);
}
@@ -333,6 +345,15 @@ protected function doReindexByIds($ids)
$this->reindexRuleProductPrice->execute($this->batchCount, $productId);
}
+ //the case was not handled via indexer dependency decorator or via mview configuration
+ $ruleIndexer = $this->indexerRegistry->get(RuleProductProcessor::INDEXER_ID);
+ if ($ruleIndexer->isScheduled()) {
+ $priceIndexer = $this->indexerRegistry->get(PriceIndexProcessor::INDEXER_ID);
+ if (!$priceIndexer->isScheduled()) {
+ $priceIndexer->reindexList($ids);
+ }
+ }
+
$this->reindexRuleGroupWebsite->execute();
}
diff --git a/app/code/Magento/CatalogRule/Model/Rule.php b/app/code/Magento/CatalogRule/Model/Rule.php
index 82b3fd228002e..1eca8469db1c6 100644
--- a/app/code/Magento/CatalogRule/Model/Rule.php
+++ b/app/code/Magento/CatalogRule/Model/Rule.php
@@ -31,26 +31,28 @@
use Magento\Framework\Model\Context;
use Magento\Framework\Model\ResourceModel\AbstractResource;
use Magento\Framework\Model\ResourceModel\Iterator;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Framework\Registry;
use Magento\Framework\Serialize\Serializer\Json;
use Magento\Framework\Stdlib\DateTime;
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
+use Magento\Rule\Model\AbstractModel;
use Magento\Store\Model\StoreManagerInterface;
/**
* Catalog Rule data model
*
- * @method \Magento\CatalogRule\Model\Rule setFromDate(string $value)
- * @method \Magento\CatalogRule\Model\Rule setToDate(string $value)
- * @method \Magento\CatalogRule\Model\Rule setCustomerGroupIds(string $value)
+ * @method Rule setFromDate(string $value)
+ * @method Rule setToDate(string $value)
+ * @method Rule setCustomerGroupIds(string $value)
* @method string getWebsiteIds()
- * @method \Magento\CatalogRule\Model\Rule setWebsiteIds(string $value)
+ * @method Rule setWebsiteIds(string $value)
* @SuppressWarnings(PHPMD.TooManyFields)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @SuppressWarnings(PHPMD.ExcessivePublicCount)
* @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
*/
-class Rule extends \Magento\Rule\Model\AbstractModel implements RuleInterface, IdentityInterface
+class Rule extends AbstractModel implements RuleInterface, IdentityInterface, ResetAfterRequestInterface
{
/**
* Prefix of model events names
@@ -405,9 +407,16 @@ public function callbackValidateProduct($args)
$product->setData($args['row']);
$websites = $this->_getWebsitesMap();
+ $websiteIds = $this->getWebsiteIds();
+ if (!is_array($websiteIds)) {
+ $websiteIds = explode(',', $websiteIds);
+ }
$results = [];
foreach ($websites as $websiteId => $defaultStoreId) {
+ if (!in_array($websiteId, $websiteIds)) {
+ continue;
+ }
$product->setStoreId($defaultStoreId);
$results[$websiteId] = $this->getConditions()->validate($product);
}
@@ -910,4 +919,12 @@ public function clearPriceRulesData(): void
{
self::$_priceRulesData = [];
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ self::$_priceRulesData = [];
+ }
}
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogPriceRuleByProductAttributeTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogPriceRuleByProductAttributeTest.xml
index 724664917fecc..aded63210e3fc 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogPriceRuleByProductAttributeTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogPriceRuleByProductAttributeTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForDownloadableProductTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForDownloadableProductTest.xml
index d3a349fb3a19c..ac57c49c74993 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForDownloadableProductTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForDownloadableProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForFixedBundleProductWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForFixedBundleProductWithCustomOptionsTest.xml
index ee32fa1901f43..114c579033919 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForFixedBundleProductWithCustomOptionsTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForFixedBundleProductWithCustomOptionsTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml
index ce8d2dd1507fb..d8a35834cb929 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleTest.xml
index 2be55819a1004..4e8830176dc65 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleWithInvalidDataTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleWithInvalidDataTest.xml
index 77228dde8797f..c80b2b56fc35c 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleWithInvalidDataTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleWithInvalidDataTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml
index 1d4b21cb04a60..b97191a37c656 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateInactiveCatalogPriceRuleTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminMarketingCatalogPriceRuleNavigateMenuTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminMarketingCatalogPriceRuleNavigateMenuTest.xml
index c1d98a7e7128e..8a354a29c1ee2 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminMarketingCatalogPriceRuleNavigateMenuTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminMarketingCatalogPriceRuleNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml
index ba446380a4f63..b3e4b88e93366 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml
index c127f19db3749..1e0aa54d545ae 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml
index a616a7ab172f1..022b7a7aec274 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleToSimpleProductNotCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleToSimpleProductNotCustomOptionsTest.xml
index c3078a052116a..298ce68731e1d 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleToSimpleProductNotCustomOptionsTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleToSimpleProductNotCustomOptionsTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml
index 703b3655480ce..c13c85d34792a 100644
--- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml
+++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/RuleTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/RuleTest.php
index 45c9db38c5dd3..c8a4c7a59e344 100644
--- a/app/code/Magento/CatalogRule/Test/Unit/Model/RuleTest.php
+++ b/app/code/Magento/CatalogRule/Test/Unit/Model/RuleTest.php
@@ -207,15 +207,15 @@ public function testCallbackValidateProduct($validate): void
'updated_at' => '2014-06-25 14:37:15'
];
$this->storeManager->expects($this->any())->method('getWebsites')->with(false)
- ->willReturn([$this->websiteModel, $this->websiteModel]);
+ ->willReturn([$this->websiteModel, $this->websiteModel, $this->websiteModel]);
$this->websiteModel
->method('getId')
- ->willReturnOnConsecutiveCalls('1', '2');
+ ->willReturnOnConsecutiveCalls('1', '2', '3');
$this->websiteModel->expects($this->any())->method('getDefaultStore')
->willReturn($this->storeModel);
$this->storeModel
->method('getId')
- ->willReturnOnConsecutiveCalls('1', '2');
+ ->willReturnOnConsecutiveCalls('1', '2', '3');
$this->combineFactory->expects($this->any())->method('create')
->willReturn($this->condition);
$this->condition->expects($this->any())->method('validate')
@@ -224,12 +224,14 @@ public function testCallbackValidateProduct($validate): void
$this->productModel->expects($this->any())->method('getId')
->willReturn(1);
+ $this->rule->setWebsiteIds('1,2');
$this->rule->callbackValidateProduct($args);
$matchingProducts = $this->rule->getMatchingProductIds();
foreach ($matchingProducts['1'] as $matchingRules) {
$this->assertEquals($validate, $matchingRules);
}
+ $this->assertNull($matchingProducts['1']['3'] ?? null);
}
/**
diff --git a/app/code/Magento/CatalogRuleGraphQl/README.md b/app/code/Magento/CatalogRuleGraphQl/README.md
index 6f9761fedecbb..13a8f4a62e963 100644
--- a/app/code/Magento/CatalogRuleGraphQl/README.md
+++ b/app/code/Magento/CatalogRuleGraphQl/README.md
@@ -1,3 +1,3 @@
# CatalogRuleGraphQl
-The *Magento_CatalogRuleGraphQl* module applies catalog rules to products for GraphQL requests.
\ No newline at end of file
+The *Magento_CatalogRuleGraphQl* module applies catalog rules to products for GraphQL requests.
diff --git a/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php
index f014c6d133187..1904367ec3d66 100644
--- a/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php
+++ b/app/code/Magento/CatalogSearch/Model/Autocomplete/DataProvider.php
@@ -21,11 +21,9 @@ class DataProvider implements DataProviderInterface
/**
* Autocomplete limit
*/
- const CONFIG_AUTOCOMPLETE_LIMIT = 'catalog/search/autocomplete_limit';
+ public const CONFIG_AUTOCOMPLETE_LIMIT = 'catalog/search/autocomplete_limit';
/**
- * Query factory
- *
* @var QueryFactory
*/
protected $queryFactory;
@@ -38,8 +36,6 @@ class DataProvider implements DataProviderInterface
protected $itemFactory;
/**
- * Limit
- *
* @var int
*/
protected $limit;
@@ -68,8 +64,12 @@ public function __construct(
*/
public function getItems()
{
- $collection = $this->getSuggestCollection();
$query = $this->queryFactory->get()->getQueryText();
+ if (!$query) {
+ return [];
+ }
+
+ $collection = $this->getSuggestCollection();
$result = [];
foreach ($collection as $item) {
$resultItem = $this->itemFactory->create([
diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php
index 9b66606d37a9e..d29928306f24b 100644
--- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php
+++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php
@@ -219,10 +219,23 @@ public function __construct(
->get(DefaultFilterStrategyApplyChecker::class);
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ parent::_resetState();
+ $this->queryText = null;
+ $this->search = null;
+ $this->searchCriteriaBuilder = null;
+ $this->searchResult = null;
+ $this->filterBuilder = null;
+ $this->searchOrders = null;
+ }
+
/**
* Get search.
*
- * @deprecated 100.1.0
* @return \Magento\Search\Api\SearchInterface
*/
private function getSearch()
@@ -237,6 +250,7 @@ private function getSearch()
* Test search.
*
* @deprecated 100.1.0
+ * @see __construct
* @param \Magento\Search\Api\SearchInterface $object
* @return void
* @since 100.1.0
@@ -249,7 +263,6 @@ public function setSearch(\Magento\Search\Api\SearchInterface $object)
/**
* Set search criteria builder.
*
- * @deprecated 100.1.0
* @return \Magento\Framework\Api\Search\SearchCriteriaBuilder
*/
private function getSearchCriteriaBuilder()
@@ -265,6 +278,7 @@ private function getSearchCriteriaBuilder()
* Set search criteria builder.
*
* @deprecated 100.1.0
+ * @see __construct
* @param \Magento\Framework\Api\Search\SearchCriteriaBuilder $object
* @return void
* @since 100.1.0
@@ -277,7 +291,6 @@ public function setSearchCriteriaBuilder(\Magento\Framework\Api\Search\SearchCri
/**
* Get filter builder.
*
- * @deprecated 100.1.0
* @return \Magento\Framework\Api\FilterBuilder
*/
private function getFilterBuilder()
@@ -292,6 +305,7 @@ private function getFilterBuilder()
* Set filter builder.
*
* @deprecated 100.1.0
+ * @see __construct
* @param \Magento\Framework\Api\FilterBuilder $object
* @return void
* @since 100.1.0
diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php
index 7e9be408a3850..10e72e0155ff3 100644
--- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php
+++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php
@@ -23,22 +23,16 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection
private $indexUsageEnforcements;
/**
- * Attribute collection
- *
* @var array
*/
protected $_attributesCollection;
/**
- * Search query
- *
* @var string
*/
protected $_searchQuery;
/**
- * Attribute collection factory
- *
* @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory
*/
protected $_attributeCollectionFactory;
@@ -119,6 +113,16 @@ public function __construct(
$this->indexUsageEnforcements = $indexUsageEnforcements;
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ parent::_resetState();
+ $this->_attributesCollection = null;
+ $this->_searchQuery = null;
+ }
+
/**
* Add search query filter
*
@@ -240,6 +244,8 @@ private function isIndexExists(string $table, string $index) : bool
* @param mixed $query
* @param bool $searchOnlyInCurrentStore Search only in current store or in all stores
* @return string
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
protected function _getSearchEntityIdsSql($query, $searchOnlyInCurrentStore = true)
{
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml
index 72358cd002f44..c3374d4b6967a 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml
index 6361c076ce177..1163f90989eb0 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCreateSearchTermEntityTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml
index c376456a64ac4..a0d3d60dc5a7e 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminDeleteSearchTermTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminMarketingSearchTermsNavigateMenuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminMarketingSearchTermsNavigateMenuTest.xml
index 3719899d39ec6..dd429fcec645c 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminMarketingSearchTermsNavigateMenuTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminMarketingSearchTermsNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminReportsSearchTermsNavigateMenuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminReportsSearchTermsNavigateMenuTest.xml
index 96b9714a343ca..972ecd669a63e 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminReportsSearchTermsNavigateMenuTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminReportsSearchTermsNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml
index 911ed45b82f7d..8006ea9d34c7a 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml
@@ -11,6 +11,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml
index 9312eeb1c1070..6e21c8f213756 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml
@@ -11,6 +11,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml
index 02e8e30f37782..99e90ed63a417 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml
@@ -11,6 +11,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml
index 7b0835302cbdf..e6d32b5e9cc78 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml
@@ -11,6 +11,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml
index 120f2fff76333..cdfaa88b998f1 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml
index fd77b1f6b4aec..dc299476bee40 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml
index 5a487e3f0fd41..d8fd296cf7252 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml
index c4461eb31039a..b75cb4d2ca225 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml
index b52cd9fc43882..f1e14d9f58db8 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml
index 6f21f79145a30..df35689ba655b 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml
index ad817a03c2e22..11c03dbb22e06 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBy128CharQueryTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBy128CharQueryTest.xml
index b2b6bbb473091..b85c92c2a708b 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBy128CharQueryTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBy128CharQueryTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameTest.xml
index 62f4e3da1059c..6b573ac37a2e7 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameWithSpecialCharsTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameWithSpecialCharsTest.xml
index 7f21972ce801b..3da958b341307 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameWithSpecialCharsTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductByNameWithSpecialCharsTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml
index 1e777ab0ab66b..dae971667184c 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuAndDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuAndDescriptionTest.xml
index b2b4ef9cc4782..db6c10c7e45f0 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuAndDescriptionTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuAndDescriptionTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuTest.xml
index 45cec0a899361..af72b38ebb0fe 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPartialSkuTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceToTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceToTest.xml
index 33dff8aefa334..4a24b0aa83d49 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceToTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByPriceToTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByShortDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByShortDescriptionTest.xml
index c4622d02a5152..1130368ba50a3 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByShortDescriptionTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchByShortDescriptionTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchBySkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchBySkuTest.xml
index ca5e237099681..c70cd5802aa8a 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchBySkuTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchBySkuTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml
index 7508830e0f050..05b19a53c6b74 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchWithoutEnteringDataTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchWithoutEnteringDataTest.xml
index e1f297b6dffed..174a6f2985468 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchWithoutEnteringDataTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchWithoutEnteringDataTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml
index cceac0475aa78..e7940a50bb809 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontCheckUnableAdvancedSearchWithNegativePriceTest.xml
@@ -14,6 +14,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml
index d54090576128f..11deec4a95d6d 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml
index cfff1d1b3bdc1..66f3ba291f408 100644
--- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml
+++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Autocomplete/DataProviderTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Autocomplete/DataProviderTest.php
index 18d18352b8d89..5282212cacb90 100644
--- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Autocomplete/DataProviderTest.php
+++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Autocomplete/DataProviderTest.php
@@ -147,4 +147,17 @@ private function buildCollection(array $data)
->method('getIterator')
->willReturn(new \ArrayIterator($collectionData));
}
+
+ public function testGetItemsWithEmptyQueryText()
+ {
+ $this->query->expects($this->once())
+ ->method('getQueryText')
+ ->willReturn('');
+ $this->query->expects($this->never())
+ ->method('getSuggestCollection');
+ $this->itemFactory->expects($this->never())
+ ->method('create');
+ $result = $this->model->getItems();
+ $this->assertEmpty($result);
+ }
}
diff --git a/app/code/Magento/CatalogSearch/etc/search_request.xml b/app/code/Magento/CatalogSearch/etc/search_request.xml
index 376e4ced4d5ac..9a84bf4c458d5 100644
--- a/app/code/Magento/CatalogSearch/etc/search_request.xml
+++ b/app/code/Magento/CatalogSearch/etc/search_request.xml
@@ -67,7 +67,7 @@
-
+
diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenCategoriesProvider.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenCategoriesProvider.php
index 569de155c6e3a..6b6f68d0bdc5d 100644
--- a/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenCategoriesProvider.php
+++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/ChildrenCategoriesProvider.php
@@ -6,8 +6,9 @@
namespace Magento\CatalogUrlRewrite\Model\Category;
use Magento\Catalog\Model\Category;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
-class ChildrenCategoriesProvider
+class ChildrenCategoriesProvider implements ResetAfterRequestInterface
{
/**
* @var array
@@ -15,6 +16,8 @@ class ChildrenCategoriesProvider
protected $childrenIds = [];
/**
+ * Get Children Categories
+ *
* @param \Magento\Catalog\Model\Category $category
* @param boolean $recursive
* @return \Magento\Catalog\Model\Category[]
@@ -29,6 +32,8 @@ public function getChildren(Category $category, $recursive = false)
}
/**
+ * Retrieve category children ids
+ *
* @param \Magento\Catalog\Model\Category $category
* @param boolean $recursive
* @return int[]
@@ -50,4 +55,12 @@ public function getChildrenIds(Category $category, $recursive = false)
}
return $this->childrenIds[$cacheKey];
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->childrenIds = [];
+ }
}
diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryHashMap.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryHashMap.php
index cb9f3fecb4bca..3948e1ca3f180 100644
--- a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryHashMap.php
+++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryHashMap.php
@@ -8,11 +8,12 @@
use Magento\Catalog\Model\ResourceModel\CategoryFactory;
use Magento\Catalog\Model\CategoryRepository;
use Magento\Catalog\Api\Data\CategoryInterface;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Map that holds data for category ids and its subcategories ids
*/
-class DataCategoryHashMap implements HashMapInterface
+class DataCategoryHashMap implements HashMapInterface, ResetAfterRequestInterface
{
/**
* @var int[]
@@ -57,7 +58,7 @@ public function getAllData($categoryId)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function getData($categoryId, $key)
{
@@ -86,10 +87,18 @@ private function getAllCategoryChildrenIds(CategoryInterface $category)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function resetData($categoryId)
{
unset($this->hashMap[$categoryId]);
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->hashMap = [];
+ }
}
diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUsedInProductsHashMap.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUsedInProductsHashMap.php
index a70f533fbe5ba..9aa560f2aa3f5 100644
--- a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUsedInProductsHashMap.php
+++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataCategoryUsedInProductsHashMap.php
@@ -6,11 +6,12 @@
namespace Magento\CatalogUrlRewrite\Model\Map;
use Magento\Framework\App\ResourceConnection;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Map that holds data for categories used by products found in root category
*/
-class DataCategoryUsedInProductsHashMap implements HashMapInterface
+class DataCategoryUsedInProductsHashMap implements HashMapInterface, ResetAfterRequestInterface
{
/**
* @var int[]
@@ -40,8 +41,7 @@ public function __construct(
}
/**
- * Returns an array of product ids for all DataProductHashMap list,
- * that occur in other categories not part of DataCategoryHashMap list
+ * Returns product ids for all DataProductHashMap list from other categories not part of DataCategoryHashMap list
*
* @param int $categoryId
* @return array
@@ -81,7 +81,7 @@ public function getAllData($categoryId)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function getData($categoryId, $key)
{
@@ -93,7 +93,7 @@ public function getData($categoryId, $key)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function resetData($categoryId)
{
@@ -101,4 +101,12 @@ public function resetData($categoryId)
$this->hashMapPool->resetMap(DataCategoryHashMap::class, $categoryId);
unset($this->hashMap[$categoryId]);
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->hashMap = [];
+ }
}
diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductHashMap.php b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductHashMap.php
index 39e4c1f0f2012..44f183b5de8b7 100644
--- a/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductHashMap.php
+++ b/app/code/Magento/CatalogUrlRewrite/Model/Map/DataProductHashMap.php
@@ -7,11 +7,12 @@
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\Framework\App\ResourceConnection;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Map that holds data for products ids from a category and subcategories
*/
-class DataProductHashMap implements HashMapInterface
+class DataProductHashMap implements HashMapInterface, ResetAfterRequestInterface
{
/**
* @var int[]
@@ -81,7 +82,7 @@ public function getAllData($categoryId)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function getData($categoryId, $key)
{
@@ -93,11 +94,19 @@ public function getData($categoryId, $key)
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function resetData($categoryId)
{
$this->hashMapPool->resetMap(DataCategoryHashMap::class, $categoryId);
unset($this->hashMap[$categoryId]);
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->hashMap = [];
+ }
}
diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Product/GetProductUrlRewriteDataByStore.php b/app/code/Magento/CatalogUrlRewrite/Model/Product/GetProductUrlRewriteDataByStore.php
index fbacddac1ce02..d3c032a5c26aa 100644
--- a/app/code/Magento/CatalogUrlRewrite/Model/Product/GetProductUrlRewriteDataByStore.php
+++ b/app/code/Magento/CatalogUrlRewrite/Model/Product/GetProductUrlRewriteDataByStore.php
@@ -9,12 +9,13 @@
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\CatalogUrlRewrite\Model\ResourceModel\Product\GetUrlRewriteData;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Store\Model\Store;
/**
* Product data needed for url rewrite generation locator class
*/
-class GetProductUrlRewriteDataByStore
+class GetProductUrlRewriteDataByStore implements ResetAfterRequestInterface
{
/**
* @var array
@@ -51,8 +52,10 @@ public function execute(ProductInterface $product, int $storeId): array
$storesData = $this->getUrlRewriteData->execute($product);
foreach ($storesData as $storeData) {
$this->urlRewriteData[$productId][$storeData['store_id']] = [
- 'visibility' => (int)($storeData['visibility'] ?? $storesData[Store::DEFAULT_STORE_ID]['visibility']),
- 'url_key' => $storeData['url_key'] ?? $storesData[Store::DEFAULT_STORE_ID]['url_key'],
+ 'visibility' =>
+ (int)($storeData['visibility'] ?? $storesData[Store::DEFAULT_STORE_ID]['visibility']),
+ 'url_key' =>
+ $storeData['url_key'] ?? $storesData[Store::DEFAULT_STORE_ID]['url_key'],
];
}
}
@@ -73,4 +76,12 @@ public function clearProductUrlRewriteDataCache(ProductInterface $product)
{
unset($this->urlRewriteData[$product->getId()]);
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->urlRewriteData = [];
+ }
}
diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php
index e68b38b046afd..f82c8a99ac7f6 100644
--- a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php
+++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php
@@ -161,12 +161,18 @@ public function generateForGlobalScope($productCategories, Product $product, $ro
Product::ENTITY
)) {
$mergeDataProvider->merge(
- $this->generateForSpecificStoreView($id, $productCategories, $product, $rootCategoryId)
+ $this->generateForSpecificStoreView($id, $productCategories, $product, $rootCategoryId, true)
);
} else {
$scopedProduct = $this->productRepository->getById($productId, false, $id);
$mergeDataProvider->merge(
- $this->generateForSpecificStoreView($id, $productCategories, $scopedProduct, $rootCategoryId)
+ $this->generateForSpecificStoreView(
+ $id,
+ $productCategories,
+ $scopedProduct,
+ $rootCategoryId,
+ true
+ )
);
}
}
@@ -182,12 +188,20 @@ public function generateForGlobalScope($productCategories, Product $product, $ro
* @param \Magento\Framework\Data\Collection|Category[] $productCategories
* @param \Magento\Catalog\Model\Product $product
* @param int|null $rootCategoryId
+ * @param bool $isGlobalScope
* @return \Magento\UrlRewrite\Service\V1\Data\UrlRewrite[]
+ * @throws NoSuchEntityException
*/
- public function generateForSpecificStoreView($storeId, $productCategories, Product $product, $rootCategoryId = null)
- {
+ public function generateForSpecificStoreView(
+ $storeId,
+ $productCategories,
+ Product $product,
+ $rootCategoryId = null,
+ bool $isGlobalScope = false
+ ) {
$mergeDataProvider = clone $this->mergeDataProviderPrototype;
$categories = [];
+
foreach ($productCategories as $category) {
if (!$this->isCategoryProperForGenerating($category, $storeId)) {
continue;
@@ -196,35 +210,29 @@ public function generateForSpecificStoreView($storeId, $productCategories, Produ
$categories[] = $this->getCategoryWithOverriddenUrlKey($storeId, $category);
}
- $productCategories = $this->objectRegistryFactory->create(['entities' => $categories]);
-
$mergeDataProvider->merge(
$this->canonicalUrlRewriteGenerator->generate($storeId, $product)
);
- if ($this->isCategoryRewritesEnabled()) {
- $mergeDataProvider->merge(
- $this->categoriesUrlRewriteGenerator->generate($storeId, $product, $productCategories)
- );
+ $productCategories = $this->objectRegistryFactory->create(['entities' => $categories]);
+
+ if ($isGlobalScope) {
+ $generatedUrls = $this->generateCategoryUrls((int) $storeId, $product, $productCategories);
+ } else {
+ $generatedUrls = $this->generateCategoryUrlsInStoreGroup((int) $storeId, $product, $productCategories);
}
+ $mergeDataProvider->merge(array_merge(...$generatedUrls));
$mergeDataProvider->merge(
- $this->currentUrlRewritesRegenerator->generate(
+ $this->currentUrlRewritesRegenerator->generateAnchor(
$storeId,
$product,
$productCategories,
$rootCategoryId
)
);
-
- if ($this->isCategoryRewritesEnabled()) {
- $mergeDataProvider->merge(
- $this->anchorUrlRewriteGenerator->generate($storeId, $product, $productCategories)
- );
- }
-
$mergeDataProvider->merge(
- $this->currentUrlRewritesRegenerator->generateAnchor(
+ $this->currentUrlRewritesRegenerator->generate(
$storeId,
$product,
$productCategories,
@@ -252,6 +260,65 @@ public function isCategoryProperForGenerating(Category $category, $storeId)
return false;
}
+ /**
+ * Generate category URLs for the whole store group.
+ *
+ * @param int $storeId
+ * @param Product $product
+ * @param ObjectRegistry $productCategories
+ *
+ * @return array
+ * @throws NoSuchEntityException
+ */
+ private function generateCategoryUrlsInStoreGroup(
+ int $storeId,
+ Product $product,
+ ObjectRegistry $productCategories
+ ): array {
+ $currentStore = $this->storeManager->getStore($storeId);
+ $currentGroupId = $currentStore->getStoreGroupId();
+ $storeList = $this->storeManager->getStores();
+ $generatedUrls = [];
+
+ foreach ($storeList as $store) {
+ if ($store->getStoreGroupId() === $currentGroupId && $this->isCategoryRewritesEnabled()) {
+ $groupStoreId = (int) $store->getId();
+ $generatedUrls[] = $this->generateCategoryUrls(
+ $groupStoreId,
+ $product,
+ $productCategories
+ );
+ }
+ }
+
+ return array_merge(...$generatedUrls);
+ }
+
+ /**
+ * Generate category URLs.
+ *
+ * @param int $storeId
+ * @param Product $product
+ * @param ObjectRegistry $categories
+ *
+ * @return array
+ */
+ private function generateCategoryUrls(int $storeId, Product $product, ObjectRegistry $categories): array
+ {
+ $generatedUrls[] = $this->categoriesUrlRewriteGenerator->generate(
+ $storeId,
+ $product,
+ $categories
+ );
+ $generatedUrls[] = $this->anchorUrlRewriteGenerator->generate(
+ $storeId,
+ $product,
+ $categories
+ );
+
+ return $generatedUrls;
+ }
+
/**
* Check if URL key has been changed
*
diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php
index 244aaf4d5cdc9..7b49114f9609b 100644
--- a/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php
+++ b/app/code/Magento/CatalogUrlRewrite/Observer/CategoryProcessUrlRewriteMovingObserver.php
@@ -103,13 +103,19 @@ public function execute(\Magento\Framework\Event\Observer $observer)
ScopeInterface::SCOPE_STORE,
$category->getStoreId()
);
+ $catRewritesEnabled = $this->isCategoryRewritesEnabled();
+
$category->setData('save_rewrites_history', $saveRewritesHistory);
$categoryUrlRewriteResult = $this->categoryUrlRewriteGenerator->generate($category, true);
+
+ if ($catRewritesEnabled) {
+ $productUrlRewriteResult = $this->urlRewriteHandler->generateProductUrlRewrites($category);
+ }
+
$this->urlRewriteHandler->deleteCategoryRewritesForChildren($category);
$this->urlRewriteBunchReplacer->doBunchReplace($categoryUrlRewriteResult);
- if ($this->isCategoryRewritesEnabled()) {
- $productUrlRewriteResult = $this->urlRewriteHandler->generateProductUrlRewrites($category);
+ if ($catRewritesEnabled) {
$this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult);
}
diff --git a/app/code/Magento/CatalogUrlRewrite/README.md b/app/code/Magento/CatalogUrlRewrite/README.md
index a03229147129c..9d49b22319af1 100644
--- a/app/code/Magento/CatalogUrlRewrite/README.md
+++ b/app/code/Magento/CatalogUrlRewrite/README.md
@@ -1,6 +1,6 @@
# Magento_CatalogUrlRewrite module
-This module generate url rewrite fields for catalog and product.
+This module generate url rewrite fields for catalog and product.
## Extensibility
@@ -8,4 +8,4 @@ Extension developers can interact with the Magento_CatalogUrlRewrite module. For
[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_CatalogUrlRewrite module.
-A lot of functionality in the module is on JavaScript, use [mixins](https://developer.adobe.com/commerce/frontend-core/javascript/mixins/) to extend it.
\ No newline at end of file
+A lot of functionality in the module is on JavaScript, use [mixins](https://developer.adobe.com/commerce/frontend-core/javascript/mixins/) to extend it.
diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminCategoryWithRestrictedUrlKeyNotCreatedTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminCategoryWithRestrictedUrlKeyNotCreatedTest.xml
index e1b59c07d187a..203e653cec882 100644
--- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminCategoryWithRestrictedUrlKeyNotCreatedTest.xml
+++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminCategoryWithRestrictedUrlKeyNotCreatedTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminRewriteProductWithTwoStoreTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminRewriteProductWithTwoStoreTest.xml
index d3471e0e4c0b0..4d0e2321b7952 100644
--- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminRewriteProductWithTwoStoreTest.xml
+++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminRewriteProductWithTwoStoreTest.xml
@@ -13,6 +13,7 @@
+
diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml
index 26996223417b7..c05ef4c15e87f 100644
--- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml
+++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/RewriteStoreLevelUrlKeyOfChildCategoryTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCheckCategoryUrlPathForCustomStoreAfterChangingHierarchyTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCheckCategoryUrlPathForCustomStoreAfterChangingHierarchyTest.xml
index 749f713c1f34f..01cd451cf5e0a 100644
--- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCheckCategoryUrlPathForCustomStoreAfterChangingHierarchyTest.xml
+++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCheckCategoryUrlPathForCustomStoreAfterChangingHierarchyTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php
index e6a99bddcbc15..16cf430ac3307 100644
--- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php
+++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php
@@ -166,6 +166,11 @@ public function testGenerationForGlobalScope()
$product = $this->createMock(Product::class);
$product->expects($this->any())->method('getStoreId')->willReturn(null);
$product->expects($this->any())->method('getStoreIds')->willReturn([1]);
+ $store = $this->getMockBuilder(Store::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $store->expects($this->any())->method('getStoreGroupId')->willReturn(1);
+ $this->storeManager->expects($this->any())->method('getStores')->willReturn([$store]);
$this->storeViewService->expects($this->once())->method('doesEntityHaveOverriddenUrlKeyForStore')
->willReturn(true);
$this->initObjectRegistryFactory([]);
@@ -211,6 +216,11 @@ public function testGenerationForSpecificStore()
$product = $this->createMock(Product::class);
$product->expects($this->any())->method('getStoreId')->willReturn(1);
$product->expects($this->never())->method('getStoreIds');
+ $store = $this->getMockBuilder(Store::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $store->expects($this->any())->method('getStoreGroupId')->willReturn(1);
+ $this->storeManager->expects($this->any())->method('getStores')->willReturn([$store]);
$this->categoryMock->expects($this->any())->method('getParentIds')
->willReturn(['root-id', $storeRootCategoryId]);
$this->categoryMock->expects($this->any())->method('getId')->willReturn($category_id);
diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php
index 843fb53914fee..7887995db956a 100644
--- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php
+++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Observer/CategoryProcessUrlRewriteMovingObserverTest.php
@@ -89,14 +89,15 @@ protected function setUp(): void
* Test category process rewrite url by changing the parent
*
* @return void
+ * @dataProvider getCategoryRewritesConfigProvider
*/
- public function testCategoryProcessUrlRewriteAfterMovingWithChangedParentId()
+ public function testCategoryProcessUrlRewriteAfterMovingWithChangedParentId(bool $isCatRewritesEnabled)
{
/** @var Observer|MockObject $observerMock */
$observerMock = $this->createMock(Observer::class);
$eventMock = $this->getMockBuilder(Event::class)
->disableOriginalConstructor()
- ->setMethods(['getCategory'])
+ ->addMethods(['getCategory'])
->getMock();
$categoryMock = $this->createPartialMock(
Category::class,
@@ -108,17 +109,32 @@ public function testCategoryProcessUrlRewriteAfterMovingWithChangedParentId()
]
);
+ $observerMock->expects($this->once())->method('getEvent')->willReturn($eventMock);
+ $eventMock->expects($this->once())->method('getCategory')->willReturn($categoryMock);
$categoryMock->expects($this->once())->method('dataHasChangedFor')->with('parent_id')
->willReturn(true);
- $eventMock->expects($this->once())->method('getCategory')->willReturn($categoryMock);
- $observerMock->expects($this->once())->method('getEvent')->willReturn($eventMock);
$this->scopeConfigMock->expects($this->once())->method('isSetFlag')
->with(UrlKeyRenderer::XML_PATH_SEO_SAVE_HISTORY)->willReturn(true);
- $this->scopeConfigMock->method('getValue')->willReturn(true);
+ $this->scopeConfigMock->expects($this->once())
+ ->method('getValue')
+ ->with('catalog/seo/generate_category_product_rewrites')
+ ->willReturn($isCatRewritesEnabled);
+
$this->categoryUrlRewriteGeneratorMock->expects($this->once())->method('generate')
->with($categoryMock, true)->willReturn(['category-url-rewrite']);
- $this->urlRewriteHandlerMock->expects($this->once())->method('generateProductUrlRewrites')
- ->with($categoryMock)->willReturn(['product-url-rewrite']);
+
+ if ($isCatRewritesEnabled) {
+ $this->urlRewriteHandlerMock->expects($this->once())
+ ->id('generateProductUrlRewrites')
+ ->method('generateProductUrlRewrites')
+ ->with($categoryMock)->willReturn(['product-url-rewrite']);
+ $this->urlRewriteHandlerMock->expects($this->once())
+ ->method('deleteCategoryRewritesForChildren')
+ ->after('generateProductUrlRewrites');
+ } else {
+ $this->urlRewriteHandlerMock->expects($this->once())
+ ->method('deleteCategoryRewritesForChildren');
+ }
$this->databaseMapPoolMock->expects($this->exactly(2))->method('resetMap')->willReturnSelf();
$this->observer->execute($observerMock);
@@ -135,7 +151,7 @@ public function testCategoryProcessUrlRewriteAfterMovingWithinNotChangedParent()
$observerMock = $this->createMock(Observer::class);
$eventMock = $this->getMockBuilder(Event::class)
->disableOriginalConstructor()
- ->setMethods(['getCategory'])
+ ->addMethods(['getCategory'])
->getMock();
$categoryMock = $this->createPartialMock(Category::class, ['dataHasChangedFor']);
$observerMock->expects($this->once())->method('getEvent')->willReturn($eventMock);
@@ -145,4 +161,15 @@ public function testCategoryProcessUrlRewriteAfterMovingWithinNotChangedParent()
$this->observer->execute($observerMock);
}
+
+ /**
+ * @return array
+ */
+ public function getCategoryRewritesConfigProvider(): array
+ {
+ return [
+ [true],
+ [false]
+ ];
+ }
}
diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php
index f1cec1c15d861..947237cbe084e 100644
--- a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php
+++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/CategoryUrlSuffix.php
@@ -7,6 +7,7 @@
namespace Magento\CatalogUrlRewriteGraphQl\Model\Resolver;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Store\Api\Data\StoreInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Framework\GraphQl\Config\Element\Field;
@@ -17,7 +18,7 @@
/**
* Returns the url suffix for category
*/
-class CategoryUrlSuffix implements ResolverInterface
+class CategoryUrlSuffix implements ResolverInterface, ResetAfterRequestInterface
{
/**
* System setting for the url suffix for categories
@@ -79,4 +80,12 @@ private function getCategoryUrlSuffix(int $storeId): ?string
}
return $this->categoryUrlSuffix[$storeId];
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->categoryUrlSuffix = [];
+ }
}
diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php
index db84784bab5b6..a91c7b4c966b7 100644
--- a/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php
+++ b/app/code/Magento/CatalogUrlRewriteGraphQl/Model/Resolver/ProductUrlSuffix.php
@@ -7,6 +7,7 @@
namespace Magento\CatalogUrlRewriteGraphQl\Model\Resolver;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Store\Api\Data\StoreInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Framework\GraphQl\Config\Element\Field;
@@ -17,7 +18,7 @@
/**
* Returns the url suffix for product
*/
-class ProductUrlSuffix implements ResolverInterface
+class ProductUrlSuffix implements ResolverInterface, ResetAfterRequestInterface
{
/**
* System setting for the url suffix for products
@@ -79,4 +80,12 @@ private function getProductUrlSuffix(int $storeId): ?string
}
return $this->productUrlSuffix[$storeId];
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->productUrlSuffix = [];
+ }
}
diff --git a/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php b/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php
index a8cafb034c0b0..d152b92a2d21c 100644
--- a/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php
+++ b/app/code/Magento/CatalogWidget/Model/Rule/Condition/Product.php
@@ -12,6 +12,7 @@
use Magento\Catalog\Model\ProductCategoryList;
use Magento\Catalog\Model\ResourceModel\Product\Collection;
use Magento\Framework\DB\Select;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Store\Model\Store;
/**
@@ -19,7 +20,7 @@
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
-class Product extends \Magento\Rule\Model\Condition\Product\AbstractProduct
+class Product extends \Magento\Rule\Model\Condition\Product\AbstractProduct implements ResetAfterRequestInterface
{
/**
* @var string
@@ -321,4 +322,12 @@ public function getBindArgumentValue()
)
: $value;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->joinedAttributes = [];
+ }
}
diff --git a/app/code/Magento/CatalogWidget/README.md b/app/code/Magento/CatalogWidget/README.md
index ea1951198c744..b80085640a2df 100644
--- a/app/code/Magento/CatalogWidget/README.md
+++ b/app/code/Magento/CatalogWidget/README.md
@@ -1,4 +1,5 @@
# CatalogWidget
**CatalogWidget** contains various widgets that extend Catalog module functionality:
+
- Product List widget provides widget that contains product list created using rule based filter.
diff --git a/app/code/Magento/Checkout/Model/Session.php b/app/code/Magento/Checkout/Model/Session.php
index 0addbf069cba3..3a2beb3b4371c 100644
--- a/app/code/Magento/Checkout/Model/Session.php
+++ b/app/code/Magento/Checkout/Model/Session.php
@@ -24,7 +24,7 @@
*/
class Session extends \Magento\Framework\Session\SessionManager
{
- const CHECKOUT_STATE_BEGIN = 'begin';
+ public const CHECKOUT_STATE_BEGIN = 'begin';
/**
* Quote instance
@@ -99,12 +99,12 @@ class Session extends \Magento\Framework\Session\SessionManager
protected $customerRepository;
/**
- * @param QuoteIdMaskFactory
+ * @var QuoteIdMaskFactory
*/
protected $quoteIdMaskFactory;
/**
- * @param bool
+ * @var bool
*/
protected $isQuoteMasked;
@@ -186,6 +186,19 @@ public function __construct(
->get(LoggerInterface::class);
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ parent::_resetState();
+ $this->_quote = null;
+ $this->_customer = null;
+ $this->_loadInactive = false;
+ $this->isLoading = false;
+ $this->_order = null;
+ }
+
/**
* Set customer data.
*
diff --git a/app/code/Magento/Checkout/Model/ShippingInformationManagement.php b/app/code/Magento/Checkout/Model/ShippingInformationManagement.php
index f397a8ddc9cf1..f08c48c55efa1 100644
--- a/app/code/Magento/Checkout/Model/ShippingInformationManagement.php
+++ b/app/code/Magento/Checkout/Model/ShippingInformationManagement.php
@@ -3,6 +3,7 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
namespace Magento\Checkout\Model;
@@ -39,60 +40,62 @@ class ShippingInformationManagement implements ShippingInformationManagementInte
/**
* @var PaymentMethodManagementInterface
*/
- protected $paymentMethodManagement;
+ protected PaymentMethodManagementInterface $paymentMethodManagement;
/**
* @var PaymentDetailsFactory
*/
- protected $paymentDetailsFactory;
+ protected PaymentDetailsFactory $paymentDetailsFactory;
/**
* @var CartTotalRepositoryInterface
*/
- protected $cartTotalsRepository;
+ protected CartTotalRepositoryInterface $cartTotalsRepository;
/**
* @var CartRepositoryInterface
*/
- protected $quoteRepository;
-
+ protected CartRepositoryInterface $quoteRepository;
/**
* @var Logger
*/
- protected $logger;
+ protected Logger $logger;
/**
* @var QuoteAddressValidator
*/
- protected $addressValidator;
+ protected QuoteAddressValidator $addressValidator;
/**
* @var AddressRepositoryInterface
* @deprecated 100.2.0
+ * @see AddressRepositoryInterface
*/
- protected $addressRepository;
+ protected AddressRepositoryInterface $addressRepository;
/**
* @var ScopeConfigInterface
* @deprecated 100.2.0
+ * @see ScopeConfigInterface
*/
- protected $scopeConfig;
+ protected ScopeConfigInterface $scopeConfig;
/**
* @var TotalsCollector
* @deprecated 100.2.0
+ * @see TotalsCollector
*/
- protected $totalsCollector;
+ protected TotalsCollector $totalsCollector;
/**
* @var CartExtensionFactory
*/
- private $cartExtensionFactory;
+ private CartExtensionFactory $cartExtensionFactory;
/**
* @var ShippingAssignmentFactory
*/
- protected $shippingAssignmentFactory;
+ protected ShippingAssignmentFactory $shippingAssignmentFactory;
/**
* @var ShippingFactory
@@ -262,8 +265,11 @@ protected function validateQuote(Quote $quote): void
* @param string $method
* @return CartInterface
*/
- private function prepareShippingAssignment(CartInterface $quote, AddressInterface $address, $method): CartInterface
- {
+ private function prepareShippingAssignment(
+ CartInterface $quote,
+ AddressInterface $address,
+ string $method
+ ): CartInterface {
$cartExtension = $quote->getExtensionAttributes();
if ($cartExtension === null) {
$cartExtension = $this->cartExtensionFactory->create();
diff --git a/app/code/Magento/Checkout/README.md b/app/code/Magento/Checkout/README.md
index 942e35ec4d772..d4d45b9ea66fc 100644
--- a/app/code/Magento/Checkout/README.md
+++ b/app/code/Magento/Checkout/README.md
@@ -1,20 +1,23 @@
# Magento_Checkout module
+
Magento\Checkout module allows merchant to register sale transaction with the customer. Module implements consumer flow
that includes such actions like adding products to cart, providing shipping and billing information and confirming
the purchase.
#### Observer
+
This module observes the following events
`etc/events.xml`
- `sales_quote_save_after` event in
+ `sales_quote_save_after` event in
`Magento\Checkout\Observer\SalesQuoteSaveAfterObserver` file.
`/etc/frontend/events.xml`
`customer_login` event in `Magento\Checkout\Observer\LoadCustomerQuoteObserver`
file.
`customer_logout` event in `Magento\Checkout\Observer\UnsetAllObserver`
- ### Layouts
- The module interacts with the following layout handles in the
+### Layouts
+
+ The module interacts with the following layout handles in the
`view/frontend/layout`
`catalog_category_view`
`catalog_product_view`
@@ -30,4 +33,4 @@ the purchase.
`checkout_onepage_failure`
`checkout_onepage_review_item_renderers`
`checkout_onepage_success`
- `default`
\ No newline at end of file
+ `default`
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertShoppingCartIsEmptyActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertShoppingCartIsEmptyActionGroup.xml
index 5cf9f009ba375..1f8a526a22d2d 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertShoppingCartIsEmptyActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertShoppingCartIsEmptyActionGroup.xml
@@ -15,6 +15,6 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontNotCalculatedValueInShippingTotalInOrderSummaryActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontNotCalculatedValueInShippingTotalInOrderSummaryActionGroup.xml
index 1ec42033a782b..cf64a783644a4 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontNotCalculatedValueInShippingTotalInOrderSummaryActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontNotCalculatedValueInShippingTotalInOrderSummaryActionGroup.xml
@@ -14,7 +14,7 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CustomerLoggedInCheckoutFillNewBillingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CustomerLoggedInCheckoutFillNewBillingAddressActionGroup.xml
new file mode 100644
index 0000000000000..91b91e0e439b9
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CustomerLoggedInCheckoutFillNewBillingAddressActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ EXTENDS: LoggedInCheckoutFillNewBillingAddressActionGroup. Removes 'selectCountry' and 'selectState' to select state after country.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillGuestCheckoutShippingAddressFormActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillGuestCheckoutShippingAddressFormActionGroup.xml
index 527afdc26a5f4..be2c8989c67a9 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillGuestCheckoutShippingAddressFormActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/FillGuestCheckoutShippingAddressFormActionGroup.xml
@@ -13,6 +13,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewShippingAddressActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewShippingAddressActionGroup.xml
index c9f315929dcfa..8132890d7937c 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewShippingAddressActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillNewShippingAddressActionGroup.xml
@@ -20,7 +20,7 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml
index 0c97a81b1c0d7..ee06ec3ef1b4d 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionActionGroup.xml
@@ -36,6 +36,6 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionUnavailablePaymentActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionUnavailablePaymentActionGroup.xml
index 9d7c72522d003..d6c2e90df9a9e 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionUnavailablePaymentActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionUnavailablePaymentActionGroup.xml
@@ -29,7 +29,7 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionWithoutRegionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionWithoutRegionActionGroup.xml
index ada3674a07b8d..b39816588e904 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionWithoutRegionActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingSectionWithoutRegionActionGroup.xml
@@ -30,6 +30,6 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingWithMultipleStreetLinesSectionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingWithMultipleStreetLinesSectionActionGroup.xml
index 441e3571d0f55..51214a2a7c6b0 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingWithMultipleStreetLinesSectionActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/GuestCheckoutFillingShippingWithMultipleStreetLinesSectionActionGroup.xml
@@ -37,6 +37,6 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutAddNewShippingSectionWithoutRegionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutAddNewShippingSectionWithoutRegionActionGroup.xml
index 0c4cea142b4e6..4b16c0db2e09a 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutAddNewShippingSectionWithoutRegionActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutAddNewShippingSectionWithoutRegionActionGroup.xml
@@ -30,6 +30,6 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml
index 4b6680442a470..5a6aae21719f9 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml
@@ -30,6 +30,6 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/PlaceOrderWithLoggedUserActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/PlaceOrderWithLoggedUserActionGroup.xml
index 95d78777ed922..aff2bef433939 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/PlaceOrderWithLoggedUserActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/PlaceOrderWithLoggedUserActionGroup.xml
@@ -25,7 +25,7 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutClickNextOnShippingStepActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutClickNextOnShippingStepActionGroup.xml
index f13850357b182..077e4eb960294 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutClickNextOnShippingStepActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutClickNextOnShippingStepActionGroup.xml
@@ -8,11 +8,14 @@
-
+
Scrolls and clicks next on Checkout Shipping step
-
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutForwardFromShippingStepActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutForwardFromShippingStepActionGroup.xml
index 524e3f784ed3f..9b2101e2fd8f9 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutForwardFromShippingStepActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckoutForwardFromShippingStepActionGroup.xml
@@ -8,11 +8,12 @@
-
+
Clicks next on Checkout Shipping step
-
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontGuestCheckoutProceedToPaymentStepActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontGuestCheckoutProceedToPaymentStepActionGroup.xml
index a55db2b92e9c3..2d8be3ec50d69 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontGuestCheckoutProceedToPaymentStepActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontGuestCheckoutProceedToPaymentStepActionGroup.xml
@@ -15,6 +15,6 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontShippmentFromActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontShippmentFromActionGroup.xml
index bf82d4cf20b1b..ee91191a63352 100644
--- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontShippmentFromActionGroup.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontShippmentFromActionGroup.xml
@@ -9,11 +9,14 @@
-
+
Fills in the Customer details for the 'Shipping Address' section of the Storefront Checkout page. Selects 'Free Shipping'. Clicks on Next. Validates that the URL is present and correct.
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml
index 84f9a7930d40b..c3c3a5f855f4c 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml
@@ -54,5 +54,6 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml
index 13db791d3f474..95aad2a9ddf92 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingMethodsSection.xml
@@ -9,6 +9,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml
index 581c0976e6d71..edc9e029264db 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutShippingSection.xml
@@ -28,6 +28,7 @@
+
@@ -51,5 +52,6 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml
index abdb4ddac7343..e5e912af73343 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutSuccessMainSection.xml
@@ -21,5 +21,6 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml
index 9bb999d643add..2e587e3f7962b 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml
@@ -48,6 +48,8 @@
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml
index 5a065e5dead9c..55534aff2aefe 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml
@@ -58,7 +58,7 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml
index eddb7d430387c..278b1b3f36965 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderIsInProcessingStatusTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderIsInProcessingStatusTest.xml
index 12a524d2d6ad8..1f97e2230ad0f 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderIsInProcessingStatusTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderIsInProcessingStatusTest.xml
@@ -18,6 +18,7 @@
+
@@ -62,7 +63,17 @@
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithCustomStatus.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithCustomStatus.xml
index f08640d895b6b..b19c34cafd30e 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithCustomStatus.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithCustomStatus.xml
@@ -24,12 +24,6 @@
-
-
-
-
-
-
100
@@ -37,15 +31,20 @@
+
+
+
+
+
+
+
+
-
-
-
@@ -54,6 +53,8 @@
+
+
@@ -62,29 +63,31 @@
-
-
+
+
-
-
+
+
-
+
-
+
+
+
-
-
+
+
@@ -94,9 +97,20 @@
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
@@ -113,7 +127,7 @@
-
+
@@ -121,8 +135,10 @@
-
-
-
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithGeneratedInvoiceTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithGeneratedInvoiceTest.xml
index 81de8664f98e2..1a32120bad3bb 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithGeneratedInvoiceTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckZeroSubtotalOrderWithGeneratedInvoiceTest.xml
@@ -63,7 +63,17 @@
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AssertSuccessMessageAppearsAfterAddingProductToCartThatContainsOutOfStockProductTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AssertSuccessMessageAppearsAfterAddingProductToCartThatContainsOutOfStockProductTest.xml
index 979976caf78ae..e3cbad4ad2797 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/AssertSuccessMessageAppearsAfterAddingProductToCartThatContainsOutOfStockProductTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/AssertSuccessMessageAppearsAfterAddingProductToCartThatContainsOutOfStockProductTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsGuestTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsGuestTest.xml
index 9a3a590952467..e336a65e61c6a 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsGuestTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckCheckoutSuccessPageTest/CheckCheckoutSuccessPageAsGuestTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml
index 54ac1143b3573..9adbf4dd2e36e 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckNotVisibleProductInMinicartTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/ClearShoppingCartEnableDisableConfigurationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/ClearShoppingCartEnableDisableConfigurationTest.xml
index 92a4b9563ab3d..39bd49b196302 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/ClearShoppingCartEnableDisableConfigurationTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/ClearShoppingCartEnableDisableConfigurationTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml
index 824fb9e063038..cc89dbacc66ea 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/DefaultBillingAddressShouldBeCheckedOnPaymentPageTest.xml
@@ -34,7 +34,7 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml
index a30f118bd6207..efc29bb6a4e30 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.xml
index 9958b12ceaf25..467ce4c963d13 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteGroupedProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteGroupedProductFromShoppingCartTest.xml
index b82df28ebb95f..1a63544c44bc8 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteGroupedProductFromShoppingCartTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteGroupedProductFromShoppingCartTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteVirtualProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteVirtualProductFromShoppingCartTest.xml
index 39b4e66ef9f07..f37f6cba72194 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteVirtualProductFromShoppingCartTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteVirtualProductFromShoppingCartTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml
index d45fb92744544..694c3c70b7fff 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml
index 64f392d39edcb..f9ddee8284d8b 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml
@@ -18,13 +18,14 @@
+
+
560
-
@@ -40,6 +41,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml
index 138fbe5055d61..5f5b90d11d132 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml
index f6db22cbccaa8..8ccb7af5a334a 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml
@@ -18,8 +18,10 @@
+
+
560
@@ -40,6 +42,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutForErrorTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutForErrorTest.xml
index ecd1e91a62a3a..69f7dab981e50 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutForErrorTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutForErrorTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml
index e8bb89e0f112d..b3b0c993bca63 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithSignInLinkForEmailVerificationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithSignInLinkForEmailVerificationTest.xml
index 2e1c8d5a27886..ab003f05bb392 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithSignInLinkForEmailVerificationTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithSignInLinkForEmailVerificationTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml
index 68dcf6600f49a..6ef158f8f371b 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAppliedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAppliedTest.xml
index c29e19275f759..11b78553d3f90 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAppliedTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAppliedTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontGuestCustomerProductsMerged.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontGuestCustomerProductsMerged.xml
index a9d34db16b506..88eb7c2e417a9 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontGuestCustomerProductsMerged.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontGuestCustomerProductsMerged.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml
index 678929ff228c2..e1e0a307f9cd8 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.xml
index a403f928229bb..a25040497c2c7 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml
index feab5625c115d..4778a4024e4d1 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml
@@ -56,6 +56,8 @@
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressDeletedStreetAddressRemainsEmptyAfterRefreshTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressDeletedStreetAddressRemainsEmptyAfterRefreshTest.xml
index 7e142597e47b0..427cf230da742 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressDeletedStreetAddressRemainsEmptyAfterRefreshTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressDeletedStreetAddressRemainsEmptyAfterRefreshTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml
index 6b9c1f8f9b009..2e06d1533e65b 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCashOnDeliveryPaymentForSpecificCountryTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCashOnDeliveryPaymentForSpecificCountryTest.xml
index 024e1221d95e6..aa5a3511e00e0 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCashOnDeliveryPaymentForSpecificCountryTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCashOnDeliveryPaymentForSpecificCountryTest.xml
@@ -37,7 +37,7 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckAddressAddedOnCheckoutIsSavedAfterOrderIsPlacedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckAddressAddedOnCheckoutIsSavedAfterOrderIsPlacedTest.xml
index 736e045f588aa..8b4720ad7c26c 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckAddressAddedOnCheckoutIsSavedAfterOrderIsPlacedTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckAddressAddedOnCheckoutIsSavedAfterOrderIsPlacedTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest/StorefrontCartItemsCountDisplayItemsDecimalQuantitiesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest/StorefrontCartItemsCountDisplayItemsDecimalQuantitiesTest.xml
new file mode 100644
index 0000000000000..7aa0259d43621
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest/StorefrontCartItemsCountDisplayItemsDecimalQuantitiesTest.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest/StorefrontCartItemsCountDisplayItemsQuantitiesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest/StorefrontCartItemsCountDisplayItemsQuantitiesTest.xml
index 83ed32803654e..a64e667acb702 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest/StorefrontCartItemsCountDisplayItemsQuantitiesTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndCheckoutItemsCountTest/StorefrontCartItemsCountDisplayItemsQuantitiesTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml
index e31db8ee28c7f..13c65c7242f96 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml
index c68961c3e8c2b..a6368d71c28b9 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoOnOrderPageCreatedByGuestTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoOnOrderPageCreatedByGuestTest.xml
index e769d9d37286d..53b1a0938e355 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoOnOrderPageCreatedByGuestTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoOnOrderPageCreatedByGuestTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckPagerShoppingCartWithMoreThan20ProductsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckPagerShoppingCartWithMoreThan20ProductsTest.xml
index 93d1c4092c05e..386360e2cdbcb 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckPagerShoppingCartWithMoreThan20ProductsTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckPagerShoppingCartWithMoreThan20ProductsTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml
index ee32ce6d928a1..dd8675e4e8533 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml
index 743f4e0165159..f0650fb187d1c 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithEnabledMinimumOrderAmountOptionTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithEnabledMinimumOrderAmountOptionTest.xml
index acb274886a6c8..da73a2f62b96d 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithEnabledMinimumOrderAmountOptionTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithEnabledMinimumOrderAmountOptionTest.xml
@@ -67,7 +67,7 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithSpecialPriceProductsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithSpecialPriceProductsTest.xml
index 1a85bb0bee1ee..a176a7ccd27f3 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithSpecialPriceProductsTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithSpecialPriceProductsTest.xml
@@ -18,6 +18,7 @@
+
@@ -101,10 +102,14 @@
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml
index 68842ee09a855..1926637257213 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml
index a8c694f4a8436..6909ff4a9fa2a 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml
@@ -17,6 +17,7 @@
+
@@ -53,7 +54,7 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml
index f12dd6fb34827..c2645f3e4d41a 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml
@@ -60,7 +60,7 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutWithCustomerGroupTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutWithCustomerGroupTest.xml
index 28e779f802cde..266c8210c273c 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutWithCustomerGroupTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutWithCustomerGroupTest.xml
@@ -65,7 +65,7 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerLoginDuringCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerLoginDuringCheckoutTest.xml
index eb76748a81c97..d59af6328f736 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerLoginDuringCheckoutTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerLoginDuringCheckoutTest.xml
@@ -17,8 +17,10 @@
+
+
@@ -39,9 +41,9 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.xml
index d289bdc0dc8d1..97aa966aa4ad5 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteSimpleProductFromMiniShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteSimpleProductFromMiniShoppingCartTest.xml
index 2d6f36c78edf6..ddf65a0922bbf 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteSimpleProductFromMiniShoppingCartTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteSimpleProductFromMiniShoppingCartTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.xml
index 45eb3443e4205..2a51c69d4a691 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutDataPersistTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTest.xml
index da4a1b93691b6..5879131ff91a7 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontShoppingCartGuestCheckoutDisabledTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontShoppingCartGuestCheckoutDisabledTest.xml
index d6b27b73601e8..261f03308d818 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontShoppingCartGuestCheckoutDisabledTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontShoppingCartGuestCheckoutDisabledTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontMissingPagerShoppingCartWith20ProductsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontMissingPagerShoppingCartWith20ProductsTest.xml
index a5a3675ea0a0b..287c737e24e67 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontMissingPagerShoppingCartWith20ProductsTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontMissingPagerShoppingCartWith20ProductsTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml
index 2a81ff2b64186..7ded05eda92f0 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml
@@ -17,6 +17,7 @@
+
@@ -36,6 +37,7 @@
+
@@ -60,7 +62,7 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutJsValidationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutJsValidationTest.xml
index 66a4f417aed9d..98dfc2b4f26a0 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutJsValidationTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutJsValidationTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml
index 3ab3a0b4ad3f7..5cb5b375a3967 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml
@@ -22,13 +22,13 @@
10
-
+
+
-
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForRegisteredCustomerWithVirtualQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForRegisteredCustomerWithVirtualQuoteTest.xml
new file mode 100644
index 0000000000000..d4163b1dae357
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForRegisteredCustomerWithVirtualQuoteTest.xml
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRefreshPageDuringGuestCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRefreshPageDuringGuestCheckoutTest.xml
index 87eba009f5e56..83f410c7dc369 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRefreshPageDuringGuestCheckoutTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRefreshPageDuringGuestCheckoutTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest.xml
index 44bfe81b40dc0..7dddc2d238122 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontRegionUpdatesAfterChangingCountryAndLeavingRegionSelectUnselectedTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml
index f0c3a23a8d39c..b6d193dd4a6a3 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleProductQtyTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml
index afb4ff03a4fc9..291c22408ba09 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdateShoppingCartSimpleWithCustomOptionsProductQtyTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontValidateEmailOnCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontValidateEmailOnCheckoutTest.xml
index 65f5dd365b215..4a90b01a700b1 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontValidateEmailOnCheckoutTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontValidateEmailOnCheckoutTest.xml
@@ -20,6 +20,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml
index e7dd7a0db223f..e1a2a6d97b7ee 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyMapMessagePopupOnCartViewPageTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyMapMessagePopupOnCartViewPageTest.xml
index 8fc37bdaafdee..918c737f67de5 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyMapMessagePopupOnCartViewPageTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyMapMessagePopupOnCartViewPageTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyZipCodeWorkingAsPerCountryTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyZipCodeWorkingAsPerCountryTest.xml
new file mode 100644
index 0000000000000..3317a72e9f610
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyZipCodeWorkingAsPerCountryTest.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVisiblePasswordFieldForUnregisteredEmailOnCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVisiblePasswordFieldForUnregisteredEmailOnCheckoutTest.xml
index 41b5f734d0096..af275e148102f 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVisiblePasswordFieldForUnregisteredEmailOnCheckoutTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVisiblePasswordFieldForUnregisteredEmailOnCheckoutTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml
index 6e484c30fa81e..4ee8a0b1c209e 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromMiniShoppingCartEntityTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromShoppingCartTest.xml
index 97906cade542c..bfa8557698a8e 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromShoppingCartTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/UpdateProductFromShoppingCartTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/VerifyStateOptionApplicableForCheckoutFlowTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/VerifyStateOptionApplicableForCheckoutFlowTest.xml
index 6016893ed3e28..95fe1db043e1e 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/VerifyStateOptionApplicableForCheckoutFlowTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/VerifyStateOptionApplicableForCheckoutFlowTest.xml
@@ -63,7 +63,7 @@
-
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/VerifyThatOptionAllowToChooseStateIfItIsOptionalForCountryIsApplicableForCheckoutFlowTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/VerifyThatOptionAllowToChooseStateIfItIsOptionalForCountryIsApplicableForCheckoutFlowTest.xml
new file mode 100644
index 0000000000000..3167bf1df03a2
--- /dev/null
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/VerifyThatOptionAllowToChooseStateIfItIsOptionalForCountryIsApplicableForCheckoutFlowTest.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml
index 66f8e327b9d22..71bb7d1e65954 100644
--- a/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml
+++ b/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml
@@ -19,7 +19,7 @@
Use AdminCheckZeroSubtotalOrderIsInProcessingStatusTest instead
-
+
@@ -74,7 +74,17 @@
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Checkout/etc/adminhtml/system.xml b/app/code/Magento/Checkout/etc/adminhtml/system.xml
index b56566a043c3e..5bb0f37f3bc25 100644
--- a/app/code/Magento/Checkout/etc/adminhtml/system.xml
+++ b/app/code/Magento/Checkout/etc/adminhtml/system.xml
@@ -13,6 +13,11 @@
Magento_Checkout::checkout
Checkout Options
+
+ Enable Guest Checkout Login
+ Magento\Config\Model\Config\Source\Yesno
+ Enabling this setting will allow unauthenticated users to query if an e-mail address is already associated with a customer account. This can be used to enhance the checkout workflow for guests that do not realize they already have an account but comes at the cost of exposing information to unauthenticated users.
+
Enable Onepage Checkout
Magento\Config\Model\Config\Source\Yesno
@@ -23,7 +28,7 @@
Display Billing Address On
- \Magento\Checkout\Model\Adminhtml\BillingAddressDisplayOptions
+ Magento\Checkout\Model\Adminhtml\BillingAddressDisplayOptions
Maximum Number of Items to Display in Order Summary
diff --git a/app/code/Magento/Checkout/etc/config.xml b/app/code/Magento/Checkout/etc/config.xml
index eac0bd849da35..c85d68b35f714 100644
--- a/app/code/Magento/Checkout/etc/config.xml
+++ b/app/code/Magento/Checkout/etc/config.xml
@@ -9,6 +9,7 @@
+ 0
1
1
0
diff --git a/app/code/Magento/Checkout/i18n/en_US.csv b/app/code/Magento/Checkout/i18n/en_US.csv
index aa3cf0748cb0c..7e87fe3f7e009 100644
--- a/app/code/Magento/Checkout/i18n/en_US.csv
+++ b/app/code/Magento/Checkout/i18n/en_US.csv
@@ -176,6 +176,7 @@ Summary,Summary
"We'll send your order confirmation here.","We'll send your order confirmation here."
Payment,Payment
"Not yet calculated","Not yet calculated"
+"Selected shipping method is not available. Please select another shipping method for this order.","Selected shipping method is not available. Please select another shipping method for this order."
"The order was not successful!","The order was not successful!"
"Thank you for your purchase!","Thank you for your purchase!"
"Password", "Password"
diff --git a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml
index b20b4d02706f3..411726607c669 100644
--- a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml
+++ b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml
@@ -373,7 +373,7 @@
- Magento_Checkout/js/view/summary/shipping
-
- Shipping
- - Not yet calculated
+ - Selected shipping method is not available. Please select another shipping method for this order.
-
diff --git a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html
index d23e220aa9942..ab2e495730a11 100644
--- a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html
+++ b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html
@@ -38,7 +38,7 @@
-
+
diff --git a/app/code/Magento/CheckoutAgreements/README.md b/app/code/Magento/CheckoutAgreements/README.md
index 3d31bffd1b542..628bfa165013a 100644
--- a/app/code/Magento/CheckoutAgreements/README.md
+++ b/app/code/Magento/CheckoutAgreements/README.md
@@ -1,3 +1,3 @@
Magento\CheckoutAgreements module provides the ability add web store agreement that customers must accept before purchasing
products from store. The customer will need to accept the terms and conditions in the Order Review section of the
-checkout process to be able to place an order if Terms and Conditions functionality is enabled.
\ No newline at end of file
+checkout process to be able to place an order if Terms and Conditions functionality is enabled.
diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminDeleteAllTermConditionsActionGroup.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminDeleteAllTermConditionsActionGroup.xml
new file mode 100644
index 0000000000000..fec0a686d839e
--- /dev/null
+++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminDeleteAllTermConditionsActionGroup.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+ Deletes all rows one by one on the 'Terms and Conditions' page.
+
+
+
+
+
+ {{AdminTermGridSection.allTermRows}}
+ {{AdminMainActionsSection.delete}}
+ {{AdminConfirmationModalSection.ok}}
+ You deleted the condition.
+ {{AdminMessagesSection.success}}
+
+
+
+
+
diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminOpenEditPageTermsConditionsByNameActionGroup.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminOpenEditPageTermsConditionsByNameActionGroup.xml
new file mode 100644
index 0000000000000..3cddd2ebb5389
--- /dev/null
+++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminOpenEditPageTermsConditionsByNameActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Opens Edit Page of Terms and Conditions By Provided Name
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminTermsConditionsEditTermByNameActionGroup.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminTermsConditionsEditTermByNameActionGroup.xml
index fe40a92948cc5..8f2e65415ac22 100644
--- a/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminTermsConditionsEditTermByNameActionGroup.xml
+++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/AdminTermsConditionsEditTermByNameActionGroup.xml
@@ -13,7 +13,7 @@
Filters Terms and Conditions grid and opens the first result Edit page
-
+
diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/StorefrontProcessCheckoutToPaymentActionGroup.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/StorefrontProcessCheckoutToPaymentActionGroup.xml
index bc0c48142e223..0f25fcf84e4b6 100644
--- a/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/StorefrontProcessCheckoutToPaymentActionGroup.xml
+++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/ActionGroup/StorefrontProcessCheckoutToPaymentActionGroup.xml
@@ -30,6 +30,6 @@
-
+
diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Data/TermData.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Data/TermData.xml
index 0172ffc771384..5fd439c0ce244 100644
--- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Data/TermData.xml
+++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Data/TermData.xml
@@ -44,4 +44,13 @@
test_checkbox
<html>
+
+ Test name
+ Enabled
+ Text
+ Manually
+ All Store Views
+ test_checkbox
+ TestMessage
+
diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Helper/CheckoutAgreementsHelpers.php b/app/code/Magento/CheckoutAgreements/Test/Mftf/Helper/CheckoutAgreementsHelpers.php
new file mode 100644
index 0000000000000..7f0150b274f47
--- /dev/null
+++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Helper/CheckoutAgreementsHelpers.php
@@ -0,0 +1,61 @@
+getModule("\\" . MagentoWebDriver::class);
+ $webDriver = $magentoWebDriver->webDriver;
+
+ $magentoWebDriver->waitForPageLoad(30);
+ $rows = $webDriver->findElements(WebDriverBy::xpath($rowsToDelete));
+ while (!empty($rows)) {
+ $rows[0]->click();
+ $magentoWebDriver->waitForPageLoad(30);
+ $magentoWebDriver->waitForElementVisible($deleteButton, 10);
+ $magentoWebDriver->click($deleteButton);
+ $magentoWebDriver->waitForPageLoad(30);
+ $magentoWebDriver->waitForElementVisible($modalAcceptButton, 10);
+ $magentoWebDriver->click($modalAcceptButton);
+ $magentoWebDriver->waitForPageLoad(60);
+ $magentoWebDriver->waitForText($successMessage, 10, $successMessageContainer);
+ $rows = $webDriver->findElements(WebDriverBy::xpath($rowsToDelete));
+ }
+ } catch (Exception $exception) {
+ $this->fail($exception->getMessage());
+ }
+ }
+}
diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/AdminTermGridSection.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/AdminTermGridSection.xml
index 326f9dcce4320..b80a4b83c502c 100644
--- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/AdminTermGridSection.xml
+++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/AdminTermGridSection.xml
@@ -11,8 +11,11 @@
+
+
+
diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/StorefrontCheckoutAgreementsSection.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/StorefrontCheckoutAgreementsSection.xml
index cb3e98949c622..e62148ad30a94 100644
--- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/StorefrontCheckoutAgreementsSection.xml
+++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Section/StorefrontCheckoutAgreementsSection.xml
@@ -12,5 +12,6 @@
+
diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveHtmlTermEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveHtmlTermEntityTest.xml
index ad39e8105e957..9720c3784d996 100644
--- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveHtmlTermEntityTest.xml
+++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveHtmlTermEntityTest.xml
@@ -29,14 +29,13 @@
-
+
-
-
-
-
+
+
+
diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveTextTermEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveTextTermEntityTest.xml
index a90c3536ec744..f0c2bac759829 100644
--- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveTextTermEntityTest.xml
+++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateActiveTextTermEntityTest.xml
@@ -20,9 +20,7 @@
-
-
-
+
diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateDisabledTextTermEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateDisabledTextTermEntityTest.xml
index e74235dba19db..68104d4554685 100644
--- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateDisabledTextTermEntityTest.xml
+++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateDisabledTextTermEntityTest.xml
@@ -32,10 +32,9 @@
-
-
-
-
+
+
+
diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateEnabledTextTermOnMultishippingEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateEnabledTextTermOnMultishippingEntityTest.xml
index 3eb1e9dd02c9d..2484efcda6fd5 100644
--- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateEnabledTextTermOnMultishippingEntityTest.xml
+++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminCreateEnabledTextTermOnMultishippingEntityTest.xml
@@ -33,7 +33,7 @@
-
+
@@ -41,10 +41,9 @@
-
-
-
-
+
+
+
diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminDeleteActiveTextTermEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminDeleteActiveTextTermEntityTest.xml
index 175d5eb621501..0e7074d1a1803 100644
--- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminDeleteActiveTextTermEntityTest.xml
+++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminDeleteActiveTextTermEntityTest.xml
@@ -18,6 +18,7 @@
+
@@ -32,14 +33,14 @@
-
+
-
+
diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminStoresTermsAndConditionsNavigateMenuTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminStoresTermsAndConditionsNavigateMenuTest.xml
index 83ce4df697e46..53c2b8663bd58 100644
--- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminStoresTermsAndConditionsNavigateMenuTest.xml
+++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminStoresTermsAndConditionsNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledHtmlTermEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledHtmlTermEntityTest.xml
index f9d60796d0424..e0fcd2643606b 100644
--- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledHtmlTermEntityTest.xml
+++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledHtmlTermEntityTest.xml
@@ -32,10 +32,9 @@
-
-
-
-
+
+
+
diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledTextTermEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledTextTermEntityTest.xml
index 198a9fe3fc7b4..18f52b197b3fe 100644
--- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledTextTermEntityTest.xml
+++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateDisabledTextTermEntityTest.xml
@@ -21,9 +21,7 @@
-
-
-
+
diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateEnabledTextTermEntityTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateEnabledTextTermEntityTest.xml
index f82840bc07c7d..1613bab85edba 100644
--- a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateEnabledTextTermEntityTest.xml
+++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/AdminUpdateEnabledTextTermEntityTest.xml
@@ -18,11 +18,10 @@
+
-
-
-
+
diff --git a/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/StoreFrontManualTermsAndConditionsTest.xml b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/StoreFrontManualTermsAndConditionsTest.xml
new file mode 100644
index 0000000000000..9b7670dc54bba
--- /dev/null
+++ b/app/code/Magento/CheckoutAgreements/Test/Mftf/Test/StoreFrontManualTermsAndConditionsTest.xml
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Cms/Block/Adminhtml/Block/Widget/Chooser.php b/app/code/Magento/Cms/Block/Adminhtml/Block/Widget/Chooser.php
index 86976f6c912e8..897ce651146b8 100644
--- a/app/code/Magento/Cms/Block/Adminhtml/Block/Widget/Chooser.php
+++ b/app/code/Magento/Cms/Block/Adminhtml/Block/Widget/Chooser.php
@@ -100,7 +100,7 @@ public function getRowClickCallback()
$js = '
function (grid, event) {
var trElement = Event.findElement(event, "tr");
- var blockId = trElement.down("td").innerHTML.replace(/^\s+|\s+$/g,"");
+ var blockId = trElement.down("td").next().next().innerHTML.replace(/^\s+|\s+$/g,"");
var blockTitle = trElement.down("td").next().innerHTML;
' .
$chooserJsObject .
diff --git a/app/code/Magento/Cms/Controller/Noroute/Index.php b/app/code/Magento/Cms/Controller/Noroute/Index.php
index b30beae73dce1..6eeb80be375e4 100644
--- a/app/code/Magento/Cms/Controller/Noroute/Index.php
+++ b/app/code/Magento/Cms/Controller/Noroute/Index.php
@@ -4,17 +4,21 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\Cms\Controller\Noroute;
+use Magento\Framework\Controller\Result\ForwardFactory;
+
/**
* @SuppressWarnings(PHPMD.AllPurposeAction)
*/
class Index extends \Magento\Framework\App\Action\Action
{
/**
- * @var \Magento\Framework\Controller\Result\ForwardFactory
+ * @var ForwardFactory
*/
- protected $resultForwardFactory;
+ protected ForwardFactory $resultForwardFactory;
/**
* @param \Magento\Framework\App\Action\Context $context
@@ -48,6 +52,7 @@ public function execute()
if ($resultPage) {
$resultPage->setStatusHeader(404, '1.1', 'Not Found');
$resultPage->setHeader('Status', '404 File not found');
+ $resultPage->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0', true);
return $resultPage;
} else {
/** @var \Magento\Framework\Controller\Result\Forward $resultForward */
diff --git a/app/code/Magento/Cms/Model/Page/IdentityMap.php b/app/code/Magento/Cms/Model/Page/IdentityMap.php
index 249010fbf90ce..ba26f0520c567 100644
--- a/app/code/Magento/Cms/Model/Page/IdentityMap.php
+++ b/app/code/Magento/Cms/Model/Page/IdentityMap.php
@@ -8,11 +8,12 @@
namespace Magento\Cms\Model\Page;
use Magento\Cms\Model\Page;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Identity map of loaded pages.
*/
-class IdentityMap
+class IdentityMap implements ResetAfterRequestInterface
{
/**
* @var Page[]
@@ -69,4 +70,12 @@ public function clear(): void
{
$this->pages = [];
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->pages = [];
+ }
}
diff --git a/app/code/Magento/Cms/Model/Page/TargetUrlBuilder.php b/app/code/Magento/Cms/Model/Page/TargetUrlBuilder.php
new file mode 100644
index 0000000000000..c25a0b58c9c9d
--- /dev/null
+++ b/app/code/Magento/Cms/Model/Page/TargetUrlBuilder.php
@@ -0,0 +1,53 @@
+frontendUrlBuilder = $frontendUrlBuilder;
+ }
+
+ /**
+ * Get target URL
+ *
+ * @param string $routePath
+ * @param string $store
+ * @return string
+ */
+ public function process(string $routePath, string $store): string
+ {
+ return $this->frontendUrlBuilder->getUrl(
+ $routePath,
+ [
+ '_current' => false,
+ '_nosid' => true,
+ '_query' => [
+ StoreManagerInterface::PARAM_NAME => $store
+ ]
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/Cms/Model/Page/TargetUrlBuilderInterface.php b/app/code/Magento/Cms/Model/Page/TargetUrlBuilderInterface.php
new file mode 100644
index 0000000000000..2ac8d5d3379ea
--- /dev/null
+++ b/app/code/Magento/Cms/Model/Page/TargetUrlBuilderInterface.php
@@ -0,0 +1,23 @@
+
+
+
+
+
+ Assert content param with value on CMS page.
+
+
+
+
+
+
+
+
+ {$grabContent}
+ {{param}}="{{value}}"
+
+
+
diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminClickSelectBlockActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminClickSelectBlockActionGroup.xml
new file mode 100644
index 0000000000000..7ff5cb3dabecd
--- /dev/null
+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminClickSelectBlockActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+ Click on Select Block button.
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSelectBlockOnGridActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSelectBlockOnGridActionGroup.xml
new file mode 100644
index 0000000000000..0afce4f1dc222
--- /dev/null
+++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSelectBlockOnGridActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+ Selects block on grid and click insert widget button.
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml
index bb276b2adb0de..de70c5706360a 100644
--- a/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Section/StorefrontCMSPageSection.xml
@@ -16,5 +16,6 @@
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml
index 5be91f61e1e1e..c9ef757ca7477 100644
--- a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml
@@ -48,5 +48,8 @@
+
+
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddBlockWidgetToCMSPageTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddBlockWidgetToCMSPageTest.xml
new file mode 100644
index 0000000000000..0eb511beb2a05
--- /dev/null
+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddBlockWidgetToCMSPageTest.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ block-id-777
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithTaxRuleForBundleProductInRecentlyViewedWidgetTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithTaxRuleForBundleProductInRecentlyViewedWidgetTest.xml
new file mode 100644
index 0000000000000..6fd7acbdd2e98
--- /dev/null
+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithTaxRuleForBundleProductInRecentlyViewedWidgetTest.xml
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $grabRelatedProductPosition
+ $133.30
+
+
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsBlockGridUrlFilterApplierTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsBlockGridUrlFilterApplierTest.xml
index 0d483e21499fb..9ad4df9e52296 100644
--- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsBlockGridUrlFilterApplierTest.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsBlockGridUrlFilterApplierTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageGridUrlFilterApplierTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageGridUrlFilterApplierTest.xml
index c7b3f7f27e946..ca07a5cdd0bb5 100644
--- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageGridUrlFilterApplierTest.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageGridUrlFilterApplierTest.xml
@@ -17,6 +17,7 @@
+
@@ -32,6 +33,7 @@
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageMassActionTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageMassActionTest.xml
index 53bb2619075a7..b6165f5c59554 100644
--- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageMassActionTest.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageMassActionTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigDefaultCMSPageLayoutFromConfigurationSettingTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigDefaultCMSPageLayoutFromConfigurationSettingTest.xml
index 03a168adf4903..f324b5d1a413d 100644
--- a/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigDefaultCMSPageLayoutFromConfigurationSettingTest.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigDefaultCMSPageLayoutFromConfigurationSettingTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigureStoreInformationTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigureStoreInformationTest.xml
new file mode 100644
index 0000000000000..b06bb0341d517
--- /dev/null
+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigureStoreInformationTest.xml
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminContentBlocksNavigateMenuTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentBlocksNavigateMenuTest.xml
index 7d3946ea86c92..51f62376439ff 100644
--- a/app/code/Magento/Cms/Test/Mftf/Test/AdminContentBlocksNavigateMenuTest.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentBlocksNavigateMenuTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminContentPagesNavigateMenuTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentPagesNavigateMenuTest.xml
index 0a7d794b6d17a..900b3ec4341b8 100644
--- a/app/code/Magento/Cms/Test/Mftf/Test/AdminContentPagesNavigateMenuTest.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminContentPagesNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateDisabledCmsBlockEntityAndAssignToCategoryTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateDisabledCmsBlockEntityAndAssignToCategoryTest.xml
index 4ac851b8b2a1e..eabcdf9e84c92 100644
--- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateDisabledCmsBlockEntityAndAssignToCategoryTest.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateDisabledCmsBlockEntityAndAssignToCategoryTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateEnabledCmsBlockEntityAndAssignToCategoryTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateEnabledCmsBlockEntityAndAssignToCategoryTest.xml
index 6f9861cd18dcf..99b06033763a3 100644
--- a/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateEnabledCmsBlockEntityAndAssignToCategoryTest.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCreateEnabledCmsBlockEntityAndAssignToCategoryTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.xml
index 9e83b02d9184e..99cff6935ec11 100644
--- a/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml
index 245b1486058b8..0fffdf35a0ee4 100644
--- a/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontMobileViewValidationTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontMobileViewValidationTest.xml
index 33e614e566c29..c1a53e0f4ff8c 100644
--- a/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontMobileViewValidationTest.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontMobileViewValidationTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml
index 8c15d6f4c24ce..448f757bc28cb 100644
--- a/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml
+++ b/app/code/Magento/Cms/Test/Mftf/Test/StoreViewLanguageCorrectSwitchTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Noroute/IndexTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Noroute/IndexTest.php
index 665b79fdf48be..6f1998ac98023 100644
--- a/app/code/Magento/Cms/Test/Unit/Controller/Noroute/IndexTest.php
+++ b/app/code/Magento/Cms/Test/Unit/Controller/Noroute/IndexTest.php
@@ -29,17 +29,17 @@ class IndexTest extends TestCase
/**
* @var Index
*/
- protected $_controller;
+ protected Index $_controller;
/**
* @var MockObject
*/
- protected $_cmsHelperMock;
+ protected MockObject $_cmsHelperMock;
/**
* @var MockObject
*/
- protected $_requestMock;
+ protected MockObject $_requestMock;
/**
* @var ForwardFactory|MockObject
@@ -119,10 +119,14 @@ public function testExecuteResultPage(): void
->method('setStatusHeader')
->with(404, '1.1', 'Not Found')
->willReturn($this->resultPageMock);
+
$this->resultPageMock
->method('setHeader')
- ->with('Status', '404 File not found')
- ->willReturn($this->resultPageMock);
+ ->withConsecutive(
+ ['Status', '404 File not found'],
+ ['Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0']
+ )->willReturn($this->resultPageMock);
+
$this->_cmsHelperMock->expects(
$this->once()
)->method(
diff --git a/app/code/Magento/Cms/Test/Unit/ViewModel/Page/Grid/UrlBuilderTest.php b/app/code/Magento/Cms/Test/Unit/ViewModel/Page/Grid/UrlBuilderTest.php
index bc291b865c6ef..6193b1f968713 100644
--- a/app/code/Magento/Cms/Test/Unit/ViewModel/Page/Grid/UrlBuilderTest.php
+++ b/app/code/Magento/Cms/Test/Unit/ViewModel/Page/Grid/UrlBuilderTest.php
@@ -7,6 +7,7 @@
namespace Magento\Cms\Test\Unit\ViewModel\Page\Grid;
+use Magento\Cms\Model\Page\TargetUrlBuilderInterface;
use Magento\Cms\ViewModel\Page\Grid\UrlBuilder;
use Magento\Framework\Url\EncoderInterface;
use Magento\Framework\UrlInterface;
@@ -42,23 +43,31 @@ class UrlBuilderTest extends TestCase
*/
private $storeManagerMock;
+ /**
+ * @var TargetUrlBuilderInterface
+ */
+ private $getTargetUrlMock;
+
/**
* Set Up
*/
protected function setUp(): void
{
$this->frontendUrlBuilderMock = $this->getMockBuilder(UrlInterface::class)
- ->setMethods(['getUrl', 'setScope'])
+ ->onlyMethods(['getUrl', 'setScope'])
->getMockForAbstractClass();
$this->urlEncoderMock = $this->getMockForAbstractClass(EncoderInterface::class);
$this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class)
->disableOriginalConstructor()
->getMockForAbstractClass();
-
+ $this->getTargetUrlMock = $this->getMockBuilder(TargetUrlBuilderInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
$this->viewModel = new UrlBuilder(
$this->frontendUrlBuilderMock,
$this->urlEncoderMock,
- $this->storeManagerMock
+ $this->storeManagerMock,
+ $this->getTargetUrlMock
);
}
@@ -109,54 +118,34 @@ public function nonScopedUrlsDataProvider(): array
/**
* Testing url builder with a scope provided
*
- * @dataProvider scopedUrlsDataProvider
+ * @param array $routePaths
+ * @param array $expectedUrls
*
- * @param string $storeCode
- * @param string $defaultStoreCode
- * @param array $urlParams
- * @param string $scope
+ * @dataProvider scopedUrlsDataProvider
*/
public function testScopedUrlBuilder(
- string $storeCode,
- string $defaultStoreCode,
- array $urlParams,
- string $scope = 'store'
+ array $routePaths,
+ array $expectedUrls
) {
/** @var StoreInterface|MockObject $storeMock */
$storeMock = $this->getMockForAbstractClass(StoreInterface::class);
$storeMock->expects($this->any())
->method('getCode')
- ->willReturn($defaultStoreCode);
+ ->willReturn('en');
$this->storeManagerMock->expects($this->once())
->method('getDefaultStoreView')
->willReturn($storeMock);
-
+ $this->getTargetUrlMock->expects($this->any())
+ ->method('process')
+ ->withConsecutive([$routePaths[0], 'en'], [$routePaths[1], 'en'])
+ ->willReturnOnConsecutiveCalls($routePaths[0], $routePaths[1]);
$this->frontendUrlBuilderMock->expects($this->any())
->method('getUrl')
- ->withConsecutive(
- [
- 'test/index',
- [
- '_current' => false,
- '_nosid' => true,
- '_query' => [
- StoreManagerInterface::PARAM_NAME => $storeCode
- ]
- ]
- ],
- [
- 'stores/store/switch',
- $urlParams
- ]
- )
- ->willReturnOnConsecutiveCalls(
- 'http://domain.com/test',
- 'http://domain.com/test/index'
- );
-
- $result = $this->viewModel->getUrl('test/index', $scope, $storeCode);
-
- $this->assertSame('http://domain.com/test/index', $result);
+ ->willReturnOnConsecutiveCalls($expectedUrls[0], $expectedUrls[1]);
+
+ $result = $this->viewModel->getUrl($routePaths[0], 'store', 'en');
+
+ $this->assertSame($expectedUrls[0], $result);
}
/**
@@ -166,28 +155,14 @@ public function testScopedUrlBuilder(
*/
public function scopedUrlsDataProvider(): array
{
- $enStoreCode = 'en';
- $frStoreCode = 'fr';
- $scopedDefaultUrlParams = $defaultUrlParams = [
- '_current' => false,
- '_nosid' => true,
- '_query' => [
- '___store' => $enStoreCode,
- 'uenc' => null,
- ]
- ];
- $scopedDefaultUrlParams['_query']['___from_store'] = $frStoreCode;
-
return [
[
- $enStoreCode,
- $enStoreCode,
- $defaultUrlParams,
+ ['test1/index1', 'stores/store/switch'],
+ ['http://domain.com/test1', 'http://domain.com/test1/index1']
],
[
- $enStoreCode,
- $frStoreCode,
- $scopedDefaultUrlParams
+ ['fr/test2/index2', 'stores/store/switch'],
+ ['http://domain.com/fr/test2', 'http://domain.com/fr/test2/index2']
]
];
}
diff --git a/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php b/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php
index 15b9fe408d228..312496f2683f6 100644
--- a/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php
+++ b/app/code/Magento/Cms/ViewModel/Page/Grid/UrlBuilder.php
@@ -7,8 +7,11 @@
namespace Magento\Cms\ViewModel\Page\Grid;
-use Magento\Framework\Url\EncoderInterface;
+use Magento\Cms\Model\Page\TargetUrlBuilderInterface;
use Magento\Framework\App\ActionInterface;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Url\EncoderInterface;
+use Magento\Framework\UrlInterface;
use Magento\Store\Model\StoreManagerInterface;
/**
@@ -17,7 +20,7 @@
class UrlBuilder
{
/**
- * @var \Magento\Framework\UrlInterface
+ * @var UrlInterface
*/
private $frontendUrlBuilder;
@@ -32,18 +35,27 @@ class UrlBuilder
private $storeManager;
/**
- * @param \Magento\Framework\UrlInterface $frontendUrlBuilder
+ * @var TargetUrlBuilderInterface
+ */
+ private $getTargetUrl;
+
+ /**
+ * @param UrlInterface $frontendUrlBuilder
* @param EncoderInterface $urlEncoder
* @param StoreManagerInterface $storeManager
+ * @param TargetUrlBuilderInterface|null $getTargetUrl
*/
public function __construct(
- \Magento\Framework\UrlInterface $frontendUrlBuilder,
+ UrlInterface $frontendUrlBuilder,
EncoderInterface $urlEncoder,
- StoreManagerInterface $storeManager
+ StoreManagerInterface $storeManager,
+ ?TargetUrlBuilderInterface $getTargetUrl = null
) {
$this->frontendUrlBuilder = $frontendUrlBuilder;
$this->urlEncoder = $urlEncoder;
$this->storeManager = $storeManager;
+ $this->getTargetUrl = $getTargetUrl ?:
+ ObjectManager::getInstance()->get(TargetUrlBuilderInterface::class);
}
/**
@@ -58,16 +70,7 @@ public function getUrl($routePath, $scope, $store)
{
if ($scope) {
$this->frontendUrlBuilder->setScope($scope);
- $targetUrl = $this->frontendUrlBuilder->getUrl(
- $routePath,
- [
- '_current' => false,
- '_nosid' => true,
- '_query' => [
- StoreManagerInterface::PARAM_NAME => $store
- ]
- ]
- );
+ $targetUrl = $this->getTargetUrl->process($routePath, $store);
$href = $this->frontendUrlBuilder->getUrl(
'stores/store/switch',
[
diff --git a/app/code/Magento/Cms/etc/adminhtml/di.xml b/app/code/Magento/Cms/etc/adminhtml/di.xml
index e2ef86b7f650b..aa1b812561a2d 100644
--- a/app/code/Magento/Cms/etc/adminhtml/di.xml
+++ b/app/code/Magento/Cms/etc/adminhtml/di.xml
@@ -62,4 +62,10 @@
Magento\Variable\Model\Variable\Config\Proxy
+
+
+
+ Magento\Framework\Url
+
+
diff --git a/app/code/Magento/CmsGraphQl/Model/Resolver/Block/ResolverCacheIdentity.php b/app/code/Magento/CmsGraphQl/Model/Resolver/Block/ResolverCacheIdentity.php
new file mode 100644
index 0000000000000..d4cce9f7d58f7
--- /dev/null
+++ b/app/code/Magento/CmsGraphQl/Model/Resolver/Block/ResolverCacheIdentity.php
@@ -0,0 +1,37 @@
+cacheTag, $item[BlockInterface::BLOCK_ID]);
+ $ids[] = sprintf('%s_%s', $this->cacheTag, $item[BlockInterface::IDENTIFIER]);
+ }
+ }
+
+ return $ids;
+ }
+}
diff --git a/app/code/Magento/CmsGraphQl/Model/Resolver/Page/ResolverCacheIdentity.php b/app/code/Magento/CmsGraphQl/Model/Resolver/Page/ResolverCacheIdentity.php
new file mode 100644
index 0000000000000..1a48504dac22e
--- /dev/null
+++ b/app/code/Magento/CmsGraphQl/Model/Resolver/Page/ResolverCacheIdentity.php
@@ -0,0 +1,32 @@
+cacheTag, $resolvedData[PageInterface::PAGE_ID])];
+ }
+}
diff --git a/app/code/Magento/CmsGraphQl/Test/Integration/Model/Resolver/PageTest.php b/app/code/Magento/CmsGraphQl/Test/Integration/Model/Resolver/PageTest.php
new file mode 100644
index 0000000000000..79028dde33468
--- /dev/null
+++ b/app/code/Magento/CmsGraphQl/Test/Integration/Model/Resolver/PageTest.php
@@ -0,0 +1,252 @@
+objectManager = $objectManager = Bootstrap::getObjectManager();
+ $this->graphQlRequest = $objectManager->create(GraphQlRequest::class);
+ $this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class);
+ $this->pageRepository = $objectManager->get(PageRepository::class);
+ $this->originalResolverResultCachePlugin = $objectManager->get(ResolverResultCachePlugin::class);
+
+ $this->cacheState = $objectManager->get(CacheStateInterface::class);
+ $this->originalCacheStateEnabledStatus = $this->cacheState->isEnabled(GraphQlResolverCache::TYPE_IDENTIFIER);
+ $this->cacheState->setEnabled(GraphQlResolverCache::TYPE_IDENTIFIER, true);
+ }
+
+ protected function tearDown(): void
+ {
+ $objectManager = $this->objectManager;
+
+ // reset to original resolver plugin
+ $objectManager->addSharedInstance($this->originalResolverResultCachePlugin, ResolverResultCachePlugin::class);
+
+ // clean graphql resolver cache and reset to original enablement status
+ $objectManager->get(GraphQlResolverCache::class)->clean();
+ $this->cacheState->setEnabled(GraphQlResolverCache::TYPE_IDENTIFIER, $this->originalCacheStateEnabledStatus);
+ }
+
+ /**
+ * Test that result can be loaded continuously after saving once when passing the same arguments
+ *
+ * @magentoDataFixture Magento/Cms/Fixtures/page_list.php
+ * @return void
+ */
+ public function testResultIsLoadedMultipleTimesAfterOnlyBeingSavedOnce()
+ {
+ $objectManager = $this->objectManager;
+ $page = $this->getPageByTitle('Page with 1column layout');
+
+ $frontendPool = $objectManager->get(FrontendPool::class);
+
+ $cacheProxy = $this->getMockBuilder(GraphQlResolverCache::class)
+ ->enableProxyingToOriginalMethods()
+ ->setConstructorArgs([
+ $frontendPool
+ ])
+ ->getMock();
+
+ // assert cache proxy calls load at least once for the same CMS page query
+ $cacheProxy
+ ->expects($this->atLeastOnce())
+ ->method('load');
+
+ // assert save is called at most once for the same CMS page query
+ $cacheProxy
+ ->expects($this->once())
+ ->method('save');
+
+ $resolverPluginWithCacheProxy = $objectManager->create(ResolverResultCachePlugin::class, [
+ 'graphQlResolverCache' => $cacheProxy,
+ ]);
+
+ // override resolver plugin with plugin instance containing cache proxy class
+ $objectManager->addSharedInstance($resolverPluginWithCacheProxy, ResolverResultCachePlugin::class);
+
+ $query = $this->getQuery($page->getIdentifier());
+
+ // send request and assert save is called
+ $this->graphQlRequest->send($query);
+
+ // send again and assert save is not called (i.e. result is loaded from resolver cache)
+ $this->graphQlRequest->send($query);
+
+ // send again with whitespace appended and assert save is not called (i.e. result is loaded from resolver cache)
+ $this->graphQlRequest->send($query . ' ');
+
+ // send again with a different field and assert save is not called (i.e. result is loaded from resolver cache)
+ $differentQuery = $this->getQuery($page->getIdentifier(), ['meta_title']);
+ $this->graphQlRequest->send($differentQuery);
+ }
+
+ /**
+ * Test that resolver plugin does not call GraphQlResolverCache's save or load methods when it is disabled
+ *
+ * @magentoDataFixture Magento/Cms/Fixtures/page_list.php
+ * @return void
+ */
+ public function testNeitherSaveNorLoadAreCalledWhenResolverCacheIsDisabled()
+ {
+ $objectManager = $this->objectManager;
+ $page = $this->getPageByTitle('Page with 1column layout');
+
+ // disable graphql resolver cache
+ $this->cacheState->setEnabled(GraphQlResolverCache::TYPE_IDENTIFIER, false);
+
+ $frontendPool = $objectManager->get(FrontendPool::class);
+
+ $cacheProxy = $this->getMockBuilder(GraphQlResolverCache::class)
+ ->enableProxyingToOriginalMethods()
+ ->setConstructorArgs([
+ $frontendPool
+ ])
+ ->getMock();
+
+ // assert cache proxy never calls load
+ $cacheProxy
+ ->expects($this->never())
+ ->method('load');
+
+ // assert save is also never called
+ $cacheProxy
+ ->expects($this->never())
+ ->method('save');
+
+ $resolverPluginWithCacheProxy = $objectManager->create(ResolverResultCachePlugin::class, [
+ 'graphQlResolverCache' => $cacheProxy,
+ ]);
+
+ // override resolver plugin with plugin instance containing cache proxy class
+ $objectManager->addSharedInstance($resolverPluginWithCacheProxy, ResolverResultCachePlugin::class);
+
+ $query = $this->getQuery($page->getIdentifier());
+
+ // send request multiple times and assert neither save nor load are called
+ $this->graphQlRequest->send($query);
+ $this->graphQlRequest->send($query);
+ }
+
+ public function testSaveIsNeverCalledWhenMissingRequiredArgumentInQuery()
+ {
+ $objectManager = $this->objectManager;
+
+ $frontendPool = $objectManager->get(FrontendPool::class);
+
+ $cacheProxy = $this->getMockBuilder(GraphQlResolverCache::class)
+ ->enableProxyingToOriginalMethods()
+ ->setConstructorArgs([
+ $frontendPool
+ ])
+ ->getMock();
+
+ // assert cache proxy never calls save
+ $cacheProxy
+ ->expects($this->never())
+ ->method('save');
+
+ $resolverPluginWithCacheProxy = $objectManager->create(ResolverResultCachePlugin::class, [
+ 'graphQlResolverCache' => $cacheProxy,
+ ]);
+
+ // override resolver plugin with plugin instance containing cache proxy class
+ $objectManager->addSharedInstance($resolverPluginWithCacheProxy, ResolverResultCachePlugin::class);
+
+ $query = <<graphQlRequest->send($query);
+ $this->graphQlRequest->send($query);
+ }
+
+ private function getQuery(string $identifier, array $fields = ['title']): string
+ {
+ $fields = implode(PHP_EOL, $fields);
+
+ return <<searchCriteriaBuilder
+ ->addFilter('title', $title)
+ ->create();
+
+ $pages = $this->pageRepository->getList($searchCriteria)->getItems();
+
+ /** @var PageInterface $page */
+ $page = reset($pages);
+
+ return $page;
+ }
+}
diff --git a/app/code/Magento/CmsGraphQl/composer.json b/app/code/Magento/CmsGraphQl/composer.json
index 07b7261823d92..ea6e6152cacca 100644
--- a/app/code/Magento/CmsGraphQl/composer.json
+++ b/app/code/Magento/CmsGraphQl/composer.json
@@ -7,7 +7,8 @@
"magento/framework": "*",
"magento/module-cms": "*",
"magento/module-widget": "*",
- "magento/module-store": "*"
+ "magento/module-store": "*",
+ "magento/module-graph-ql-resolver-cache": "*"
},
"suggest": {
"magento/module-graph-ql": "*",
diff --git a/app/code/Magento/CmsGraphQl/etc/di.xml b/app/code/Magento/CmsGraphQl/etc/di.xml
new file mode 100644
index 0000000000000..86efef7b2f960
--- /dev/null
+++ b/app/code/Magento/CmsGraphQl/etc/di.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ -
+ Magento\Cms\Api\Data\PageInterface
+
+ -
+ Magento\Cms\Api\Data\BlockInterface
+
+
+
+
+
diff --git a/app/code/Magento/CmsGraphQl/etc/graphql/di.xml b/app/code/Magento/CmsGraphQl/etc/graphql/di.xml
index 78c1071d8e07c..4fd07a377f7e6 100644
--- a/app/code/Magento/CmsGraphQl/etc/graphql/di.xml
+++ b/app/code/Magento/CmsGraphQl/etc/graphql/di.xml
@@ -18,4 +18,16 @@
+
+
+
+ -
+ Magento\CmsGraphQl\Model\Resolver\Page\ResolverCacheIdentity
+
+ -
+ Magento\CmsGraphQl\Model\Resolver\Block\ResolverCacheIdentity
+
+
+
+
diff --git a/app/code/Magento/CmsGraphQl/etc/module.xml b/app/code/Magento/CmsGraphQl/etc/module.xml
index 4fca42430d166..535374716eb71 100644
--- a/app/code/Magento/CmsGraphQl/etc/module.xml
+++ b/app/code/Magento/CmsGraphQl/etc/module.xml
@@ -9,6 +9,7 @@
+
diff --git a/app/code/Magento/CmsUrlRewrite/Model/Page/TargetUrlBuilder.php b/app/code/Magento/CmsUrlRewrite/Model/Page/TargetUrlBuilder.php
new file mode 100644
index 0000000000000..f862e4ca60900
--- /dev/null
+++ b/app/code/Magento/CmsUrlRewrite/Model/Page/TargetUrlBuilder.php
@@ -0,0 +1,110 @@
+frontendUrlBuilder = $frontendUrlBuilder;
+ $this->storeManager = $storeManager;
+ $this->cmsPage = $cmsPage;
+ $this->urlFinder = $urlFinder;
+ $this->cmsPageUrlPathGenerator = $cmsPageUrlPathGenerator;
+ }
+
+ /**
+ * Get target URL
+ *
+ * @param string $routePath
+ * @param string $store
+ * @return string
+ * @throws NoSuchEntityException
+ */
+ public function process(string $routePath, string $store): string
+ {
+ $storeId = $this->storeManager->getStore($store)->getId();
+ $pageId = $this->cmsPage->checkIdentifier($routePath, $storeId);
+ $currentUrlRewrite = $this->urlFinder->findOneByData(
+ [
+ UrlRewrite::REQUEST_PATH => $routePath,
+ UrlRewrite::STORE_ID => $storeId,
+ ]
+ );
+ $existingUrlRewrite = $this->urlFinder->findOneByData(
+ [
+ UrlRewrite::REQUEST_PATH => $routePath
+ ]
+ );
+ if ($currentUrlRewrite === null && $existingUrlRewrite !== null && !empty($pageId)) {
+ $cmsPage = $this->cmsPage->load($pageId);
+ $routePath = $this->cmsPageUrlPathGenerator->getCanonicalUrlPath($cmsPage);
+ }
+ return $this->frontendUrlBuilder->getUrl(
+ $routePath,
+ [
+ '_current' => false,
+ '_nosid' => true,
+ '_query' => [
+ StoreManagerInterface::PARAM_NAME => $store
+ ]
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/CmsUrlRewrite/README.md b/app/code/Magento/CmsUrlRewrite/README.md
index 1f1b1ca782532..a1c20e3daefb0 100644
--- a/app/code/Magento/CmsUrlRewrite/README.md
+++ b/app/code/Magento/CmsUrlRewrite/README.md
@@ -1,6 +1,6 @@
## Overview
-
-The Magento_CmsUrlRewrite module adds support for URL rewrite rules for CMS pages. See also Magento_UrlRewrite module.
+
+The Magento_CmsUrlRewrite module adds support for URL rewrite rules for CMS pages. See also Magento_UrlRewrite module.
The module adds and removes URL rewrite rules as CMS pages are added or removed by a user.
-The rules can be edited by an admin user as any other URL rewrite rule.
+The rules can be edited by an admin user as any other URL rewrite rule.
diff --git a/app/code/Magento/CmsUrlRewrite/Test/Unit/Model/Page/TargetUrlBuilderTest.php b/app/code/Magento/CmsUrlRewrite/Test/Unit/Model/Page/TargetUrlBuilderTest.php
new file mode 100644
index 0000000000000..940775764c62f
--- /dev/null
+++ b/app/code/Magento/CmsUrlRewrite/Test/Unit/Model/Page/TargetUrlBuilderTest.php
@@ -0,0 +1,176 @@
+frontendUrlBuilderMock = $this->getMockBuilder(UrlInterface::class)
+ ->onlyMethods(['getUrl', 'setScope'])
+ ->getMockForAbstractClass();
+ $this->cmsPageMock = $this->getMockBuilder(Page::class)
+ ->onlyMethods(['checkIdentifier'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->cmsPageUrlPathGeneratorMock = $this->getMockBuilder(CmsPageUrlPathGenerator::class)
+ ->onlyMethods(['getCanonicalUrlPath'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->urlFinderMock = $this->getMockBuilder(UrlFinderInterface::class)
+ ->onlyMethods(['findOneByData'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->viewModel = new TargetUrlBuilder(
+ $this->frontendUrlBuilderMock,
+ $this->storeManagerMock,
+ $this->cmsPageMock,
+ $this->urlFinderMock,
+ $this->cmsPageUrlPathGeneratorMock
+ );
+ }
+
+ /**
+ * Testing getTargetUrl with a scope provided
+ *
+ * @dataProvider scopedUrlsDataProvider
+ *
+ * @param array $urlParams
+ * @param string $storeId
+ * @throws NoSuchEntityException
+ */
+ public function testGetTargetUrl(array $urlParams, string $storeId): void
+ {
+ /** @var StoreInterface|MockObject $storeMock */
+ $storeMock = $this->getMockForAbstractClass(StoreInterface::class);
+ $storeMock->expects($this->any())
+ ->method('getId')
+ ->willReturn($storeId);
+ $this->storeManagerMock->expects($this->once())
+ ->method('getStore')
+ ->willReturn($storeMock);
+
+ $this->cmsPageMock->expects($this->any())
+ ->method('checkIdentifier')
+ ->willReturn("1");
+ $this->cmsPageUrlPathGeneratorMock->expects($this->any())
+ ->method('getCanonicalUrlPath')
+ ->with($this->cmsPageMock)
+ ->willReturn('test/index');
+ $this->urlFinderMock->expects($this->any())
+ ->method('findOneByData')
+ ->willReturn('test/index');
+ $this->frontendUrlBuilderMock->expects($this->any())
+ ->method('getUrl')
+ ->withConsecutive(
+ [
+ 'test/index',
+ [
+ '_current' => false,
+ '_nosid' => true,
+ '_query' => [
+ StoreManagerInterface::PARAM_NAME => $storeId
+ ]
+ ]
+ ],
+ [
+ 'stores/store/switch',
+ $urlParams
+ ]
+ )
+ ->willReturnOnConsecutiveCalls(
+ 'http://domain.com/test',
+ 'http://domain.com/test/index'
+ );
+
+ $result = $this->viewModel->process('test/index', $storeId);
+
+ $this->assertSame('http://domain.com/test', $result);
+ }
+
+ /**
+ * Providing a scoped urls
+ *
+ * @return array
+ */
+ public function scopedUrlsDataProvider(): array
+ {
+ $enStoreCode = 'en';
+ $defaultUrlParams = [
+ '_current' => false,
+ '_nosid' => true,
+ '_query' => [
+ '___store' => $enStoreCode,
+ 'uenc' => null,
+ ]
+ ];
+
+ return [
+ [
+ $defaultUrlParams,
+ "1"
+ ],
+ [
+ $defaultUrlParams,
+ "2"
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/CmsUrlRewrite/etc/adminhtml/di.xml b/app/code/Magento/CmsUrlRewrite/etc/adminhtml/di.xml
index c6b0e4b05f16b..b0839b233f8e9 100644
--- a/app/code/Magento/CmsUrlRewrite/etc/adminhtml/di.xml
+++ b/app/code/Magento/CmsUrlRewrite/etc/adminhtml/di.xml
@@ -9,4 +9,10 @@
+
+
+
+ Magento\Framework\Url
+
+
diff --git a/app/code/Magento/CompareListGraphQl/README.md b/app/code/Magento/CompareListGraphQl/README.md
index ed1c38ab33a3b..92215c13a6792 100644
--- a/app/code/Magento/CompareListGraphQl/README.md
+++ b/app/code/Magento/CompareListGraphQl/README.md
@@ -1,4 +1,3 @@
# CompareListGraphQl module
The CompareListGraphQl module is designed to implement compare product functionality.
-
diff --git a/app/code/Magento/Config/App/Config/Type/System.php b/app/code/Magento/Config/App/Config/Type/System.php
index 3b2c85b5d71d3..66d680127f538 100644
--- a/app/code/Magento/Config/App/Config/Type/System.php
+++ b/app/code/Magento/Config/App/Config/Type/System.php
@@ -6,22 +6,23 @@
namespace Magento\Config\App\Config\Type;
+use Magento\Config\App\Config\Type\System\Reader;
+use Magento\Framework\App\Cache\StateInterface;
+use Magento\Framework\App\Cache\Type\Config;
use Magento\Framework\App\Config\ConfigSourceInterface;
use Magento\Framework\App\Config\ConfigTypeInterface;
use Magento\Framework\App\Config\Spi\PostProcessorInterface;
use Magento\Framework\App\Config\Spi\PreProcessorInterface;
use Magento\Framework\App\ObjectManager;
-use Magento\Config\App\Config\Type\System\Reader;
use Magento\Framework\App\ScopeInterface;
use Magento\Framework\Cache\FrontendInterface;
use Magento\Framework\Cache\LockGuardedCacheLoader;
+use Magento\Framework\Encryption\Encryptor;
use Magento\Framework\Lock\LockManagerInterface;
use Magento\Framework\Serialize\SerializerInterface;
use Magento\Store\Model\Config\Processor\Fallback;
-use Magento\Framework\Encryption\Encryptor;
use Magento\Store\Model\ScopeInterface as StoreScope;
-use Magento\Framework\App\Cache\StateInterface;
-use Magento\Framework\App\Cache\Type\Config;
+use Psr\Log\LoggerInterface;
/**
* System configuration type
@@ -104,6 +105,10 @@ class System implements ConfigTypeInterface
*/
private $cacheState;
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
/**
* System constructor.
* @param ConfigSourceInterface $source
@@ -119,6 +124,7 @@ class System implements ConfigTypeInterface
* @param LockManagerInterface|null $locker
* @param LockGuardedCacheLoader|null $lockQuery
* @param StateInterface|null $cacheState
+ * @param LoggerInterface $logger
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
@@ -135,7 +141,8 @@ public function __construct(
Encryptor $encryptor = null,
LockManagerInterface $locker = null,
LockGuardedCacheLoader $lockQuery = null,
- StateInterface $cacheState = null
+ StateInterface $cacheState = null,
+ LoggerInterface $logger = null
) {
$this->postProcessor = $postProcessor;
$this->cache = $cache;
@@ -148,6 +155,8 @@ public function __construct(
?: ObjectManager::getInstance()->get(LockGuardedCacheLoader::class);
$this->cacheState = $cacheState
?: ObjectManager::getInstance()->get(StateInterface::class);
+ $this->logger = $logger
+ ?: ObjectManager::getInstance()->get(LoggerInterface::class);
}
/**
@@ -265,7 +274,12 @@ private function loadDefaultScopeData()
$cachedData = $this->cache->load($this->configType . '_' . $scopeType);
$scopeData = false;
if ($cachedData !== false) {
- $scopeData = [$scopeType => $this->serializer->unserialize($this->encryptor->decrypt($cachedData))];
+ try {
+ $scopeData = [$scopeType => $this->serializer->unserialize($this->encryptor->decrypt($cachedData))];
+ } catch (\InvalidArgumentException $e) {
+ $this->logger->warning($e->getMessage());
+ $scopeData = false;
+ }
}
return $scopeData;
};
@@ -292,11 +306,13 @@ private function loadScopeData($scopeType, $scopeId)
}
$loadAction = function () use ($scopeType, $scopeId) {
+ /* Note: configType . '_scopes' needs to be loaded first to avoid race condition where cache finishes
+ saving after configType . '_' . $scopeType . '_' . $scopeId but before configType . '_scopes'. */
+ $cachedScopeData = $this->cache->load($this->configType . '_scopes');
$cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId);
$scopeData = false;
if ($cachedData === false) {
if ($this->availableDataScopes === null) {
- $cachedScopeData = $this->cache->load($this->configType . '_scopes');
if ($cachedScopeData !== false) {
$serializedCachedData = $this->encryptor->decrypt($cachedScopeData);
$this->availableDataScopes = $this->serializer->unserialize($serializedCachedData);
@@ -437,18 +453,113 @@ private function readData(): array
*/
public function clean()
{
- $this->data = [];
$cleanAction = function () {
- $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]);
+ $this->cacheData($this->readData()); // Note: If cache is enabled, pre-load the new config data.
};
+ $this->data = [];
+ if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) {
+ // Note: If cache is disabled, we still clean cache in case it will be enabled later
+ $this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]);
+ return;
+ }
+ $this->lockQuery->lockedCleanData(self::$lockName, $cleanAction);
+ }
+
+ /**
+ * Prepares data for cache by serializing and encrypting them
+ *
+ * Prepares data per scope to avoid reading data for all scopes on every request
+ *
+ * @param array $data
+ * @return array
+ */
+ private function prepareDataForCache(array $data) :array
+ {
+ $dataToSave = [];
+ $dataToSave[] = [
+ $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($data)),
+ $this->configType,
+ [System::CACHE_TAG]
+ ];
+ $dataToSave[] = [
+ $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($data['default'])),
+ $this->configType . '_default',
+ [System::CACHE_TAG]
+ ];
+ $scopes = [];
+ foreach ([StoreScope::SCOPE_WEBSITES, StoreScope::SCOPE_STORES] as $curScopeType) {
+ foreach ($data[$curScopeType] ?? [] as $curScopeId => $curScopeData) {
+ $scopes[$curScopeType][$curScopeId] = 1;
+ $dataToSave[] = [
+ $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($curScopeData)),
+ $this->configType . '_' . $curScopeType . '_' . $curScopeId,
+ [System::CACHE_TAG]
+ ];
+ }
+ }
+ $dataToSave[] = [
+ $this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($scopes)),
+ $this->configType . '_scopes',
+ [System::CACHE_TAG]
+ ];
+ return $dataToSave;
+ }
+ /**
+ * Cache prepared configuration data.
+ *
+ * Takes data prepared by prepareDataForCache
+ *
+ * @param array $dataToSave
+ * @return void
+ */
+ private function cachePreparedData(array $dataToSave) : void
+ {
+ foreach ($dataToSave as $datumToSave) {
+ $this->cache->save($datumToSave[0], $datumToSave[1], $datumToSave[2]);
+ }
+ }
+
+ /**
+ * Gets configuration then cleans and warms it while locked
+ *
+ * This is to reduce the lock time after flushing config cache.
+ *
+ * @param callable $cleaner
+ * @return void
+ */
+ public function cleanAndWarmDefaultScopeData(callable $cleaner)
+ {
if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) {
- return $cleanAction();
+ $cleaner();
+ return;
}
+ $loadAction = function () {
+ return false;
+ };
+ $dataCollector = function () use ($cleaner) {
+ /* Note: call to readData() needs to be inside lock to avoid race conditions such as multiple
+ saves at the same time. */
+ $newData = $this->readData();
+ $preparedData = $this->prepareDataForCache($newData);
+ unset($newData);
+ $cleaner(); // Note: This is where other readers start waiting for us to finish saving cache.
+ return $preparedData;
+ };
+ $dataSaver = function (array $preparedData) {
+ $this->cachePreparedData($preparedData);
+ };
+ $this->lockQuery->lockedLoadData(self::$lockName, $loadAction, $dataCollector, $dataSaver);
+ }
- $this->lockQuery->lockedCleanData(
- self::$lockName,
- $cleanAction
- );
+ /**
+ * Disable show internals with var_dump
+ *
+ * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo
+ * @return array
+ */
+ public function __debugInfo()
+ {
+ return [];
}
}
diff --git a/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php b/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php
index 2465eecec71dc..ed60f63717dac 100644
--- a/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php
+++ b/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php
@@ -26,7 +26,7 @@ class ValueProcessor
/**
* Placeholder for the output of sensitive data.
*/
- const SAFE_PLACEHOLDER = '******';
+ public const SAFE_PLACEHOLDER = '******';
/**
* System configuration structure factory.
@@ -56,6 +56,9 @@ class ValueProcessor
*/
private $jsonSerializer;
+ /** @var Structure */
+ private $configStructure;
+
/**
* @param ScopeInterface $scope The object for managing configuration scope
* @param StructureFactory $structureFactory The system configuration structure factory.
@@ -87,11 +90,7 @@ public function __construct(
*/
public function process($scope, $scopeCode, $value, $path)
{
- $areaScope = $this->scope->getCurrentScope();
- $this->scope->setCurrentScope(Area::AREA_ADMINHTML);
- /** @var Structure $configStructure */
- $configStructure = $this->configStructureFactory->create();
- $this->scope->setCurrentScope($areaScope);
+ $configStructure = $this->getConfigStructure();
/** @var Field $field */
$field = $configStructure->getElementByConfigPath($path);
@@ -118,4 +117,21 @@ public function process($scope, $scopeCode, $value, $path)
*/
return is_array($processedValue) ? $this->jsonSerializer->serialize($processedValue) : $processedValue;
}
+
+ /**
+ * Retrieve config structure
+ *
+ * @return Structure
+ */
+ private function getConfigStructure(): Structure
+ {
+ if (empty($this->configStructure)) {
+ $areaScope = $this->scope->getCurrentScope();
+ $this->scope->setCurrentScope(Area::AREA_ADMINHTML);
+ /** @var Structure $configStructure */
+ $this->configStructure = $this->configStructureFactory->create();
+ $this->scope->setCurrentScope($areaScope);
+ }
+ return $this->configStructure;
+ }
}
diff --git a/app/code/Magento/Config/Model/Config.php b/app/code/Magento/Config/Model/Config.php
index f5188d7a419b8..2ba52091161f7 100644
--- a/app/code/Magento/Config/Model/Config.php
+++ b/app/code/Magento/Config/Model/Config.php
@@ -182,6 +182,10 @@ public function save()
return $this;
}
+ /**
+ * Reload config to make sure config data is consistent with the database at this point.
+ */
+ $this->_appConfig->reinit();
$oldConfig = $this->_getConfig(true);
/** @var \Magento\Framework\DB\Transaction $deleteTransaction */
diff --git a/app/code/Magento/Config/Model/Config/Loader.php b/app/code/Magento/Config/Model/Config/Loader.php
index 625c3cf2f41fe..fa48abcc6d26a 100644
--- a/app/code/Magento/Config/Model/Config/Loader.php
+++ b/app/code/Magento/Config/Model/Config/Loader.php
@@ -4,15 +4,14 @@
* See COPYING.txt for license details.
*/
-/**
- * System configuration loader
- */
namespace Magento\Config\Model\Config;
+use Magento\Config\Model\ResourceModel\Config\Data\CollectionFactory;
+use Magento\Framework\App\ObjectManager;
+
/**
- * Class which can read config by paths
+ * System configuration loader - Class which can read config by paths
*
- * @package Magento\Config\Model\Config
* @api
* @since 100.0.2
*/
@@ -22,15 +21,26 @@ class Loader
* Config data factory
*
* @var \Magento\Framework\App\Config\ValueFactory
+ * @deprecated
+ * @see $collectionFactory
*/
protected $_configValueFactory;
+ /**
+ * @var CollectionFactory
+ */
+ private $collectionFactory;
+
/**
* @param \Magento\Framework\App\Config\ValueFactory $configValueFactory
+ * @param ?CollectionFactory $collectionFactory
*/
- public function __construct(\Magento\Framework\App\Config\ValueFactory $configValueFactory)
- {
+ public function __construct(
+ \Magento\Framework\App\Config\ValueFactory $configValueFactory,
+ CollectionFactory $collectionFactory = null
+ ) {
$this->_configValueFactory = $configValueFactory;
+ $this->collectionFactory = $collectionFactory ?: ObjectManager::getInstance()->get(CollectionFactory::class);
}
/**
@@ -44,9 +54,8 @@ public function __construct(\Magento\Framework\App\Config\ValueFactory $configVa
*/
public function getConfigByPath($path, $scope, $scopeId, $full = true)
{
- $configDataCollection = $this->_configValueFactory->create();
- $configDataCollection = $configDataCollection->getCollection()->addScopeFilter($scope, $scopeId, $path);
-
+ $configDataCollection = $this->collectionFactory->create();
+ $configDataCollection->addScopeFilter($scope, $scopeId, $path);
$config = [];
$configDataCollection->load();
foreach ($configDataCollection->getItems() as $data) {
diff --git a/app/code/Magento/Config/Model/ResourceModel/Config.php b/app/code/Magento/Config/Model/ResourceModel/Config.php
index 594a9df719daa..79805f288beb2 100644
--- a/app/code/Magento/Config/Model/ResourceModel/Config.php
+++ b/app/code/Magento/Config/Model/ResourceModel/Config.php
@@ -6,6 +6,7 @@
namespace Magento\Config\Model\ResourceModel;
use Magento\Framework\App\Config\ScopeConfigInterface;
+use Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface;
/**
* Core Resource Resource Model
@@ -17,14 +18,23 @@
class Config extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb implements
\Magento\Framework\App\Config\ConfigResource\ConfigInterface
{
+ /**
+ * @var PoisonPillPutInterface
+ */
+ private $pillPut;
+
/**
* Define main table
*
+ * @param PoisonPillPutInterface|null $pillPut
* @return void
*/
- protected function _construct()
- {
+ protected function _construct(
+ PoisonPillPutInterface $pillPut = null
+ ) {
$this->_init('core_config_data', 'config_id');
+ $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance()
+ ->get(PoisonPillPutInterface::class);
}
/**
@@ -61,6 +71,7 @@ public function saveConfig($path, $value, $scope = ScopeConfigInterface::SCOPE_T
} else {
$connection->insert($this->getMainTable(), $newData);
}
+ $this->pillPut->put();
return $this;
}
@@ -83,6 +94,7 @@ public function deleteConfig($path, $scope = ScopeConfigInterface::SCOPE_TYPE_DE
$connection->quoteInto('scope_id = ?', $scopeId)
]
);
+ $this->pillPut->put();
return $this;
}
}
diff --git a/app/code/Magento/Config/Plugin/Framework/App/Cache/TypeList/WarmConfigCache.php b/app/code/Magento/Config/Plugin/Framework/App/Cache/TypeList/WarmConfigCache.php
new file mode 100644
index 0000000000000..d82063f7c86c7
--- /dev/null
+++ b/app/code/Magento/Config/Plugin/Framework/App/Cache/TypeList/WarmConfigCache.php
@@ -0,0 +1,52 @@
+system = $system;
+ }
+
+ /**
+ * Around plugin for cache's clean type method
+ *
+ * @param TypeList $subject
+ * @param callable $proceed
+ * @param string $typeCode
+ * @return void
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function aroundCleanType(TypeList $subject, callable $proceed, $typeCode)
+ {
+ if (TypeConfig::TYPE_IDENTIFIER !== $typeCode) {
+ return $proceed($typeCode);
+ }
+ $cleaner = function () use ($proceed, $typeCode) {
+ return $proceed($typeCode);
+ };
+ $this->system->cleanAndWarmDefaultScopeData($cleaner);
+ }
+}
diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminAllowToChooseStateActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminAllowToChooseStateActionGroup.xml
new file mode 100644
index 0000000000000..e672081551d45
--- /dev/null
+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminAllowToChooseStateActionGroup.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ Goes to the 'Configuration' page for 'General'. Selects the provided Countries under 'State is Required for'. Clicks on the Save button.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminChangeTimeZoneForDifferentWebsiteActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminChangeTimeZoneForDifferentWebsiteActionGroup.xml
new file mode 100644
index 0000000000000..7b33c5229a869
--- /dev/null
+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminChangeTimeZoneForDifferentWebsiteActionGroup.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+ set the time zone for different website
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminNavigateToDefaultLocaleSettingActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminNavigateToDefaultLocaleSettingActionGroup.xml
new file mode 100644
index 0000000000000..0cae2ad05b5b8
--- /dev/null
+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminNavigateToDefaultLocaleSettingActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Goes to the 'Configuration' page for 'Locale Options'. Expands the 'Locale Options' section.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/EuropeanCountriesSystemCheckBoxActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/EuropeanCountriesSystemCheckBoxActionGroup.xml
new file mode 100644
index 0000000000000..055715d71f93c
--- /dev/null
+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/EuropeanCountriesSystemCheckBoxActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ check system value european country option value
+
+
+
+
+
+
diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/ResetBackTaxClassForGiftOptionsAndShoppingCartDisplaySettingsActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ResetBackTaxClassForGiftOptionsAndShoppingCartDisplaySettingsActionGroup.xml
new file mode 100644
index 0000000000000..99bf20b3e671c
--- /dev/null
+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ResetBackTaxClassForGiftOptionsAndShoppingCartDisplaySettingsActionGroup.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+ Goes to the 'Configuration' page for 'Tax'. Resets 'Tax Class for Shipping' to 'Taxable Goods'. Updates the Shopping cart display settongs. Clicks on the Save button.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/ResetTaxClassForShippingActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/ResetTaxClassForShippingActionGroup.xml
index 3f768bdac8055..b98041d8d6ed5 100644
--- a/app/code/Magento/Config/Test/Mftf/ActionGroup/ResetTaxClassForShippingActionGroup.xml
+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/ResetTaxClassForShippingActionGroup.xml
@@ -19,7 +19,7 @@
-
+
diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/SetTaxClassForGiftOptionsAndShoppingCartDisplaySettingsActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/SetTaxClassForGiftOptionsAndShoppingCartDisplaySettingsActionGroup.xml
new file mode 100644
index 0000000000000..3a825ebfb511f
--- /dev/null
+++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/SetTaxClassForGiftOptionsAndShoppingCartDisplaySettingsActionGroup.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+ Goes to the 'Configuration' page for 'Tax'. Sets 'Tax Class for Shipping' to 'Taxable Goods'. Updates the Shopping cart display settongs. Clicks on the Save button.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml b/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml
index 378aa0bfc510c..59c70ff68f419 100644
--- a/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml
+++ b/app/code/Magento/Config/Test/Mftf/Data/CountryOptionConfigData.xml
@@ -30,4 +30,8 @@
websites
base
+
+ general/country/eu_countries
+ GB,DE,FR
+
diff --git a/app/code/Magento/Config/Test/Mftf/Section/GeneralSection/StateOptionsSection.xml b/app/code/Magento/Config/Test/Mftf/Section/GeneralSection/StateOptionsSection.xml
index 99a76a446aaa4..e01d37f6eea2b 100644
--- a/app/code/Magento/Config/Test/Mftf/Section/GeneralSection/StateOptionsSection.xml
+++ b/app/code/Magento/Config/Test/Mftf/Section/GeneralSection/StateOptionsSection.xml
@@ -11,5 +11,6 @@
+
diff --git a/app/code/Magento/Config/Test/Mftf/Section/SalesConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/SalesConfigSection.xml
index 878a0c24f7331..f971b4dd03cec 100644
--- a/app/code/Magento/Config/Test/Mftf/Section/SalesConfigSection.xml
+++ b/app/code/Magento/Config/Test/Mftf/Section/SalesConfigSection.xml
@@ -13,5 +13,10 @@
+
+
+
+
+
diff --git a/app/code/Magento/Config/Test/Mftf/Test/DateFiltersInCustomInstanceTimeZoneTest.xml b/app/code/Magento/Config/Test/Mftf/Test/DateFiltersInCustomInstanceTimeZoneTest.xml
new file mode 100644
index 0000000000000..cd25471a0de4c
--- /dev/null
+++ b/app/code/Magento/Config/Test/Mftf/Test/DateFiltersInCustomInstanceTimeZoneTest.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Config/Test/Mftf/Test/ValidateEuropeanCountriesOptionValue.xml b/app/code/Magento/Config/Test/Mftf/Test/ValidateEuropeanCountriesOptionValue.xml
index 72a23f7e811c0..c1131d2d701e2 100644
--- a/app/code/Magento/Config/Test/Mftf/Test/ValidateEuropeanCountriesOptionValue.xml
+++ b/app/code/Magento/Config/Test/Mftf/Test/ValidateEuropeanCountriesOptionValue.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/LoaderTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/LoaderTest.php
index 0a322457ed741..1a4fa9915cc4e 100644
--- a/app/code/Magento/Config/Test/Unit/Model/Config/LoaderTest.php
+++ b/app/code/Magento/Config/Test/Unit/Model/Config/LoaderTest.php
@@ -9,7 +9,7 @@
use Magento\Config\Model\Config\Loader;
use Magento\Config\Model\ResourceModel\Config\Data\Collection;
-use Magento\Framework\App\Config\Value;
+use Magento\Config\Model\ResourceModel\Config\Data\CollectionFactory;
use Magento\Framework\App\Config\ValueFactory;
use Magento\Framework\DataObject;
use PHPUnit\Framework\MockObject\MockObject;
@@ -23,15 +23,20 @@ class LoaderTest extends TestCase
protected $_model;
/**
- * @var MockObject
+ * @var MockObject&ValueFactory
*/
protected $_configValueFactory;
/**
- * @var MockObject
+ * @var MockObject&Collection
*/
protected $_configCollection;
+ /**
+ * @var MockObject&CollectionFactory
+ */
+ protected $collectionFactory;
+
protected function setUp(): void
{
$this->_configValueFactory = $this->getMockBuilder(ValueFactory::class)
@@ -39,41 +44,19 @@ protected function setUp(): void
->onlyMethods(['create'])
->disableOriginalConstructor()
->getMock();
- $this->_model = new Loader($this->_configValueFactory);
+ $this->collectionFactory = $this->getMockBuilder(CollectionFactory::class)
+ ->addMethods(['getCollection'])
+ ->onlyMethods(['create'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->_model = new Loader($this->_configValueFactory, $this->collectionFactory);
$this->_configCollection = $this->createMock(Collection::class);
- $this->_configCollection->expects(
- $this->once()
- )->method(
- 'addScopeFilter'
- )->with(
- 'scope',
- 'scopeId',
- 'section'
- )->willReturnSelf();
-
- $configDataMock = $this->createMock(Value::class);
- $this->_configValueFactory->expects(
- $this->once()
- )->method(
- 'create'
- )->willReturn(
- $configDataMock
- );
- $configDataMock->expects(
- $this->any()
- )->method(
- 'getCollection'
- )->willReturn(
- $this->_configCollection
- );
-
- $this->_configCollection->expects(
- $this->once()
- )->method(
- 'getItems'
- )->willReturn(
- [new DataObject(['path' => 'section', 'value' => 10, 'config_id' => 20])]
- );
+ $this->_configCollection->expects($this->once())->
+ method('addScopeFilter')->with('scope', 'scopeId', 'section')->willReturnSelf();
+ $this->_configValueFactory->expects($this->never())->method('create');
+ $this->collectionFactory->expects($this->any())->method('create')->willReturn($this->_configCollection);
+ $this->_configCollection->expects($this->once())->method('getItems')
+ ->willReturn([new DataObject(['path' => 'section', 'value' => 10, 'config_id' => 20])]);
}
protected function tearDown(): void
diff --git a/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php
index deb2c4ed4a483..478e75e3f06e5 100644
--- a/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php
+++ b/app/code/Magento/Config/Test/Unit/Model/ConfigTest.php
@@ -175,6 +175,8 @@ protected function setUp(): void
*/
public function testSaveDoesNotDoAnythingIfGroupsAreNotPassed(): void
{
+ $this->appConfigMock->expects($this->never())
+ ->method('reinit');
$this->configLoaderMock->expects($this->never())->method('getConfigByPath');
$this->model->save();
}
@@ -184,6 +186,8 @@ public function testSaveDoesNotDoAnythingIfGroupsAreNotPassed(): void
*/
public function testSaveEmptiesNonSetArguments(): void
{
+ $this->appConfigMock->expects($this->never())
+ ->method('reinit');
$this->structureReaderMock->expects($this->never())->method('getConfiguration');
$this->assertNull($this->model->getSection());
$this->assertNull($this->model->getWebsite());
@@ -199,6 +203,8 @@ public function testSaveEmptiesNonSetArguments(): void
*/
public function testSaveToCheckAdminSystemConfigChangedSectionEvent(): void
{
+ $this->appConfigMock->expects($this->exactly(2))
+ ->method('reinit');
$transactionMock = $this->createMock(Transaction::class);
$this->transFactoryMock->expects($this->any())->method('create')->willReturn($transactionMock);
@@ -227,6 +233,8 @@ public function testSaveToCheckAdminSystemConfigChangedSectionEvent(): void
*/
public function testDoNotSaveReadOnlyFields(): void
{
+ $this->appConfigMock->expects($this->exactly(2))
+ ->method('reinit');
$transactionMock = $this->createMock(Transaction::class);
$this->transFactoryMock->expects($this->any())->method('create')->willReturn($transactionMock);
@@ -265,6 +273,8 @@ public function testDoNotSaveReadOnlyFields(): void
*/
public function testSaveToCheckScopeDataSet(): void
{
+ $this->appConfigMock->expects($this->exactly(2))
+ ->method('reinit');
$transactionMock = $this->createMock(Transaction::class);
$this->transFactoryMock->expects($this->any())->method('create')->willReturn($transactionMock);
diff --git a/app/code/Magento/Config/etc/di.xml b/app/code/Magento/Config/etc/di.xml
index 4536bc71c6c10..5052e7d0fba87 100644
--- a/app/code/Magento/Config/etc/di.xml
+++ b/app/code/Magento/Config/etc/di.xml
@@ -380,4 +380,7 @@
\Magento\Config\Model\Config\Structure\Proxy
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductIdentitiesExtender.php b/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductIdentitiesExtender.php
index 75d8b3635d0ee..8be73279478c4 100644
--- a/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductIdentitiesExtender.php
+++ b/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductIdentitiesExtender.php
@@ -11,11 +11,12 @@
use Magento\ConfigurableProduct\Model\Product\Type\Configurable as ConfigurableType;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\Product;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Extender of product identities for child of configurable products
*/
-class ProductIdentitiesExtender
+class ProductIdentitiesExtender implements ResetAfterRequestInterface
{
/**
* @var ConfigurableType
@@ -79,4 +80,12 @@ private function getParentIdsByChild($childId)
return $this->cacheParentIdsByChild[$childId];
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->cacheParentIdsByChild = [];
+ }
}
diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php
index 7f228caeb3e46..c2f95bebdb887 100644
--- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php
+++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php
@@ -18,6 +18,7 @@
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\File\UploaderFactory;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Configurable product type implementation
@@ -31,7 +32,7 @@
* @api
* @since 100.0.2
*/
-class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType
+class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType implements ResetAfterRequestInterface
{
/**
* Product type code
@@ -1494,4 +1495,13 @@ function($attr) {
return array_unique(array_merge($productAttributes, $requiredAttributes, $usedAttributes));
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->isSaleableBySku = [];
+ }
+
}
diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php b/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php
index df2a9707f18d5..50421c6967b36 100644
--- a/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php
+++ b/app/code/Magento/ConfigurableProduct/Model/Product/VariationHandler.php
@@ -9,14 +9,14 @@
use Magento\Catalog\Model\Product\Type as ProductType;
use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
- * Variation Handler
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @api
* @since 100.0.2
*/
-class VariationHandler
+class VariationHandler implements ResetAfterRequestInterface
{
/**
* @var \Magento\Catalog\Model\Product\Gallery\Processor
@@ -52,6 +52,7 @@ class VariationHandler
/**
* @var \Magento\CatalogInventory\Api\StockConfigurationInterface
* @deprecated 100.1.0
+ * @see MSI
*/
protected $stockConfiguration;
@@ -120,6 +121,7 @@ public function generateSimpleProducts($parentProduct, $productsData)
* Prepare attribute set comprising all selected configurable attributes
*
* @deprecated 100.1.0
+ * @see prepareAttributeSet()
* @param \Magento\Catalog\Model\Product $product
* @return void
*/
@@ -198,7 +200,10 @@ protected function fillSimpleProductData(
continue;
}
- $product->setData($attribute->getAttributeCode(), $parentProduct->getData($attribute->getAttributeCode()));
+ $product->setData(
+ $attribute->getAttributeCode(),
+ $parentProduct->getData($attribute->getAttributeCode()) ?? $attribute->getDefaultValue()
+ );
}
$keysFilter = ['item_id', 'product_id', 'stock_id', 'type_id', 'website_id'];
@@ -298,4 +303,12 @@ function ($image) {
}
return $productData;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->attributes = [];
+ }
}
diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProvider.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProvider.php
index 6434cf65bfd67..c2da11493a0ee 100644
--- a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProvider.php
+++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableOptionsProvider.php
@@ -9,11 +9,12 @@
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
use Magento\Framework\App\ObjectManager;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Provide configurable child products for price calculation
*/
-class ConfigurableOptionsProvider implements ConfigurableOptionsProviderInterface
+class ConfigurableOptionsProvider implements ConfigurableOptionsProviderInterface, ResetAfterRequestInterface
{
/**
* @var Configurable
@@ -56,4 +57,12 @@ public function getProducts(ProductInterface $product)
}
return $this->products[$product->getId()];
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->products = [];
+ }
}
diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableRegularPrice.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableRegularPrice.php
index a6a6b8753824f..25f1a464e3b5c 100644
--- a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableRegularPrice.php
+++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableRegularPrice.php
@@ -8,17 +8,20 @@
use Magento\Catalog\Model\Product;
use Magento\Framework\App\ObjectManager;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Framework\Pricing\Price\AbstractPrice;
/**
* Class RegularPrice
*/
-class ConfigurableRegularPrice extends AbstractPrice implements ConfigurableRegularPriceInterface
+class ConfigurableRegularPrice extends AbstractPrice implements
+ ConfigurableRegularPriceInterface,
+ ResetAfterRequestInterface
{
/**
* Price type
*/
- const PRICE_CODE = 'regular_price';
+ public const PRICE_CODE = 'regular_price';
/**
* @var \Magento\Framework\Pricing\Amount\AmountInterface
@@ -73,7 +76,7 @@ public function __construct(
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function getValue()
{
@@ -85,7 +88,7 @@ public function getValue()
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function getAmount()
{
@@ -93,7 +96,7 @@ public function getAmount()
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function getMaxRegularAmount()
{
@@ -121,7 +124,7 @@ protected function doGetMaxRegularAmount()
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function getMinRegularAmount()
{
@@ -159,8 +162,11 @@ protected function getUsedProducts()
}
/**
+ * Retrieve Configurable Option Provider
+ *
* @return \Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsProviderInterface
* @deprecated 100.1.1
+ * @see we don't recommend this approach anymore
*/
private function getConfigurableOptionsProvider()
{
@@ -170,4 +176,12 @@ private function getConfigurableOptionsProvider()
}
return $this->configurableOptionsProvider;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->values = [];
+ }
}
diff --git a/app/code/Magento/ConfigurableProduct/README.md b/app/code/Magento/ConfigurableProduct/README.md
index 1a693b0db94eb..d495fca96f40d 100644
--- a/app/code/Magento/ConfigurableProduct/README.md
+++ b/app/code/Magento/ConfigurableProduct/README.md
@@ -35,7 +35,7 @@ Value | Description
If the `gallery_switch_strategy` variable is not defined, the default value `replace` will be used.
-For example, adding these lines of code to the theme view.xml file will set the gallery behavior to `replace` mode.
+For example, adding these lines of code to the theme view.xml file will set the gallery behavior to `replace` mode.
```xml
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AddNewProductConfigurationWithThreeAttributeActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AddNewProductConfigurationWithThreeAttributeActionGroup.xml
new file mode 100644
index 0000000000000..cc0b31a5b3c9c
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AddNewProductConfigurationWithThreeAttributeActionGroup.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Generates the Product Configurations for the 3 provided Attribute Names on the Configurable Product creation/edit page.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ChangeProductConfigurationsWithThirdInGridActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ChangeProductConfigurationsWithThirdInGridActionGroup.xml
new file mode 100644
index 0000000000000..1f9eb4d4bd3aa
--- /dev/null
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/ChangeProductConfigurationsWithThirdInGridActionGroup.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+ Edit the Product Configuration with 3rd attribute via the Admin Product grid page.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ProductConfigurableAttributeData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ProductConfigurableAttributeData.xml
index d2873e79a8b89..de0225d509d6a 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ProductConfigurableAttributeData.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ProductConfigurableAttributeData.xml
@@ -65,14 +65,14 @@
10
1
-
+
Black
sku-black
simple
2
1
- 10
+ 6
1
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection/AdminProductFormConfigurationsSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection/AdminProductFormConfigurationsSection.xml
index 22cb822dbe762..1ceb33a231c99 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection/AdminProductFormConfigurationsSection.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/AdminProductFormConfigurationsSection/AdminProductFormConfigurationsSection.xml
@@ -53,5 +53,12 @@
+
+
+
+
+
+
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.xml
index 07ed24d9bdbc7..521481c992a9a 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminApplyTierPriceForConfigurableProdTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminApplyTierPriceForConfigurableProdTest.xml
index 80adfebb57f7a..747002b55029c 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminApplyTierPriceForConfigurableProdTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminApplyTierPriceForConfigurableProdTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml
index e82efcf811359..71933dc32d2b1 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml
index f4cad6590e1f6..bc56c333ac4fb 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml
@@ -17,6 +17,7 @@
+
@@ -200,5 +201,6 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductAddNewOptionsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductAddNewOptionsTest.xml
index 7dfd0bffaa3c6..84f4f5c53bcca 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductAddNewOptionsTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductAddNewOptionsTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminConfigurableProductCreateTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminConfigurableProductCreateTest.xml
index 9e558659229cb..6048972adad3e 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminConfigurableProductCreateTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminConfigurableProductCreateTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest.xml
index 60bc6182b09b7..7c1e86105492a 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductBulkDeleteTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductBulkDeleteTest.xml
index 33a77a96a6bcd..6a8ff4bdfe6da 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductBulkDeleteTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductBulkDeleteTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductDeleteTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductDeleteTest.xml
index b2fe25e9691a3..7cf6691c2e4f4 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductDeleteTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductDeleteTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDisplayAssociatedProductPriceTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDisplayAssociatedProductPriceTest.xml
index 6093e39e899c6..4edc7e2e09504 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDisplayAssociatedProductPriceTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDisplayAssociatedProductPriceTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml
index 68d60dfa90e65..d8e5fea3cdc85 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockTestDeleteChildrenTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockTestDeleteChildrenTest.xml
index 893cfd3fa5338..dc618a6b64595 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockTestDeleteChildrenTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockTestDeleteChildrenTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductAddConfigurationTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductAddConfigurationTest.xml
index a741272bfca0c..9458226c2a552 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductAddConfigurationTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductAddConfigurationTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductDisableAnOptionTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductDisableAnOptionTest.xml
index f7e171c23f8d7..24f71a3253abe 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductDisableAnOptionTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductDisableAnOptionTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveAnOptionTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveAnOptionTest.xml
index 08ab165f95682..3d879405bb4a4 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveAnOptionTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveAnOptionTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToSimpleTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToSimpleTest.xml
index ae6de82987a99..f342e3d0c5e0d 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToSimpleTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToSimpleTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateDownloadableProductSwitchToConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateDownloadableProductSwitchToConfigurableTest.xml
index 17c7426dc547f..f19fc7556b30d 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateDownloadableProductSwitchToConfigurableTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateDownloadableProductSwitchToConfigurableTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml
index 75c699d7299a8..8e56475575f4a 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml
index 36d1eb799c19f..afedf6ee5b5c2 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml
index 990d7a7dfbc41..f2c709a6b09d5 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml
index 1f39a49fb277e..d46613b99caea 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminConfigurableProductTypeSwitchingToVirtualProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminConfigurableProductTypeSwitchingToVirtualProductTest.xml
index bf92d6c886937..40dbdf9c65e98 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminConfigurableProductTypeSwitchingToVirtualProductTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminConfigurableProductTypeSwitchingToVirtualProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToConfigurableProductTest.xml
index 63e38c5aa2c06..7dbf05cb8cfec 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToConfigurableProductTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToConfigurableProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToConfigurableProductTest.xml
index e26759892a07e..c7367ac58fef1 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToConfigurableProductTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToConfigurableProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml
index 076d55025aca5..4bd2af5240f0c 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductwithanOutofStockItemInShoppingCartTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductwithanOutofStockItemInShoppingCartTest.xml
index c8bc4541015d7..76e86ac218f17 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductwithanOutofStockItemInShoppingCartTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductwithanOutofStockItemInShoppingCartTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/CustomerReorderConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/CustomerReorderConfigurableProductTest.xml
index c620023ae102f..ba685f577991f 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/CustomerReorderConfigurableProductTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/CustomerReorderConfigurableProductTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoErrorForMiniCartItemEditTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoErrorForMiniCartItemEditTest.xml
index 06942f69672e4..b71daa0aac9bc 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoErrorForMiniCartItemEditTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoErrorForMiniCartItemEditTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml
index b75dd590dbbf1..1cefe06bff514 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NoOptionAvailableToConfigureDisabledProductTest.xml
@@ -129,7 +129,7 @@
-
+
@@ -145,7 +145,7 @@
-
+
@@ -160,7 +160,7 @@
-
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductBasicInfoTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductBasicInfoTest.xml
index 9fe38d3fd6117..71493e47d6e27 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductBasicInfoTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductBasicInfoTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCanAddToCartTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCanAddToCartTest.xml
index 0348570f63909..965165eeae4f7 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCanAddToCartTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCanAddToCartTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCantAddToCartTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCantAddToCartTest.xml
index c4dcccc53c8d8..17e03496d2b47 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCantAddToCartTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductCantAddToCartTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductOptionsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductOptionsTest.xml
index a2d1b2c077f2d..62d94edc10893 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductOptionsTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductDetailsTest/StorefrontConfigurableProductOptionsTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/SpecialPriceForConfigurableProductBasedOnVisualSwatchAttributeTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/SpecialPriceForConfigurableProductBasedOnVisualSwatchAttributeTest.xml
index 678c6f99a9f2a..66fd0544be274 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/SpecialPriceForConfigurableProductBasedOnVisualSwatchAttributeTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/SpecialPriceForConfigurableProductBasedOnVisualSwatchAttributeTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductAddToCartTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductAddToCartTest.xml
index b23f59ffbc861..4de10a9c3264e 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductAddToCartTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductAddToCartTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductListViewTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductListViewTest.xml
index 8f7924c3f3094..b36986813298b 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductListViewTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductListViewTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontGalleryConfigurableProductWithVisualSwatchAttributePrependMediaTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontGalleryConfigurableProductWithVisualSwatchAttributePrependMediaTest.xml
index ea4a0607d1d4b..0e3466957277c 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontGalleryConfigurableProductWithVisualSwatchAttributePrependMediaTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontGalleryConfigurableProductWithVisualSwatchAttributePrependMediaTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml
index ea309271abace..1f90819b5cb15 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml
index 5f68a14a36193..0c6365ad5aa17 100644
--- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml
+++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/summary.js b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/summary.js
index f5c9382af0bc3..240fa180fd871 100644
--- a/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/summary.js
+++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/variations/steps/summary.js
@@ -135,20 +135,8 @@ define([
if (productId && !images.file) {
images = product.images;
}
- productDataFromGrid = _.pick(
- productDataFromGrid,
- 'sku',
- 'name',
- 'weight',
- 'status',
- 'price',
- 'qty'
- );
+ productDataFromGrid = this.prepareProductDataFromGrid(productDataFromGrid);
- if (productDataFromGrid.hasOwnProperty('qty')) {
- productDataFromGrid[this.quantityFieldName] = productDataFromGrid.qty;
- }
- delete productDataFromGrid.qty;
product = _.pick(
product || {},
'sku',
@@ -288,6 +276,32 @@ define([
* Back.
*/
back: function () {
+ },
+
+ /**
+ * Prepare product data from grid to have all the current fields values
+ *
+ * @param {Object} productDataFromGrid
+ * @return {Object}
+ */
+ prepareProductDataFromGrid: function (productDataFromGrid) {
+ productDataFromGrid = _.pick(
+ productDataFromGrid,
+ 'sku',
+ 'name',
+ 'weight',
+ 'status',
+ 'price',
+ 'qty'
+ );
+
+ if (productDataFromGrid.hasOwnProperty('qty')) {
+ productDataFromGrid[this.quantityFieldName] = productDataFromGrid.qty;
+ }
+
+ delete productDataFromGrid.qty;
+
+ return productDataFromGrid;
}
});
});
diff --git a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js
index cbe840c95795f..9d19500bf6054 100644
--- a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js
+++ b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js
@@ -279,7 +279,7 @@ define([
_configureElement: function (element) {
this.simpleProduct = this._getSimpleProductId(element);
- if (element.value) {
+ if (element.value && element.config) {
this.options.state[element.config.id] = element.value;
if (element.nextSetting) {
@@ -298,9 +298,11 @@ define([
}
this._reloadPrice();
- this._displayRegularPriceBlock(this.simpleProduct);
- this._displayTierPriceBlock(this.simpleProduct);
- this._displayNormalPriceLabel();
+ if (element.config) {
+ this._displayRegularPriceBlock(this.simpleProduct);
+ this._displayTierPriceBlock(this.simpleProduct);
+ this._displayNormalPriceLabel();
+ }
this._changeProductImage();
},
@@ -372,7 +374,7 @@ define([
*/
_sortImages: function (images) {
return _.sortBy(images, function (image) {
- return image.position;
+ return parseInt(image.position, 10);
});
},
@@ -439,8 +441,10 @@ define([
filteredSalableProducts;
this._clearSelect(element);
- element.options[0] = new Option('', '');
- element.options[0].innerHTML = this.options.spConfig.chooseText;
+ if (element.options) {
+ element.options[0] = new Option('', '');
+ element.options[0].innerHTML = this.options.spConfig.chooseText;
+ }
prevConfig = false;
if (element.prevSetting) {
@@ -552,8 +556,10 @@ define([
_clearSelect: function (element) {
var i;
- for (i = element.options.length - 1; i >= 0; i--) {
- element.remove(i);
+ if (element.options) {
+ for (i = element.options.length - 1; i >= 0; i--) {
+ element.remove(i);
+ }
}
},
@@ -585,26 +591,31 @@ define([
_getPrices: function () {
var prices = {},
elements = _.toArray(this.options.settings),
- allowedProduct;
+ allowedProduct,
+ selected,
+ config,
+ priceValue;
_.each(elements, function (element) {
- var selected = element.options[element.selectedIndex],
- config = selected && selected.config,
+ if (element.options) {
+ selected = element.options[element.selectedIndex];
+ config = selected && selected.config;
priceValue = this._calculatePrice({});
- if (config && config.allowedProducts.length === 1) {
- priceValue = this._calculatePrice(config);
- } else if (element.value) {
- allowedProduct = this._getAllowedProductWithMinPrice(config.allowedProducts);
- priceValue = this._calculatePrice({
- 'allowedProducts': [
- allowedProduct
- ]
- });
- }
+ if (config && config.allowedProducts.length === 1) {
+ priceValue = this._calculatePrice(config);
+ } else if (element.value) {
+ allowedProduct = this._getAllowedProductWithMinPrice(config.allowedProducts);
+ priceValue = this._calculatePrice({
+ 'allowedProducts': [
+ allowedProduct
+ ]
+ });
+ }
- if (!_.isEmpty(priceValue)) {
- prices.prices = priceValue;
+ if (!_.isEmpty(priceValue)) {
+ prices.prices = priceValue;
+ }
}
}, this);
@@ -664,19 +675,23 @@ define([
_getSimpleProductId: function (element) {
// TODO: Rewrite algorithm. It should return ID of
// simple product based on selected options.
- var allOptions = element.config.options,
- value = element.value,
+ var allOptions,
+ value,
config;
- config = _.filter(allOptions, function (option) {
- return option.id === value;
- });
- config = _.first(config);
+ if (element.config) {
+ allOptions = element.config.options;
+ value = element.value;
- return _.isEmpty(config) ?
- undefined :
- _.first(config.allowedProducts);
+ config = _.filter(allOptions, function (option) {
+ return option.id === value;
+ });
+ config = _.first(config);
+ return _.isEmpty(config) ?
+ undefined :
+ _.first(config.allowedProducts);
+ }
},
/**
diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php
index fa8b669a1bdd3..8e57ae7cf5ba7 100644
--- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php
+++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php
@@ -17,11 +17,12 @@
use Magento\Framework\App\ObjectManager;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\GraphQl\Query\Uid;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Collection for fetching options for all configurable options pulled back in result set.
*/
-class Collection
+class Collection implements ResetAfterRequestInterface
{
/**
* Option type name
@@ -159,4 +160,13 @@ function ($value) use ($attribute) {
return $this->attributeMap;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->productIds = [];
+ $this->attributeMap = [];
+ }
}
diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Product/Price/Provider.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Product/Price/Provider.php
index c42a020a2fb6f..b9158cc89176f 100644
--- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Product/Price/Provider.php
+++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Product/Price/Provider.php
@@ -7,11 +7,11 @@
namespace Magento\ConfigurableProductGraphQl\Model\Resolver\Product\Price;
-use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus;
use Magento\Catalog\Pricing\Price\FinalPrice;
use Magento\Catalog\Pricing\Price\RegularPrice;
use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderInterface;
-use Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsProviderInterface;
+use Magento\ConfigurableProduct\Pricing\Price\ConfigurableOptionsProviderInterfaceFactory;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Framework\Pricing\Amount\AmountInterface;
use Magento\Framework\Pricing\Amount\BaseFactory;
use Magento\Framework\Pricing\SaleableInterface;
@@ -19,13 +19,18 @@
/**
* Provides product prices for configurable products
*/
-class Provider implements ProviderInterface
+class Provider implements ProviderInterface, ResetAfterRequestInterface
{
/**
* @var ConfigurableOptionsProviderInterface
*/
private $optionsProvider;
+ /**
+ * @var ConfigurableOptionsProviderInterfaceFactory
+ */
+ private $optionsProviderFactory;
+
/**
* @var BaseFactory
*/
@@ -48,14 +53,15 @@ class Provider implements ProviderInterface
];
/**
- * @param ConfigurableOptionsProviderInterface $optionsProvider
+ * @param ConfigurableOptionsProviderInterfaceFactory $optionsProviderFactory
* @param BaseFactory $amountFactory
*/
public function __construct(
- ConfigurableOptionsProviderInterface $optionsProvider,
+ ConfigurableOptionsProviderInterfaceFactory $optionsProviderFactory,
BaseFactory $amountFactory
) {
- $this->optionsProvider = $optionsProvider;
+ $this->optionsProvider = $optionsProviderFactory->create();
+ $this->optionsProviderFactory = $optionsProviderFactory;
$this->amountFactory = $amountFactory;
}
@@ -144,4 +150,16 @@ private function getMaximalPrice(SaleableInterface $product, string $code): Amou
return $this->maximalPrice[$code][$product->getId()] ?? $this->amountFactory->create(['amount' => null]);
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState():void
+ {
+ $this->minimalPrice[RegularPrice::PRICE_CODE] = [];
+ $this->minimalPrice[FinalPrice::PRICE_CODE] = [];
+ $this->maximalPrice[RegularPrice::PRICE_CODE] = [];
+ $this->maximalPrice[FinalPrice::PRICE_CODE] = [];
+ $this->optionsProvider = $this->optionsProviderFactory->create();
+ }
}
diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php
index bc67046dee8d3..fac7e82ce4666 100644
--- a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php
+++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php
@@ -14,6 +14,7 @@
use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory;
use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\Api\SearchCriteriaBuilder;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\GraphQl\Model\Query\ContextInterface;
use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface;
use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionPostProcessor;
@@ -22,7 +23,7 @@
/**
* Collection for fetching configurable child product data.
*/
-class Collection
+class Collection implements ResetAfterRequestInterface
{
/**
* @var CollectionFactory
@@ -201,4 +202,14 @@ private function getAttributesCodes(Product $currentProduct): array
return $attributeCodes;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->parentProducts = [];
+ $this->childrenMap = [];
+ $this->attributeCodes = [];
+ }
}
diff --git a/app/code/Magento/ConfigurableProductSales/README.md b/app/code/Magento/ConfigurableProductSales/README.md
index af915a8265827..f49c6c0284d34 100644
--- a/app/code/Magento/ConfigurableProductSales/README.md
+++ b/app/code/Magento/ConfigurableProductSales/README.md
@@ -1,4 +1,4 @@
# Magento_ConfigurableProductSales module
The Magento_ConfigurableProductSales module checks that the selected options of order item are still presented in
-Catalog. Returns true if the previously ordered item configuration is still available.
\ No newline at end of file
+Catalog. Returns true if the previously ordered item configuration is still available.
diff --git a/app/code/Magento/ContactGraphQl/Model/ContactUsValidator.php b/app/code/Magento/ContactGraphQl/Model/ContactUsValidator.php
new file mode 100644
index 0000000000000..e608df9db8b2d
--- /dev/null
+++ b/app/code/Magento/ContactGraphQl/Model/ContactUsValidator.php
@@ -0,0 +1,52 @@
+emailValidator = $emailValidator;
+ }
+
+ /**
+ * Validate input data
+ *
+ * @param string[] $input
+ * @return void
+ * @throws GraphQlInputException
+ */
+ public function execute(array $input): void
+ {
+ if (!$this->emailValidator->isValid($input['email'])) {
+ throw new GraphQlInputException(
+ __('The email address is invalid. Verify the email address and try again.')
+ );
+ }
+
+ if ($input['name'] === '') {
+ throw new GraphQlInputException(__('Name field is required.'));
+ }
+
+ if ($input['comment'] === '') {
+ throw new GraphQlInputException(__('Comment field is required.'));
+ }
+ }
+}
diff --git a/app/code/Magento/ContactGraphQl/Model/Resolver/ContactUs.php b/app/code/Magento/ContactGraphQl/Model/Resolver/ContactUs.php
new file mode 100644
index 0000000000000..eb6e852358579
--- /dev/null
+++ b/app/code/Magento/ContactGraphQl/Model/Resolver/ContactUs.php
@@ -0,0 +1,95 @@
+mail = $mail;
+ $this->contactConfig = $contactConfig;
+ $this->logger = $logger;
+ $this->validator = $validator;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ if (!$this->contactConfig->isEnabled()) {
+ throw new GraphQlInputException(
+ __('The contact form is unavailable.')
+ );
+ }
+
+ $input = array_map(function ($field) {
+ return $field === null ? '' : trim($field);
+ }, $args['input']);
+ $this->validator->execute($input);
+
+ try {
+ $this->mail->send($input['email'], ['data' => $input]);
+ } catch (\Exception $e) {
+ $this->logger->critical($e);
+ throw new GraphQlInputException(
+ __('An error occurred while processing your form. Please try again later.')
+ );
+ }
+
+ return [
+ 'status' => true
+ ];
+ }
+}
diff --git a/app/code/Magento/ContactGraphQl/README.md b/app/code/Magento/ContactGraphQl/README.md
new file mode 100644
index 0000000000000..0d983ddf4a4e4
--- /dev/null
+++ b/app/code/Magento/ContactGraphQl/README.md
@@ -0,0 +1,3 @@
+# ContactGraphQlPwa
+
+**ContactGraphQlPwa** provides GraphQL support for `magento/module-contact`.
diff --git a/app/code/Magento/Elasticsearch8/composer.json b/app/code/Magento/ContactGraphQl/composer.json
similarity index 50%
rename from app/code/Magento/Elasticsearch8/composer.json
rename to app/code/Magento/ContactGraphQl/composer.json
index 11a6d78f33f4a..9c08ecbb16758 100644
--- a/app/code/Magento/Elasticsearch8/composer.json
+++ b/app/code/Magento/ContactGraphQl/composer.json
@@ -1,19 +1,18 @@
{
- "name": "magento/module-elasticsearch-8",
+ "name": "magento/module-contact-graph-ql",
"description": "N/A",
+ "type": "magento2-module",
+ "config": {
+ "sort-packages": true
+ },
"require": {
"php": "~8.1.0||~8.2.0",
"magento/framework": "*",
- "magento/module-elasticsearch": "*",
- "elasticsearch/elasticsearch": "^8.5",
- "magento/module-advanced-search": "*",
- "magento/module-catalog-search": "*",
- "magento/module-search": "*"
+ "magento/module-contact": "*"
},
"suggest": {
- "magento/module-config": "*"
+ "magento/module-graph-ql": "*"
},
- "type": "magento2-module",
"license": [
"OSL-3.0",
"AFL-3.0"
@@ -23,7 +22,7 @@
"registration.php"
],
"psr-4": {
- "Magento\\Elasticsearch8\\": ""
+ "Magento\\ContactGraphQl\\": ""
}
}
}
diff --git a/app/code/Magento/ContactGraphQl/etc/graphql/di.xml b/app/code/Magento/ContactGraphQl/etc/graphql/di.xml
new file mode 100644
index 0000000000000..b46225b07eede
--- /dev/null
+++ b/app/code/Magento/ContactGraphQl/etc/graphql/di.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ - contact/contact/enabled
+
+
+
+
diff --git a/app/code/Magento/ContactGraphQl/etc/module.xml b/app/code/Magento/ContactGraphQl/etc/module.xml
new file mode 100644
index 0000000000000..801683ba43618
--- /dev/null
+++ b/app/code/Magento/ContactGraphQl/etc/module.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/app/code/Magento/ContactGraphQl/etc/schema.graphqls b/app/code/Magento/ContactGraphQl/etc/schema.graphqls
new file mode 100644
index 0000000000000..400c5471d942f
--- /dev/null
+++ b/app/code/Magento/ContactGraphQl/etc/schema.graphqls
@@ -0,0 +1,23 @@
+# Copyright © Magento, Inc. All rights reserved.
+# See COPYING.txt for license details.
+
+type Mutation {
+ contactUs(
+ input: ContactUsInput! @doc(description: "An input object that defines shopper information.")
+ ): ContactUsOutput @doc(description: "Send a 'Contact Us' email to the merchant.") @resolver(class: "Magento\\ContactGraphQl\\Model\\Resolver\\ContactUs")
+}
+
+input ContactUsInput {
+ email: String! @doc(description: "The email address of the shopper.")
+ name: String! @doc(description: "The full name of the shopper.")
+ telephone: String @doc(description: "The shopper's telephone number.")
+ comment: String! @doc(description: "The shopper's comment to the merchant.")
+}
+
+type ContactUsOutput @doc(description: "Contains the status of the request."){
+ status: Boolean! @doc(description: "Indicates whether the request was successful.")
+}
+
+type StoreConfig {
+ contact_enabled: Boolean! @doc(description: "Indicates whether the Contact Us form in enabled.")
+}
diff --git a/app/code/Magento/ContactGraphQl/registration.php b/app/code/Magento/ContactGraphQl/registration.php
new file mode 100644
index 0000000000000..27782c62d7966
--- /dev/null
+++ b/app/code/Magento/ContactGraphQl/registration.php
@@ -0,0 +1,10 @@
+
+
diff --git a/app/code/Magento/Cron/README.md b/app/code/Magento/Cron/README.md
index 445666301ade4..47238153f9cad 100644
--- a/app/code/Magento/Cron/README.md
+++ b/app/code/Magento/Cron/README.md
@@ -1,2 +1,2 @@
Cron is a module that enables scheduling of jobs. Other modules can add cron jobs by including crontab.xml in their etc directory. The command "bin/magento cron:run" should be run periodically to trigger the Cron module to run its scheduled jobs.
-This module also allows administrators to tune cron options in Magento Admin.
\ No newline at end of file
+This module also allows administrators to tune cron options in Magento Admin.
diff --git a/app/code/Magento/Csp/README.md b/app/code/Magento/Csp/README.md
index 0cd2cbb907054..6006f5cf14500 100644
--- a/app/code/Magento/Csp/README.md
+++ b/app/code/Magento/Csp/README.md
@@ -1,4 +1,5 @@
# Magento_Csp module
+
Magento_Csp implements Content Security Policies for Magento. Allows CSP configuration for Merchants,
provides a way for extension and theme developers to configure CSP headers for their extensions.
diff --git a/app/code/Magento/CurrencySymbol/README.md b/app/code/Magento/CurrencySymbol/README.md
index 39fb926e410de..c839781f5594a 100644
--- a/app/code/Magento/CurrencySymbol/README.md
+++ b/app/code/Magento/CurrencySymbol/README.md
@@ -5,11 +5,12 @@
## Controllers
### Currency Controllers
+
***CurrencySymbol\Controller\Adminhtml\System\Currency\FetchRates.php*** gets a specified currency conversion rate.
Supports all defined currencies in the system.
***CurrencySymbol\Controller\Adminhtml\System\Currency\SaveRates.php*** saves rates for defined currencies.
### Currency Symbol Controllers
+
***CurrencySymbol\Controller\Adminhtml\System\Currencysymbol\Reset.php*** resets all custom currency symbols.
***CurrencySymbol\Controller\Adminhtml\System\Currencysymbol\Save.php*** creates custom currency symbols.
-
diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/StorefrontSwitchCurrencyActionGroup.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/StorefrontSwitchCurrencyActionGroup.xml
index 77d00b09d655c..2611c4903c47f 100644
--- a/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/StorefrontSwitchCurrencyActionGroup.xml
+++ b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/StorefrontSwitchCurrencyActionGroup.xml
@@ -13,8 +13,8 @@
-
-
+
+
diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Section/StorefrontSwitchCurrencyRatesSection.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/StorefrontSwitchCurrencyRatesSection.xml
index 43512796a134d..8dc00a759c2fe 100644
--- a/app/code/Magento/CurrencySymbol/Test/Mftf/Section/StorefrontSwitchCurrencyRatesSection.xml
+++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/StorefrontSwitchCurrencyRatesSection.xml
@@ -12,5 +12,6 @@
+
diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCheckCurrencyConverterApiConfigurationTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCheckCurrencyConverterApiConfigurationTest.xml
index 91ae96e2656df..e6086c427a708 100644
--- a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCheckCurrencyConverterApiConfigurationTest.xml
+++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCheckCurrencyConverterApiConfigurationTest.xml
@@ -18,6 +18,8 @@
+
+
diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencyRatesNavigateMenuTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencyRatesNavigateMenuTest.xml
index 7c1b918a0528a..6600c46d836d7 100644
--- a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencyRatesNavigateMenuTest.xml
+++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencyRatesNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencySymbolsNavigateMenuTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencySymbolsNavigateMenuTest.xml
index 77b2fc9f32330..a7b6b9d36f9ad 100644
--- a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencySymbolsNavigateMenuTest.xml
+++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminStoresCurrencySymbolsNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Customer/Api/AccountManagementInterface.php b/app/code/Magento/Customer/Api/AccountManagementInterface.php
index 9c607be9f217c..165233cc6a880 100644
--- a/app/code/Magento/Customer/Api/AccountManagementInterface.php
+++ b/app/code/Magento/Customer/Api/AccountManagementInterface.php
@@ -8,6 +8,7 @@
namespace Magento\Customer\Api;
use Magento\Framework\Exception\InputException;
+use Magento\Framework\Exception\LocalizedException;
/**
* Interface for managing customers accounts.
@@ -194,7 +195,7 @@ public function resendConfirmation($email, $websiteId, $redirectUrl = '');
* Check if given email is associated with a customer account in given website.
*
* @param string $customerEmail
- * @param int $websiteId If not set, will use the current websiteId
+ * @param int|null $websiteId If not set, will use the current websiteId
* @return bool
* @throws \Magento\Framework\Exception\LocalizedException
*/
diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter.php
index 0d94a01698b31..698ed0d03390b 100644
--- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter.php
+++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Newsletter.php
@@ -46,8 +46,6 @@ class Newsletter extends Generic implements TabInterface
protected $customerAccountManagement;
/**
- * Core registry
- *
* @var Registry
*/
protected $_coreRegistry = null;
@@ -414,7 +412,7 @@ protected function updateFromSession(Form $form, $customerId)
*/
public function getStatusChangedDate()
{
- $customer = $this->getCurrentCustomerId();
+ $customer = $this->getCurrentCustomer();
if ($customer === null) {
return '';
}
diff --git a/app/code/Magento/Customer/Controller/Account/Confirm.php b/app/code/Magento/Customer/Controller/Account/Confirm.php
index 71c2a86a98a6a..d215a935545eb 100644
--- a/app/code/Magento/Customer/Controller/Account/Confirm.php
+++ b/app/code/Magento/Customer/Controller/Account/Confirm.php
@@ -1,9 +1,10 @@
session = $customerSession;
$this->scopeConfig = $scopeConfig;
@@ -92,9 +114,40 @@ public function __construct(
$this->customerRepository = $customerRepository;
$this->addressHelper = $addressHelper;
$this->urlModel = $urlFactory->create();
+ $this->customerLogger = $customerLogger ?? ObjectManager::getInstance()->get(CustomerLogger::class);
parent::__construct($context);
}
+ /**
+ * Retrieve cookie manager
+ *
+ * @return \Magento\Framework\Stdlib\Cookie\PhpCookieManager
+ */
+ private function getCookieManager()
+ {
+ if (!$this->cookieMetadataManager) {
+ $this->cookieMetadataManager = \Magento\Framework\App\ObjectManager::getInstance()->get(
+ \Magento\Framework\Stdlib\Cookie\PhpCookieManager::class
+ );
+ }
+ return $this->cookieMetadataManager;
+ }
+
+ /**
+ * Retrieve cookie metadata factory
+ *
+ * @return \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory
+ */
+ private function getCookieMetadataFactory()
+ {
+ if (!$this->cookieMetadataFactory) {
+ $this->cookieMetadataFactory = \Magento\Framework\App\ObjectManager::getInstance()->get(
+ \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory::class
+ );
+ }
+ return $this->cookieMetadataFactory;
+ }
+
/**
* Confirm customer account by id and confirmation key
*
@@ -110,7 +163,7 @@ public function execute()
return $resultRedirect;
}
- $customerId = $this->getRequest()->getParam('id', false);
+ $customerId = $this->getCustomerId();
$key = $this->getRequest()->getParam('key', false);
if (empty($customerId) || empty($key)) {
$this->messageManager->addErrorMessage(__('Bad request.'));
@@ -119,10 +172,22 @@ public function execute()
}
try {
- //activate and send greeting email
+ // log in and send greeting email
$customerEmail = $this->customerRepository->getById($customerId)->getEmail();
- $this->customerAccountManagement->activate($customerEmail, $key);
- $this->messageManager->addSuccess($this->getSuccessMessage());
+ $customer = $this->customerAccountManagement->activate($customerEmail, $key);
+ $successMessage = $this->getSuccessMessage();
+ $this->session->setCustomerDataAsLoggedIn($customer);
+
+ if ($this->getCookieManager()->getCookie('mage-cache-sessid')) {
+ $metadata = $this->getCookieMetadataFactory()->createCookieMetadata();
+ $metadata->setPath('/');
+ $this->getCookieManager()->deleteCookie('mage-cache-sessid', $metadata);
+ }
+
+ if ($successMessage) {
+ $this->messageManager->addSuccess($successMessage);
+ }
+
$resultRedirect->setUrl($this->getSuccessRedirect());
return $resultRedirect;
} catch (StateException $e) {
@@ -135,33 +200,41 @@ public function execute()
return $resultRedirect->setUrl($this->_redirect->error($url));
}
+ /**
+ * Returns customer id from request
+ *
+ * @return int
+ */
+ private function getCustomerId(): int
+ {
+ return (int)$this->getRequest()->getParam('id', 0);
+ }
+
/**
* Retrieve success message
*
- * @return string
+ * @return Phrase|null
+ * @throws NoSuchEntityException
*/
protected function getSuccessMessage()
{
if ($this->addressHelper->isVatValidationEnabled()) {
- if ($this->addressHelper->getTaxCalculationAddressType() == Address::TYPE_SHIPPING) {
- // @codingStandardsIgnoreStart
- $message = __(
- 'If you are a registered VAT customer, please click here to enter your shipping address for proper VAT calculation.',
- $this->urlModel->getUrl('customer/address/edit')
- );
- // @codingStandardsIgnoreEnd
- } else {
- // @codingStandardsIgnoreStart
- $message = __(
- 'If you are a registered VAT customer, please click here to enter your billing address for proper VAT calculation.',
- $this->urlModel->getUrl('customer/address/edit')
- );
- // @codingStandardsIgnoreEnd
- }
- } else {
- $message = __('Thank you for registering with %1.', $this->storeManager->getStore()->getFrontendName());
+ return __(
+ $this->addressHelper->getTaxCalculationAddressType() == Address::TYPE_SHIPPING
+ ? 'If you are a registered VAT customer, please click here to enter your '
+ .'shipping address for proper VAT calculation.'
+ :'If you are a registered VAT customer, please click here to enter your '
+ .'billing address for proper VAT calculation.',
+ $this->urlModel->getUrl('customer/address/edit')
+ );
}
- return $message;
+
+ $customerId = $this->getCustomerId();
+ if ($customerId && $this->customerLogger->get($customerId)->getLastLoginAt()) {
+ return null;
+ }
+
+ return __('Thank you for registering with %1.', $this->storeManager->getStore()->getFrontendName());
}
/**
diff --git a/app/code/Magento/Customer/Controller/Account/EditPost.php b/app/code/Magento/Customer/Controller/Account/EditPost.php
index d616c03be6bd0..085b4ab2d3fd9 100644
--- a/app/code/Magento/Customer/Controller/Account/EditPost.php
+++ b/app/code/Magento/Customer/Controller/Account/EditPost.php
@@ -1,6 +1,5 @@
session = $customerSession;
- $this->customerAccountManagement = $customerAccountManagement;
+ $this->accountManagement = $accountManagement;
$this->customerRepository = $customerRepository;
$this->formKeyValidator = $formKeyValidator;
$this->customerExtractor = $customerExtractor;
@@ -143,6 +161,9 @@ public function __construct(
$this->addressRegistry = $addressRegistry ?: ObjectManager::getInstance()->get(AddressRegistry::class);
$this->filesystem = $filesystem ?: ObjectManager::getInstance()->get(Filesystem::class);
$this->sessionCleaner = $sessionCleaner ?: ObjectManager::getInstance()->get(SessionCleanerInterface::class);
+ $this->accountConfirmation = $accountConfirmation ?: ObjectManager::getInstance()
+ ->get(AccountConfirmation::class);
+ $this->customerUrl = $customerUrl ?: ObjectManager::getInstance()->get(Url::class);
}
/**
@@ -164,7 +185,6 @@ private function getAuthentication()
* Get email notification
*
* @return EmailNotificationInterface
- * @deprecated 100.1.0
*/
private function getEmailNotification()
{
@@ -180,7 +200,6 @@ private function getEmailNotification()
*/
public function createCsrfValidationException(RequestInterface $request): ?InvalidRequestException
{
- /** @var Redirect $resultRedirect */
$resultRedirect = $this->resultRedirectFactory->create();
$resultRedirect->setPath('*/*/edit');
@@ -203,50 +222,49 @@ public function validateForCsrf(RequestInterface $request): ?bool
*
* @return Redirect
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ * @throws SessionException
*/
public function execute()
{
- /** @var Redirect $resultRedirect */
$resultRedirect = $this->resultRedirectFactory->create();
$validFormKey = $this->formKeyValidator->validate($this->getRequest());
if ($validFormKey && $this->getRequest()->isPost()) {
- $currentCustomerDataObject = $this->getCustomerDataObject($this->session->getCustomerId());
- $customerCandidateDataObject = $this->populateNewCustomerDataObject(
- $this->_request,
- $currentCustomerDataObject
- );
+ $customer = $this->getCustomerDataObject($this->session->getCustomerId());
+ $customerCandidate = $this->populateNewCustomerDataObject($this->_request, $customer);
$attributeToDelete = $this->_request->getParam('delete_attribute_value');
if ($attributeToDelete !== null) {
- $this->deleteCustomerFileAttribute(
- $customerCandidateDataObject,
- $attributeToDelete
- );
+ $this->deleteCustomerFileAttribute($customerCandidate, $attributeToDelete);
}
try {
// whether a customer enabled change email option
- $isEmailChanged = $this->processChangeEmailRequest($currentCustomerDataObject);
+ $isEmailChanged = $this->processChangeEmailRequest($customer);
// whether a customer enabled change password option
- $isPasswordChanged = $this->changeCustomerPassword($currentCustomerDataObject->getEmail());
+ $isPasswordChanged = $this->changeCustomerPassword($customer->getEmail());
// No need to validate customer address while editing customer profile
- $this->disableAddressValidation($customerCandidateDataObject);
+ $this->disableAddressValidation($customerCandidate);
+
+ $this->customerRepository->save($customerCandidate);
+ $updatedCustomer = $this->customerRepository->getById($customerCandidate->getId());
- $this->customerRepository->save($customerCandidateDataObject);
$this->getEmailNotification()->credentialsChanged(
- $customerCandidateDataObject,
- $currentCustomerDataObject->getEmail(),
+ $updatedCustomer,
+ $customer->getEmail(),
$isPasswordChanged
);
- $this->dispatchSuccessEvent($customerCandidateDataObject);
+
+ $this->dispatchSuccessEvent($updatedCustomer);
$this->messageManager->addSuccessMessage(__('You saved the account information.'));
// logout from current session if password or email changed.
if ($isPasswordChanged || $isEmailChanged) {
$this->session->logout();
$this->session->start();
+ $this->addComplexSuccessMessage($customer, $updatedCustomer);
+
return $resultRedirect->setPath('customer/account/login');
}
return $resultRedirect->setPath('customer/account');
@@ -276,13 +294,32 @@ public function execute()
$this->session->setCustomerFormData($this->getRequest()->getPostValue());
}
- /** @var Redirect $resultRedirect */
$resultRedirect = $this->resultRedirectFactory->create();
$resultRedirect->setPath('*/*/edit');
return $resultRedirect;
}
+ /**
+ * Adds a complex success message if email confirmation is required
+ *
+ * @param CustomerInterface $outdatedCustomer
+ * @param CustomerInterface $updatedCustomer
+ * @throws LocalizedException
+ */
+ private function addComplexSuccessMessage(
+ CustomerInterface $outdatedCustomer,
+ CustomerInterface $updatedCustomer
+ ): void {
+ if (($outdatedCustomer->getEmail() !== $updatedCustomer->getEmail())
+ && $this->accountConfirmation->isCustomerEmailChangedConfirmRequired($updatedCustomer)) {
+ $this->messageManager->addComplexSuccessMessage(
+ 'confirmAccountSuccessMessage',
+ ['url' => $this->customerUrl->getEmailConfirmationUrl($updatedCustomer->getEmail())]
+ );
+ }
+ }
+
/**
* Account editing action completed successfully event
*
@@ -303,6 +340,8 @@ private function dispatchSuccessEvent(CustomerInterface $customerCandidateDataOb
* @param int $customerId
*
* @return CustomerInterface
+ * @throws LocalizedException
+ * @throws NoSuchEntityException
*/
private function getCustomerDataObject($customerId)
{
@@ -342,7 +381,7 @@ private function populateNewCustomerDataObject(
*
* @param string $email
* @return boolean
- * @throws InvalidEmailOrPasswordException|InputException
+ * @throws InvalidEmailOrPasswordException|InputException|LocalizedException
*/
protected function changeCustomerPassword($email)
{
@@ -355,7 +394,7 @@ protected function changeCustomerPassword($email)
throw new InputException(__('Password confirmation doesn\'t match entered password.'));
}
- $isPasswordChanged = $this->customerAccountManagement->changePassword($email, $currPass, $newPass);
+ $isPasswordChanged = $this->accountManagement->changePassword($email, $currPass, $newPass);
}
return $isPasswordChanged;
@@ -393,8 +432,6 @@ private function processChangeEmailRequest(CustomerInterface $currentCustomerDat
* Get Customer Mapper instance
*
* @return Mapper
- *
- * @deprecated 100.1.3
*/
private function getCustomerMapper()
{
@@ -424,6 +461,7 @@ private function disableAddressValidation($customer)
* @param CustomerInterface $customerCandidateDataObject
* @param string $attributeToDelete
* @return void
+ * @throws FileSystemException
*/
private function deleteCustomerFileAttribute(
CustomerInterface $customerCandidateDataObject,
diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php
index 4c07864f9b957..da70e7e10bd44 100644
--- a/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php
+++ b/app/code/Magento/Customer/Controller/Adminhtml/Group/Save.php
@@ -91,8 +91,7 @@ public function execute()
? [] : $this->getRequest()->getParam('customer_group_excluded_websites');
$resultRedirect = $this->resultRedirectFactory->create();
try {
- $customerGroupCode = (string)$this->getRequest()->getParam('code');
-
+ $customerGroupCode = trim((string)$this->getRequest()->getParam('code'));
if ($id !== null) {
$customerGroup = $this->groupRepository->getById((int)$id);
$customerGroupCode = $customerGroupCode ?: $customerGroup->getCode();
diff --git a/app/code/Magento/Customer/Helper/Address.php b/app/code/Magento/Customer/Helper/Address.php
index 74eee759b4abd..020b1799bb2c0 100644
--- a/app/code/Magento/Customer/Helper/Address.php
+++ b/app/code/Magento/Customer/Helper/Address.php
@@ -10,6 +10,7 @@
use Magento\Customer\Api\Data\AttributeMetadataInterface;
use Magento\Directory\Model\Country\Format;
use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Framework\View\Element\BlockInterface;
use Magento\Store\Model\ScopeInterface;
@@ -19,28 +20,31 @@
* @api
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @since 100.0.2
+ * phpcs:disable Magento2.CodeAnalysis.EmptyBlock
+ * phpcs:disable Magento2.Commenting.ClassPropertyPHPDocFormatting
*/
-class Address extends \Magento\Framework\App\Helper\AbstractHelper
+class Address extends \Magento\Framework\App\Helper\AbstractHelper implements ResetAfterRequestInterface
{
/**
* VAT Validation parameters XML paths
*/
- const XML_PATH_VIV_DISABLE_AUTO_ASSIGN_DEFAULT = 'customer/create_account/viv_disable_auto_group_assign_default';
+ public const XML_PATH_VIV_DISABLE_AUTO_ASSIGN_DEFAULT =
+ 'customer/create_account/viv_disable_auto_group_assign_default';
- const XML_PATH_VIV_ON_EACH_TRANSACTION = 'customer/create_account/viv_on_each_transaction';
+ public const XML_PATH_VIV_ON_EACH_TRANSACTION = 'customer/create_account/viv_on_each_transaction';
- const XML_PATH_VAT_VALIDATION_ENABLED = 'customer/create_account/auto_group_assign';
+ public const XML_PATH_VAT_VALIDATION_ENABLED = 'customer/create_account/auto_group_assign';
- const XML_PATH_VIV_TAX_CALCULATION_ADDRESS_TYPE = 'customer/create_account/tax_calculation_address_type';
+ public const XML_PATH_VIV_TAX_CALCULATION_ADDRESS_TYPE = 'customer/create_account/tax_calculation_address_type';
- const XML_PATH_VAT_FRONTEND_VISIBILITY = 'customer/create_account/vat_frontend_visibility';
+ public const XML_PATH_VAT_FRONTEND_VISIBILITY = 'customer/create_account/vat_frontend_visibility';
/**
* Possible customer address types
*/
- const TYPE_BILLING = 'billing';
+ public const TYPE_BILLING = 'billing';
- const TYPE_SHIPPING = 'shipping';
+ public const TYPE_SHIPPING = 'shipping';
/**
* Array of Customer Address Attributes
@@ -82,6 +86,7 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper
* @var CustomerMetadataInterface
*
* @deprecated 101.0.0
+ * phpcs:disable Magento2.Annotation.ClassPropertyPHPDocFormatting
*/
protected $_customerMetadataService;
@@ -161,7 +166,7 @@ public function getCreateUrl()
* Retrieve block renderer.
*
* @param string $renderer
- * @return \Magento\Framework\View\Element\BlockInterface
+ * @return BlockInterface
*/
public function getRenderer($renderer)
{
@@ -281,7 +286,7 @@ public function getAttributeValidationClass($attributeCode)
: $this->_addressMetadataService->getAttributeMetadata($attributeCode);
$class = $attribute ? $attribute->getFrontendClass() : '';
- } catch (NoSuchEntityException $e) {
+ } catch (NoSuchEntityException $e) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
// the attribute does not exist so just return an empty string
}
@@ -417,4 +422,14 @@ public function isAttributeVisible($code)
}
return false;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->_config = [];
+ $this->_attributes = [];
+ $this->_streetLines = [];
+ }
}
diff --git a/app/code/Magento/Customer/Model/AccountConfirmation.php b/app/code/Magento/Customer/Model/AccountConfirmation.php
index f5193bc50026f..d95308e4fbe2a 100644
--- a/app/code/Magento/Customer/Model/AccountConfirmation.php
+++ b/app/code/Magento/Customer/Model/AccountConfirmation.php
@@ -3,8 +3,11 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\Customer\Model;
+use Magento\Customer\Api\Data\CustomerInterface;
use Magento\Store\Model\ScopeInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Registry;
@@ -15,10 +18,30 @@
class AccountConfirmation
{
/**
- * Configuration path for email confirmation.
+ * Configuration path for email confirmation when creating a new customer
*/
public const XML_PATH_IS_CONFIRM = 'customer/create_account/confirm';
+ /**
+ * Configuration path for email confirmation when updating an existing customer's email
+ */
+ public const XML_PATH_IS_CONFIRM_EMAIL_CHANGED = 'customer/account_information/confirm';
+
+ /**
+ * Constant for confirmed status
+ */
+ private const ACCOUNT_CONFIRMED = 'account_confirmed';
+
+ /**
+ * Constant for confirmation required status
+ */
+ private const ACCOUNT_CONFIRMATION_REQUIRED = 'account_confirmation_required';
+
+ /**
+ * Constant for confirmation not required status
+ */
+ private const ACCOUNT_CONFIRMATION_NOT_REQUIRED = 'account_confirmation_not_required';
+
/**
* @var ScopeConfigInterface
*/
@@ -64,6 +87,54 @@ public function isConfirmationRequired($websiteId, $customerId, $customerEmail):
);
}
+ /**
+ * Check if accounts confirmation is required if email has been changed
+ *
+ * @param int|null $websiteId
+ * @param int|null $customerId
+ * @param string|null $customerEmail
+ * @return bool
+ */
+ public function isEmailChangedConfirmationRequired($websiteId, $customerId, $customerEmail): bool
+ {
+ return !$this->canSkipConfirmation($customerId, $customerEmail)
+ && $this->scopeConfig->isSetFlag(
+ self::XML_PATH_IS_CONFIRM_EMAIL_CHANGED,
+ ScopeInterface::SCOPE_WEBSITES,
+ $websiteId
+ );
+ }
+
+ /**
+ * Returns an email confirmation status if email has been changed
+ *
+ * @param CustomerInterface $customer
+ * @return string
+ */
+ private function getEmailChangedConfirmStatus(CustomerInterface $customer): string
+ {
+ $isEmailChangedConfirmationRequired = $this->isEmailChangedConfirmationRequired(
+ (int)$customer->getWebsiteId(),
+ (int)$customer->getId(),
+ $customer->getEmail()
+ );
+
+ return $isEmailChangedConfirmationRequired
+ ? $customer->getConfirmation() ? self::ACCOUNT_CONFIRMATION_REQUIRED : self::ACCOUNT_CONFIRMED
+ : self::ACCOUNT_CONFIRMATION_NOT_REQUIRED;
+ }
+
+ /**
+ * Checks if email confirmation is required for the customer
+ *
+ * @param CustomerInterface $customer
+ * @return bool
+ */
+ public function isCustomerEmailChangedConfirmRequired(CustomerInterface $customer):bool
+ {
+ return $this->getEmailChangedConfirmStatus($customer) === self::ACCOUNT_CONFIRMATION_REQUIRED;
+ }
+
/**
* Check whether confirmation may be skipped when registering using certain email address.
*
diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php
index d5689fd2b8c08..29f1ebc0e531b 100644
--- a/app/code/Magento/Customer/Model/AccountManagement.php
+++ b/app/code/Magento/Customer/Model/AccountManagement.php
@@ -3,6 +3,7 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
namespace Magento\Customer\Model;
@@ -19,6 +20,7 @@
use Magento\Customer\Model\Customer as CustomerModel;
use Magento\Customer\Model\Customer\CredentialsValidator;
use Magento\Customer\Model\ForgotPasswordToken\GetCustomerByToken;
+use Magento\Customer\Model\Logger as CustomerLogger;
use Magento\Customer\Model\Metadata\Validator;
use Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory;
use Magento\Directory\Model\AllowedCountries;
@@ -67,6 +69,11 @@
*/
class AccountManagement implements AccountManagementInterface
{
+ /**
+ * System Configuration Path for Enable/Disable Login at Guest Checkout
+ */
+ public const GUEST_CHECKOUT_LOGIN_OPTION_SYS_CONFIG = 'checkout/options/enable_guest_checkout_login';
+
/**
* Configuration paths for create account email template
*
@@ -219,7 +226,7 @@ class AccountManagement implements AccountManagementInterface
private $customerFactory;
/**
- * @var \Magento\Customer\Api\Data\ValidationResultsInterfaceFactory
+ * @var ValidationResultsInterfaceFactory
*/
private $validationResultsDataFactory;
@@ -229,7 +236,7 @@ class AccountManagement implements AccountManagementInterface
private $eventManager;
/**
- * @var \Magento\Store\Model\StoreManagerInterface
+ * @var StoreManagerInterface
*/
private $storeManager;
@@ -299,7 +306,7 @@ class AccountManagement implements AccountManagementInterface
protected $dataProcessor;
/**
- * @var \Magento\Framework\Registry
+ * @var Registry
*/
protected $registry;
@@ -319,7 +326,7 @@ class AccountManagement implements AccountManagementInterface
protected $objectFactory;
/**
- * @var \Magento\Framework\Api\ExtensibleDataObjectConverter
+ * @var ExtensibleDataObjectConverter
*/
protected $extensibleDataObjectConverter;
@@ -339,7 +346,7 @@ class AccountManagement implements AccountManagementInterface
private $emailNotification;
/**
- * @var \Magento\Eav\Model\Validator\Attribute\Backend
+ * @var Backend
*/
private $eavValidator;
@@ -388,6 +395,11 @@ class AccountManagement implements AccountManagementInterface
*/
private $authorization;
+ /**
+ * @var CustomerLogger
+ */
+ private CustomerLogger $customerLogger;
+
/**
* @param CustomerFactory $customerFactory
* @param ManagerInterface $eventManager
@@ -426,6 +438,7 @@ class AccountManagement implements AccountManagementInterface
* @param AuthorizationInterface|null $authorization
* @param AuthenticationInterface|null $authentication
* @param Backend|null $eavValidator
+ * @param CustomerLogger|null $customerLogger
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
* @SuppressWarnings(PHPMD.NPathComplexity)
@@ -469,7 +482,8 @@ public function __construct(
SessionCleanerInterface $sessionCleaner = null,
AuthorizationInterface $authorization = null,
AuthenticationInterface $authentication = null,
- Backend $eavValidator = null
+ Backend $eavValidator = null,
+ ?CustomerLogger $customerLogger = null
) {
$this->customerFactory = $customerFactory;
$this->eventManager = $eventManager;
@@ -512,6 +526,7 @@ public function __construct(
$this->authorization = $authorization ?? $objectManager->get(AuthorizationInterface::class);
$this->authentication = $authentication ?? $objectManager->get(AuthenticationInterface::class);
$this->eavValidator = $eavValidator ?? $objectManager->get(Backend::class);
+ $this->customerLogger = $customerLogger ?? $objectManager->get(CustomerLogger::class);
}
/**
@@ -562,9 +577,9 @@ public function activateById($customerId, $confirmationKey)
/**
* Activate a customer account using a key that was sent in a confirmation email.
*
- * @param \Magento\Customer\Api\Data\CustomerInterface $customer
+ * @param CustomerInterface $customer
* @param string $confirmationKey
- * @return \Magento\Customer\Api\Data\CustomerInterface
+ * @return CustomerInterface
* @throws InputException
* @throws InputMismatchException
* @throws InvalidTransitionException
@@ -586,12 +601,17 @@ private function activateCustomer($customer, $confirmationKey)
// No need to validate customer and customer address while activating customer
$this->setIgnoreValidationFlag($customer);
$this->customerRepository->save($customer);
- $this->getEmailNotification()->newAccount(
- $customer,
- 'confirmed',
- '',
- $this->storeManager->getStore()->getId()
- );
+
+ $customerLastLoginAt = $this->customerLogger->get((int)$customer->getId())->getLastLoginAt();
+ if (!$customerLastLoginAt) {
+ $this->getEmailNotification()->newAccount(
+ $customer,
+ 'confirmed',
+ '',
+ $this->storeManager->getStore()->getId()
+ );
+ }
+
return $customer;
}
@@ -615,7 +635,9 @@ public function authenticate($username, $password)
} catch (InvalidEmailOrPasswordException $e) {
throw new InvalidEmailOrPasswordException(__('Invalid login or password.'));
}
- if ($customer->getConfirmation() && $this->isConfirmationRequired($customer)) {
+
+ if ($customer->getConfirmation()
+ && ($this->isConfirmationRequired($customer) || $this->isEmailChangedConfirmationRequired($customer))) {
throw new EmailNotConfirmedException(__("This account isn't confirmed. Verify and try again."));
}
@@ -630,6 +652,21 @@ public function authenticate($username, $password)
return $customer;
}
+ /**
+ * Checks if account confirmation is required if the email address has been changed
+ *
+ * @param CustomerInterface $customer
+ * @return bool
+ */
+ private function isEmailChangedConfirmationRequired(CustomerInterface $customer): bool
+ {
+ return $this->accountConfirmation->isEmailChangedConfirmationRequired(
+ (int)$customer->getWebsiteId(),
+ (int)$customer->getId(),
+ $customer->getEmail()
+ );
+ }
+
/**
* @inheritdoc
*/
@@ -687,7 +724,7 @@ private function handleUnknownTemplate($template)
throw new InputException(
__(
'Invalid value of "%value" provided for the %fieldName field. '
- . 'Possible values: %template1 or %template2.',
+ . 'Possible values: %template1 or %template2.',
[
'value' => $template,
'fieldName' => 'template',
@@ -715,7 +752,7 @@ public function resetPassword($email, $resetToken, $newPassword)
$this->setIgnoreValidationFlag($customer);
//Validate Token and new password strength
- $this->validateResetPasswordToken($customer->getId(), $resetToken);
+ $this->validateResetPasswordToken((int)$customer->getId(), $resetToken);
$this->credentialsValidator->checkPasswordDifferentFromEmail(
$email,
$newPassword
@@ -832,13 +869,10 @@ public function getConfirmationStatus($customerId)
{
// load customer by id
$customer = $this->customerRepository->getById($customerId);
- if ($this->isConfirmationRequired($customer)) {
- if (!$customer->getConfirmation()) {
- return self::ACCOUNT_CONFIRMED;
- }
- return self::ACCOUNT_CONFIRMATION_REQUIRED;
- }
- return self::ACCOUNT_CONFIRMATION_NOT_REQUIRED;
+
+ return $this->isConfirmationRequired($customer)
+ ? $customer->getConfirmation() ? self::ACCOUNT_CONFIRMATION_REQUIRED : self::ACCOUNT_CONFIRMED
+ : self::ACCOUNT_CONFIRMATION_NOT_REQUIRED;
}
/**
@@ -848,11 +882,6 @@ public function getConfirmationStatus($customerId)
*/
public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '')
{
- $groupId = $customer->getGroupId();
- if (isset($groupId) && !$this->authorization->isAllowed(self::ADMIN_RESOURCE)) {
- $customer->setGroupId(null);
- }
-
if ($password !== null) {
$this->checkPasswordStrength($password);
$customerEmail = $customer->getEmail();
@@ -1096,7 +1125,7 @@ public function validate(CustomerInterface $customer)
$result = $this->eavValidator->isValid($customerModel);
if ($result === false && is_array($this->eavValidator->getMessages())) {
return $validationResults->setIsValid(false)->setMessages(
- // phpcs:ignore Magento2.Functions.DiscouragedFunction
+ // phpcs:ignore Magento2.Functions.DiscouragedFunction
call_user_func_array(
'array_merge',
array_values($this->eavValidator->getMessages())
@@ -1108,9 +1137,24 @@ public function validate(CustomerInterface $customer)
/**
* @inheritdoc
+ *
+ * @param string $customerEmail
+ * @param int|null $websiteId
+ * @return bool
+ * @throws LocalizedException
*/
public function isEmailAvailable($customerEmail, $websiteId = null)
{
+ $guestLoginConfig = $this->scopeConfig->getValue(
+ self::GUEST_CHECKOUT_LOGIN_OPTION_SYS_CONFIG,
+ ScopeInterface::SCOPE_WEBSITE,
+ $websiteId
+ );
+
+ if (!$guestLoginConfig) {
+ return true;
+ }
+
try {
if ($websiteId === null) {
$websiteId = $this->storeManager->getStore()->getWebsiteId();
@@ -1219,7 +1263,7 @@ public function isReadonly($customerId)
* @return $this
* @throws LocalizedException
* @deprecated 100.1.0
- * @see MAGETWO-71174
+ * @see EmailNotification::newAccount()
*/
protected function sendNewAccountEmail(
$customer,
@@ -1263,7 +1307,7 @@ protected function sendNewAccountEmail(
* @throws LocalizedException
* @throws NoSuchEntityException
* @deprecated 100.1.0
- * @see MAGETWO-71174
+ * @see EmailNotification::credentialsChanged()
*/
protected function sendPasswordResetNotificationEmail($customer)
{
@@ -1277,7 +1321,7 @@ protected function sendPasswordResetNotificationEmail($customer)
* @param int|string|null $defaultStoreId
* @return int
* @deprecated 100.1.0
- * @see MAGETWO-71174
+ * @see StoreManagerInterface::getWebsite()
* @throws LocalizedException
*/
protected function getWebsiteStoreId($customer, $defaultStoreId = null)
@@ -1295,7 +1339,7 @@ protected function getWebsiteStoreId($customer, $defaultStoreId = null)
*
* @return array
* @deprecated 100.1.0
- * @see MAGETWO-71174
+ * @see EmailNotification::TEMPLATE_TYPES
*/
protected function getTemplateTypes()
{
@@ -1329,7 +1373,7 @@ protected function getTemplateTypes()
* @return $this
* @throws MailException
* @deprecated 100.1.0
- * @see MAGETWO-71174
+ * @see EmailNotification::sendEmailTemplate()
*/
protected function sendEmailTemplate(
$customer,
@@ -1484,7 +1528,7 @@ public function changeResetPasswordLinkToken(CustomerInterface $customer, string
* @throws LocalizedException
* @throws NoSuchEntityException
* @deprecated 100.1.0
- * @see MAGETWO-71174
+ * @see EmailNotification::passwordReminder()
*/
public function sendPasswordReminderEmail($customer)
{
@@ -1514,7 +1558,7 @@ public function sendPasswordReminderEmail($customer)
* @throws LocalizedException
* @throws NoSuchEntityException
* @deprecated 100.1.0
- * @see MAGETWO-71174
+ * @see EmailNotification::passwordResetConfirmation()
*/
public function sendPasswordResetConfirmationEmail($customer)
{
@@ -1560,7 +1604,7 @@ protected function getAddressById(CustomerInterface $customer, $addressId)
* @return Data\CustomerSecure
* @throws NoSuchEntityException
* @deprecated 100.1.0
- * @see MAGETWO-71174
+ * @see EmailNotification::getFullCustomerObject()
*/
protected function getFullCustomerObject($customer)
{
@@ -1569,7 +1613,7 @@ protected function getFullCustomerObject($customer)
$mergedCustomerData = $this->customerRegistry->retrieveSecureData($customer->getId());
$customerData = $this->dataProcessor->buildOutputDataArray(
$customer,
- \Magento\Customer\Api\Data\CustomerInterface::class
+ CustomerInterface::class
);
$mergedCustomerData->addData($customerData);
$mergedCustomerData->setData('name', $this->customerViewHelper->getCustomerName($customer));
@@ -1605,8 +1649,6 @@ private function disableAddressValidation($customer)
* Get email notification
*
* @return EmailNotificationInterface
- * @deprecated 100.1.0
- * @see MAGETWO-71174
*/
private function getEmailNotification()
{
diff --git a/app/code/Magento/Customer/Model/AccountManagementApi.php b/app/code/Magento/Customer/Model/AccountManagementApi.php
index 02a05705b57ef..8b4f78ab26c77 100644
--- a/app/code/Magento/Customer/Model/AccountManagementApi.php
+++ b/app/code/Magento/Customer/Model/AccountManagementApi.php
@@ -6,16 +6,127 @@
namespace Magento\Customer\Model;
+use Magento\Customer\Api\AddressRepositoryInterface;
+use Magento\Customer\Api\CustomerMetadataInterface;
+use Magento\Customer\Api\CustomerRepositoryInterface;
use Magento\Customer\Api\Data\CustomerInterface;
+use Magento\Customer\Api\Data\ValidationResultsInterfaceFactory;
+use Magento\Customer\Helper\View as CustomerViewHelper;
+use Magento\Customer\Model\Config\Share as ConfigShare;
+use Magento\Customer\Model\Customer as CustomerModel;
+use Magento\Customer\Model\Metadata\Validator;
+use Magento\Framework\Api\ExtensibleDataObjectConverter;
+use Magento\Framework\App\Config\ScopeConfigInterface;
+use Magento\Framework\AuthorizationInterface;
+use Magento\Framework\DataObjectFactory as ObjectFactory;
+use Magento\Framework\Encryption\EncryptorInterface as Encryptor;
+use Magento\Framework\Event\ManagerInterface;
+use Magento\Framework\Exception\AuthorizationException;
+use Magento\Framework\Mail\Template\TransportBuilder;
+use Magento\Framework\Math\Random;
+use Magento\Framework\Reflection\DataObjectProcessor;
+use Magento\Framework\Registry;
+use Magento\Framework\Stdlib\DateTime;
+use Magento\Framework\Stdlib\StringUtils as StringHelper;
+use Magento\Store\Model\StoreManagerInterface;
+use Psr\Log\LoggerInterface as PsrLogger;
/**
* Account Management service implementation for external API access.
+ *
* Handle various customer account actions.
*
* @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class AccountManagementApi extends AccountManagement
{
+ /**
+ * @var AuthorizationInterface
+ */
+ private $authorization;
+
+ /**
+ * @param CustomerFactory $customerFactory
+ * @param ManagerInterface $eventManager
+ * @param StoreManagerInterface $storeManager
+ * @param Random $mathRandom
+ * @param Validator $validator
+ * @param ValidationResultsInterfaceFactory $validationResultsDataFactory
+ * @param AddressRepositoryInterface $addressRepository
+ * @param CustomerMetadataInterface $customerMetadataService
+ * @param CustomerRegistry $customerRegistry
+ * @param PsrLogger $logger
+ * @param Encryptor $encryptor
+ * @param ConfigShare $configShare
+ * @param StringHelper $stringHelper
+ * @param CustomerRepositoryInterface $customerRepository
+ * @param ScopeConfigInterface $scopeConfig
+ * @param TransportBuilder $transportBuilder
+ * @param DataObjectProcessor $dataProcessor
+ * @param Registry $registry
+ * @param CustomerViewHelper $customerViewHelper
+ * @param DateTime $dateTime
+ * @param CustomerModel $customerModel
+ * @param ObjectFactory $objectFactory
+ * @param ExtensibleDataObjectConverter $extensibleDataObjectConverter
+ * @param AuthorizationInterface $authorization
+ * @SuppressWarnings(PHPMD.ExcessiveParameterList)
+ */
+ public function __construct(
+ CustomerFactory $customerFactory,
+ ManagerInterface $eventManager,
+ StoreManagerInterface $storeManager,
+ Random $mathRandom,
+ Validator $validator,
+ ValidationResultsInterfaceFactory $validationResultsDataFactory,
+ AddressRepositoryInterface $addressRepository,
+ CustomerMetadataInterface $customerMetadataService,
+ CustomerRegistry $customerRegistry,
+ PsrLogger $logger,
+ Encryptor $encryptor,
+ ConfigShare $configShare,
+ StringHelper $stringHelper,
+ CustomerRepositoryInterface $customerRepository,
+ ScopeConfigInterface $scopeConfig,
+ TransportBuilder $transportBuilder,
+ DataObjectProcessor $dataProcessor,
+ Registry $registry,
+ CustomerViewHelper $customerViewHelper,
+ DateTime $dateTime,
+ CustomerModel $customerModel,
+ ObjectFactory $objectFactory,
+ ExtensibleDataObjectConverter $extensibleDataObjectConverter,
+ AuthorizationInterface $authorization
+ ) {
+ $this->authorization = $authorization;
+ parent::__construct(
+ $customerFactory,
+ $eventManager,
+ $storeManager,
+ $mathRandom,
+ $validator,
+ $validationResultsDataFactory,
+ $addressRepository,
+ $customerMetadataService,
+ $customerRegistry,
+ $logger,
+ $encryptor,
+ $configShare,
+ $stringHelper,
+ $customerRepository,
+ $scopeConfig,
+ $transportBuilder,
+ $dataProcessor,
+ $registry,
+ $customerViewHelper,
+ $dateTime,
+ $customerModel,
+ $objectFactory,
+ $extensibleDataObjectConverter
+ );
+ }
+
/**
* @inheritDoc
*
@@ -23,9 +134,30 @@ class AccountManagementApi extends AccountManagement
*/
public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '')
{
+ $this->validateCustomerRequest($customer);
$customer = parent::createAccount($customer, $password, $redirectUrl);
$customer->setConfirmation(null);
return $customer;
}
+
+ /**
+ * Validate anonymous request
+ *
+ * @param CustomerInterface $customer
+ * @return void
+ * @throws AuthorizationException
+ */
+ private function validateCustomerRequest(CustomerInterface $customer): void
+ {
+ $groupId = $customer->getGroupId();
+ if (isset($groupId) &&
+ !$this->authorization->isAllowed(self::ADMIN_RESOURCE)
+ ) {
+ $params = ['resources' => self::ADMIN_RESOURCE];
+ throw new AuthorizationException(
+ __("The consumer isn't authorized to access %resources.", $params)
+ );
+ }
+ }
}
diff --git a/app/code/Magento/Customer/Model/Address/AbstractAddress.php b/app/code/Magento/Customer/Model/Address/AbstractAddress.php
index f710ef6846fd6..9d55ada6d0ac5 100644
--- a/app/code/Magento/Customer/Model/Address/AbstractAddress.php
+++ b/app/code/Magento/Customer/Model/Address/AbstractAddress.php
@@ -14,6 +14,7 @@
use Magento\Customer\Model\Data\Address as AddressData;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\Model\AbstractExtensibleModel;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Address abstract model
@@ -31,11 +32,12 @@
* @method string getPostcode()
* @method bool getShouldIgnoreValidation()
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
*
* @api
* @since 100.0.2
*/
-class AbstractAddress extends AbstractExtensibleModel implements AddressModelInterface
+class AbstractAddress extends AbstractExtensibleModel implements AddressModelInterface, ResetAfterRequestInterface
{
/**
* Possible customer address types
@@ -336,7 +338,7 @@ protected function _implodeArrayValues($value)
$isScalar = true;
foreach ($value as $val) {
- if (!is_scalar($val)) {
+ if ($val !== null && !is_scalar($val)) {
$isScalar = false;
break;
}
@@ -736,4 +738,13 @@ private function processCustomAttribute(array $attribute): array
return $attribute;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ self::$_countryModels = [];
+ self::$_regionModels = [];
+ }
}
diff --git a/app/code/Magento/Customer/Model/AddressRegistry.php b/app/code/Magento/Customer/Model/AddressRegistry.php
index 1fed9d5b6b545..d29e42c1e03d8 100644
--- a/app/code/Magento/Customer/Model/AddressRegistry.php
+++ b/app/code/Magento/Customer/Model/AddressRegistry.php
@@ -7,11 +7,12 @@
namespace Magento\Customer\Model;
use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Registry for Address models
*/
-class AddressRegistry
+class AddressRegistry implements ResetAfterRequestInterface
{
/**
* @var Address[]
@@ -74,4 +75,12 @@ public function push(Address $address)
$this->registry[$address->getId()] = $address;
return $this;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->registry = [];
+ }
}
diff --git a/app/code/Magento/Customer/Model/App/FrontController/DeleteCookieWhenCustomerNotExistPlugin.php b/app/code/Magento/Customer/Model/App/FrontController/DeleteCookieWhenCustomerNotExistPlugin.php
deleted file mode 100644
index 53e170f6026f8..0000000000000
--- a/app/code/Magento/Customer/Model/App/FrontController/DeleteCookieWhenCustomerNotExistPlugin.php
+++ /dev/null
@@ -1,55 +0,0 @@
-responseHttp = $responseHttp;
- $this->session = $session;
- }
-
- /**
- * Delete the cookie when the customer is not exist before dispatch the front controller.
- *
- * @return void
- */
- public function beforeDispatch(): void
- {
- if (!$this->session->getCustomerId()) {
- $this->responseHttp->sendVary();
- }
- }
-}
diff --git a/app/code/Magento/Customer/Model/Config/Backend/Show/Customer.php b/app/code/Magento/Customer/Model/Config/Backend/Show/Customer.php
index f4418c2832855..95db7353758ad 100644
--- a/app/code/Magento/Customer/Model/Config/Backend/Show/Customer.php
+++ b/app/code/Magento/Customer/Model/Config/Backend/Show/Customer.php
@@ -3,15 +3,20 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\Customer\Model\Config\Backend\Show;
+use Magento\Config\App\Config\Source\ModularConfigSource;
use Magento\Eav\Model\Entity\Attribute\AbstractAttribute;
use Magento\Framework\App\Config\ScopeConfigInterface;
+use Magento\Framework\App\ObjectManager;
/**
* Customer Show Customer Model
*
* @author Magento Core Team
+ * @SuppressWarnings(PHPMD.UnusedPrivateField)
*/
class Customer extends \Magento\Framework\App\Config\Value
{
@@ -32,6 +37,11 @@ class Customer extends \Magento\Framework\App\Config\Value
*/
private $telephoneShowDefaultValue = 'req';
+ /**
+ * @var ModularConfigSource
+ */
+ private $configSource;
+
/**
* @var array
*/
@@ -52,6 +62,8 @@ class Customer extends \Magento\Framework\App\Config\Value
* @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource
* @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection
* @param array $data
+ * @param ModularConfigSource|null $configSource
+ * @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
\Magento\Framework\Model\Context $context,
@@ -62,11 +74,13 @@ public function __construct(
\Magento\Eav\Model\Config $eavConfig,
\Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
\Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
- array $data = []
+ array $data = [],
+ ModularConfigSource $configSource = null
) {
$this->_eavConfig = $eavConfig;
parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data);
$this->storeManager = $storeManager;
+ $this->configSource = $configSource ?: ObjectManager::getInstance()->get(ModularConfigSource::class);
}
/**
@@ -140,7 +154,8 @@ public function afterDelete()
$attributeObject->save();
}
} elseif ($this->getScope() == ScopeConfigInterface::SCOPE_TYPE_DEFAULT) {
- $valueConfig = $this->getValueConfig($this->telephoneShowDefaultValue);
+ $defaultValue = $this->configSource->get(ScopeConfigInterface::SCOPE_TYPE_DEFAULT . '/' . $this->getPath());
+ $valueConfig = $this->getValueConfig($defaultValue === [] ? '' : $defaultValue);
foreach ($this->_getAttributeObjects() as $attributeObject) {
$attributeObject->setData('is_required', $valueConfig['is_required']);
$attributeObject->setData('is_visible', $valueConfig['is_visible']);
diff --git a/app/code/Magento/Customer/Model/Customer.php b/app/code/Magento/Customer/Model/Customer.php
index 42b0f86ec6cbb..d42a8b74734e9 100644
--- a/app/code/Magento/Customer/Model/Customer.php
+++ b/app/code/Magento/Customer/Model/Customer.php
@@ -1308,7 +1308,7 @@ public function isResetPasswordLinkTokenExpired()
}
$hourDifference = floor(($currentTimestamp - $tokenTimestamp) / (60 * 60));
-
+
return $hourDifference >= $expirationPeriod;
}
diff --git a/app/code/Magento/Customer/Model/CustomerRegistry.php b/app/code/Magento/Customer/Model/CustomerRegistry.php
index 0f421c1c677ce..f05c0948ac07a 100644
--- a/app/code/Magento/Customer/Model/CustomerRegistry.php
+++ b/app/code/Magento/Customer/Model/CustomerRegistry.php
@@ -10,6 +10,7 @@
use Magento\Customer\Model\Data\CustomerSecure;
use Magento\Customer\Model\Data\CustomerSecureFactory;
use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Store\Model\StoreManagerInterface;
/**
@@ -17,9 +18,9 @@
*
* @api
*/
-class CustomerRegistry
+class CustomerRegistry implements ResetAfterRequestInterface
{
- const REGISTRY_SEPARATOR = ':';
+ public const REGISTRY_SEPARATOR = ':';
/**
* @var CustomerFactory
@@ -116,9 +117,7 @@ public function retrieveByEmail($customerEmail, $websiteId = null)
/** @var Customer $customer */
$customer = $this->customerFactory->create();
- if (isset($websiteId)) {
- $customer->setWebsiteId($websiteId);
- }
+ $customer->setWebsiteId($websiteId);
$customer->loadByEmail($customerEmail);
if (!$customer->getEmail()) {
@@ -234,4 +233,14 @@ public function push(Customer $customer)
$this->customerRegistryByEmail[$emailKey] = $customer;
return $this;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->customerRegistryById = [];
+ $this->customerRegistryByEmail = [];
+ $this->customerSecureRegistryById = [];
+ }
}
diff --git a/app/code/Magento/Customer/Model/EmailNotification.php b/app/code/Magento/Customer/Model/EmailNotification.php
index a4f85a9c4a0c9..a71cf79a4f51b 100644
--- a/app/code/Magento/Customer/Model/EmailNotification.php
+++ b/app/code/Magento/Customer/Model/EmailNotification.php
@@ -9,6 +9,8 @@
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Exception\MailException;
+use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Mail\Template\SenderResolverInterface;
use Magento\Store\Model\App\Emulation;
use Magento\Store\Model\StoreManagerInterface;
@@ -30,28 +32,28 @@ class EmailNotification implements EmailNotificationInterface
/**#@+
* Configuration paths for email templates and identities
*/
- const XML_PATH_FORGOT_EMAIL_IDENTITY = 'customer/password/forgot_email_identity';
+ public const XML_PATH_FORGOT_EMAIL_IDENTITY = 'customer/password/forgot_email_identity';
- const XML_PATH_RESET_PASSWORD_TEMPLATE = 'customer/password/reset_password_template';
+ public const XML_PATH_RESET_PASSWORD_TEMPLATE = 'customer/password/reset_password_template';
- const XML_PATH_CHANGE_EMAIL_TEMPLATE = 'customer/account_information/change_email_template';
+ public const XML_PATH_CHANGE_EMAIL_TEMPLATE = 'customer/account_information/change_email_template';
- const XML_PATH_CHANGE_EMAIL_AND_PASSWORD_TEMPLATE =
+ public const XML_PATH_CHANGE_EMAIL_AND_PASSWORD_TEMPLATE =
'customer/account_information/change_email_and_password_template';
- const XML_PATH_FORGOT_EMAIL_TEMPLATE = 'customer/password/forgot_email_template';
+ public const XML_PATH_FORGOT_EMAIL_TEMPLATE = 'customer/password/forgot_email_template';
- const XML_PATH_REMIND_EMAIL_TEMPLATE = 'customer/password/remind_email_template';
+ public const XML_PATH_REMIND_EMAIL_TEMPLATE = 'customer/password/remind_email_template';
- const XML_PATH_REGISTER_EMAIL_IDENTITY = 'customer/create_account/email_identity';
+ public const XML_PATH_REGISTER_EMAIL_IDENTITY = 'customer/create_account/email_identity';
- const XML_PATH_REGISTER_EMAIL_TEMPLATE = 'customer/create_account/email_template';
+ public const XML_PATH_REGISTER_EMAIL_TEMPLATE = 'customer/create_account/email_template';
- const XML_PATH_REGISTER_NO_PASSWORD_EMAIL_TEMPLATE = 'customer/create_account/email_no_password_template';
+ public const XML_PATH_REGISTER_NO_PASSWORD_EMAIL_TEMPLATE = 'customer/create_account/email_no_password_template';
- const XML_PATH_CONFIRM_EMAIL_TEMPLATE = 'customer/create_account/email_confirmation_template';
+ public const XML_PATH_CONFIRM_EMAIL_TEMPLATE = 'customer/create_account/email_confirmation_template';
- const XML_PATH_CONFIRMED_EMAIL_TEMPLATE = 'customer/create_account/email_confirmed_template';
+ public const XML_PATH_CONFIRMED_EMAIL_TEMPLATE = 'customer/create_account/email_confirmed_template';
/**
* self::NEW_ACCOUNT_EMAIL_REGISTERED welcome email, when confirmation is disabled
@@ -62,7 +64,7 @@ class EmailNotification implements EmailNotificationInterface
* and password is set
* self::NEW_ACCOUNT_EMAIL_CONFIRMATION email with confirmation link
*/
- const TEMPLATE_TYPES = [
+ public const TEMPLATE_TYPES = [
self::NEW_ACCOUNT_EMAIL_REGISTERED => self::XML_PATH_REGISTER_EMAIL_TEMPLATE,
self::NEW_ACCOUNT_EMAIL_REGISTERED_NO_PASSWORD => self::XML_PATH_REGISTER_NO_PASSWORD_EMAIL_TEMPLATE,
self::NEW_ACCOUNT_EMAIL_CONFIRMED => self::XML_PATH_CONFIRMED_EMAIL_TEMPLATE,
@@ -71,7 +73,9 @@ class EmailNotification implements EmailNotificationInterface
/**#@-*/
- /**#@-*/
+ /**
+ * @var CustomerRegistry
+ */
private $customerRegistry;
/**
@@ -109,6 +113,11 @@ class EmailNotification implements EmailNotificationInterface
*/
private $emulation;
+ /**
+ * @var AccountConfirmation
+ */
+ private AccountConfirmation $accountConfirmation;
+
/**
* @param CustomerRegistry $customerRegistry
* @param StoreManagerInterface $storeManager
@@ -118,6 +127,7 @@ class EmailNotification implements EmailNotificationInterface
* @param ScopeConfigInterface $scopeConfig
* @param SenderResolverInterface|null $senderResolver
* @param Emulation|null $emulation
+ * @param AccountConfirmation|null $accountConfirmation
*/
public function __construct(
CustomerRegistry $customerRegistry,
@@ -127,7 +137,8 @@ public function __construct(
DataObjectProcessor $dataProcessor,
ScopeConfigInterface $scopeConfig,
SenderResolverInterface $senderResolver = null,
- Emulation $emulation =null
+ Emulation $emulation = null,
+ ?AccountConfirmation $accountConfirmation = null
) {
$this->customerRegistry = $customerRegistry;
$this->storeManager = $storeManager;
@@ -137,6 +148,8 @@ public function __construct(
$this->scopeConfig = $scopeConfig;
$this->senderResolver = $senderResolver ?? ObjectManager::getInstance()->get(SenderResolverInterface::class);
$this->emulation = $emulation ?? ObjectManager::getInstance()->get(Emulation::class);
+ $this->accountConfirmation = $accountConfirmation ?? ObjectManager::getInstance()
+ ->get(AccountConfirmation::class);
}
/**
@@ -146,6 +159,7 @@ public function __construct(
* @param string $origCustomerEmail
* @param bool $isPasswordChanged
* @return void
+ * @throws LocalizedException
*/
public function credentialsChanged(
CustomerInterface $savedCustomer,
@@ -153,6 +167,7 @@ public function credentialsChanged(
$isPasswordChanged = false
): void {
if ($origCustomerEmail != $savedCustomer->getEmail()) {
+ $this->emailChangedConfirmation($savedCustomer);
if ($isPasswordChanged) {
$this->emailAndPasswordChanged($savedCustomer, $origCustomerEmail);
$this->emailAndPasswordChanged($savedCustomer, $savedCustomer->getEmail());
@@ -175,6 +190,8 @@ public function credentialsChanged(
* @param CustomerInterface $customer
* @param string $email
* @return void
+ * @throws MailException
+ * @throws NoSuchEntityException|LocalizedException
*/
private function emailAndPasswordChanged(CustomerInterface $customer, $email): void
{
@@ -201,6 +218,8 @@ private function emailAndPasswordChanged(CustomerInterface $customer, $email): v
* @param CustomerInterface $customer
* @param string $email
* @return void
+ * @throws MailException
+ * @throws NoSuchEntityException|LocalizedException
*/
private function emailChanged(CustomerInterface $customer, $email): void
{
@@ -226,6 +245,8 @@ private function emailChanged(CustomerInterface $customer, $email): void
*
* @param CustomerInterface $customer
* @return void
+ * @throws MailException
+ * @throws NoSuchEntityException|LocalizedException
*/
private function passwordReset(CustomerInterface $customer): void
{
@@ -255,7 +276,7 @@ private function passwordReset(CustomerInterface $customer): void
* @param int|null $storeId
* @param string $email
* @return void
- * @throws \Magento\Framework\Exception\MailException
+ * @throws MailException|LocalizedException
*/
private function sendEmailTemplate(
$customer,
@@ -293,6 +314,7 @@ private function sendEmailTemplate(
*
* @param CustomerInterface $customer
* @return CustomerSecure
+ * @throws NoSuchEntityException
*/
private function getFullCustomerObject($customer): CustomerSecure
{
@@ -312,6 +334,7 @@ private function getFullCustomerObject($customer): CustomerSecure
* @param CustomerInterface $customer
* @param int|string|null $defaultStoreId
* @return int
+ * @throws LocalizedException
*/
private function getWebsiteStoreId($customer, $defaultStoreId = null): int
{
@@ -327,6 +350,9 @@ private function getWebsiteStoreId($customer, $defaultStoreId = null): int
*
* @param CustomerInterface $customer
* @return void
+ * @throws LocalizedException
+ * @throws MailException
+ * @throws NoSuchEntityException
*/
public function passwordReminder(CustomerInterface $customer): void
{
@@ -351,6 +377,9 @@ public function passwordReminder(CustomerInterface $customer): void
*
* @param CustomerInterface $customer
* @return void
+ * @throws LocalizedException
+ * @throws MailException
+ * @throws NoSuchEntityException
*/
public function passwordResetConfirmation(CustomerInterface $customer): void
{
@@ -412,4 +441,18 @@ public function newAccount(
$storeId
);
}
+
+ /**
+ * Sending an email to confirm the email address in case the email address has been changed
+ *
+ * @param CustomerInterface $customer
+ * @throws LocalizedException
+ */
+ private function emailChangedConfirmation(CustomerInterface $customer): void
+ {
+ if (!$this->accountConfirmation->isCustomerEmailChangedConfirmRequired($customer)) {
+ return;
+ }
+ $this->newAccount($customer, self::NEW_ACCOUNT_EMAIL_CONFIRMATION, null, $customer->getStoreId());
+ }
}
diff --git a/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php b/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php
index 8e64fba4a9b08..23ce32b9e217d 100644
--- a/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php
+++ b/app/code/Magento/Customer/Model/Metadata/AttributeMetadataCache.php
@@ -11,18 +11,19 @@
use Magento\Eav\Model\Entity\Attribute;
use Magento\Framework\App\Cache\StateInterface;
use Magento\Framework\App\CacheInterface;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Framework\Serialize\SerializerInterface;
use Magento\Store\Model\StoreManagerInterface;
/**
* Cache for attribute metadata
*/
-class AttributeMetadataCache
+class AttributeMetadataCache implements ResetAfterRequestInterface
{
/**
* Cache prefix
*/
- const ATTRIBUTE_METADATA_CACHE_PREFIX = 'ATTRIBUTE_METADATA_INSTANCES_CACHE';
+ public const ATTRIBUTE_METADATA_CACHE_PREFIX = 'ATTRIBUTE_METADATA_INSTANCES_CACHE';
/**
* @var CacheInterface
@@ -155,7 +156,7 @@ public function clean()
$this->cache->clean(
[
Type::CACHE_TAG,
- Attribute::CACHE_TAG,
+ Attribute::CACHE_TAG
]
);
}
@@ -173,4 +174,12 @@ private function isEnabled()
}
return $this->isAttributeCacheEnabled;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->attributes = [];
+ }
}
diff --git a/app/code/Magento/Customer/Model/Metadata/Form/File.php b/app/code/Magento/Customer/Model/Metadata/Form/File.php
index 54b8b75c9ca3e..05788dcaf763c 100644
--- a/app/code/Magento/Customer/Model/Metadata/Form/File.php
+++ b/app/code/Magento/Customer/Model/Metadata/Form/File.php
@@ -23,6 +23,8 @@
*/
class File extends AbstractData
{
+ public const UPLOADED_FILE_SUFFIX = '_uploaded';
+
/**
* Validator for check not protected extensions
*
@@ -59,7 +61,8 @@ class File extends AbstractData
/**
* @var FileProcessorFactory
- * @deprecated 101.0.0
+ * @deprecated 101.0.0 Call fileProcessor directly from code
+ * @see $this->fileProcessor
*/
protected $fileProcessorFactory;
@@ -126,7 +129,7 @@ public function extractValue(\Magento\Framework\App\RequestInterface $request)
$attrCode = $this->getAttribute()->getAttributeCode();
// phpcs:disable Magento2.Security.Superglobal
- $uploadedFile = $request->getParam($attrCode . '_uploaded');
+ $uploadedFile = $request->getParam($attrCode . static::UPLOADED_FILE_SUFFIX);
if ($uploadedFile) {
$value = $uploadedFile;
} elseif ($this->_requestScope || !isset($_FILES[$attrCode])) {
@@ -424,7 +427,8 @@ public function outputValue($format = \Magento\Customer\Model\Metadata\ElementFa
* Get file processor
*
* @return FileProcessor
- * @deprecated 100.1.3
+ * @deprecated 100.1.3 we don’t use such approach anymore. Call fileProcessor directly
+ * @see $this->fileProcessor
*/
protected function getFileProcessor()
{
diff --git a/app/code/Magento/Customer/Model/Plugin/ClearSessionsAfterLogoutPlugin.php b/app/code/Magento/Customer/Model/Plugin/ClearSessionsAfterLogoutPlugin.php
new file mode 100644
index 0000000000000..ec837d9737595
--- /dev/null
+++ b/app/code/Magento/Customer/Model/Plugin/ClearSessionsAfterLogoutPlugin.php
@@ -0,0 +1,108 @@
+session = $customerSession;
+ $this->saveHandler = $saveHandler;
+ $this->storage = $storage;
+ $this->state = $state;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Plugin to clear session after logout
+ *
+ * @param Session $subject
+ * @param Session $result
+ * @return Session
+ * @throws LocalizedException
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterLogout(Session $subject, Session $result): Session
+ {
+ $isAreaFrontEnd = $this->state->getAreaCode() === Area::AREA_FRONTEND;
+ $previousSessions = $this->storage->getData(self::PREVIOUS_ACTIVE_SESSIONS);
+
+ if ($isAreaFrontEnd && !empty($previousSessions)) {
+ foreach ($previousSessions as $sessionId) {
+ try {
+ $this->session->start();
+ $this->saveHandler->destroy($sessionId);
+ $this->session->writeClose();
+ } catch (SessionException $e) {
+ $this->logger->error($e);
+ }
+
+ }
+ $this->storage->setData(self::PREVIOUS_ACTIVE_SESSIONS, []);
+ }
+ return $result;
+ }
+}
diff --git a/app/code/Magento/Customer/Model/Plugin/CustomerNotification.php b/app/code/Magento/Customer/Model/Plugin/CustomerNotification.php
index db694ad3295ce..ab3cf8cb7d852 100644
--- a/app/code/Magento/Customer/Model/Plugin/CustomerNotification.php
+++ b/app/code/Magento/Customer/Model/Plugin/CustomerNotification.php
@@ -3,6 +3,7 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
namespace Magento\Customer\Model\Plugin;
@@ -16,13 +17,21 @@
use Magento\Framework\App\RequestInterface;
use Magento\Framework\App\State;
use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Framework\Session\StorageInterface;
use Psr\Log\LoggerInterface;
/**
* Refresh the Customer session if `UpdateSession` notification registered
+ *
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class CustomerNotification
{
+ /**
+ * Array key for all active previous session ids.
+ */
+ private const PREVIOUS_ACTIVE_SESSIONS = 'previous_active_sessions';
+
/**
* @var Session
*/
@@ -53,6 +62,11 @@ class CustomerNotification
*/
private $request;
+ /**
+ * @var StorageInterface
+ */
+ private StorageInterface $storage;
+
/**
* Initialize dependencies.
*
@@ -61,7 +75,8 @@ class CustomerNotification
* @param State $state
* @param CustomerRepositoryInterface $customerRepository
* @param LoggerInterface $logger
- * @param RequestInterface|null $request
+ * @param RequestInterface $request
+ * @param StorageInterface|null $storage
*/
public function __construct(
Session $session,
@@ -69,7 +84,8 @@ public function __construct(
State $state,
CustomerRepositoryInterface $customerRepository,
LoggerInterface $logger,
- RequestInterface $request
+ RequestInterface $request,
+ StorageInterface $storage = null
) {
$this->session = $session;
$this->notificationStorage = $notificationStorage;
@@ -77,6 +93,7 @@ public function __construct(
$this->customerRepository = $customerRepository;
$this->logger = $logger;
$this->request = $request;
+ $this->storage = $storage ?? ObjectManager::getInstance()->get(StorageInterface::class);
}
/**
@@ -89,18 +106,33 @@ public function __construct(
*/
public function beforeExecute(ActionInterface $subject)
{
- $customerId = $this->session->getCustomerId();
-
- if ($this->isFrontendRequest() && $this->isPostRequest() && $this->isSessionUpdateRegisteredFor($customerId)) {
- try {
- $this->session->regenerateId();
- $customer = $this->customerRepository->getById($customerId);
- $this->session->setCustomerData($customer);
- $this->session->setCustomerGroupId($customer->getGroupId());
- $this->notificationStorage->remove(NotificationStorage::UPDATE_CUSTOMER_SESSION, $customer->getId());
- } catch (NoSuchEntityException $e) {
- $this->logger->error($e);
+ $customerId = (int)$this->session->getCustomerId();
+
+ if (!$this->isFrontendRequest()
+ || !$this->isPostRequest()
+ || !$this->isSessionUpdateRegisteredFor($customerId)) {
+ return;
+ }
+
+ try {
+ $oldSessionId = $this->session->getSessionId();
+ $previousSessions = $this->storage->getData(self::PREVIOUS_ACTIVE_SESSIONS);
+
+ if (empty($previousSessions)) {
+ $previousSessions = [];
}
+ $previousSessions[] = $oldSessionId;
+ $this->storage->setData(self::PREVIOUS_ACTIVE_SESSIONS, $previousSessions);
+ $this->session->regenerateId();
+ $customer = $this->customerRepository->getById($customerId);
+ $this->session->setCustomerData($customer);
+ $this->session->setCustomerGroupId($customer->getGroupId());
+ $this->notificationStorage->remove(
+ NotificationStorage::UPDATE_CUSTOMER_SESSION,
+ $customer->getId()
+ );
+ } catch (NoSuchEntityException $e) {
+ $this->logger->error($e);
}
}
@@ -131,8 +163,8 @@ private function isFrontendRequest(): bool
* @param int $customerId
* @return bool
*/
- private function isSessionUpdateRegisteredFor($customerId): bool
+ private function isSessionUpdateRegisteredFor(int $customerId): bool
{
- return $this->notificationStorage->isExists(NotificationStorage::UPDATE_CUSTOMER_SESSION, $customerId);
+ return (bool)$this->notificationStorage->isExists(NotificationStorage::UPDATE_CUSTOMER_SESSION, $customerId);
}
}
diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php b/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php
index c7b44288bc85f..ffb5f41a40687 100644
--- a/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php
+++ b/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php
@@ -94,6 +94,15 @@ public function __construct(
);
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ parent::_resetState();
+ $this->_idFieldName = 'entity_id';
+ }
+
/**
* @inheritdoc
*/
diff --git a/app/code/Magento/Customer/Model/ResourceModel/Customer.php b/app/code/Magento/Customer/Model/ResourceModel/Customer.php
index c065f85aa6483..9ae14f68923fa 100644
--- a/app/code/Magento/Customer/Model/ResourceModel/Customer.php
+++ b/app/code/Magento/Customer/Model/ResourceModel/Customer.php
@@ -7,12 +7,25 @@
namespace Magento\Customer\Model\ResourceModel;
+use Magento\Customer\Api\Data\CustomerInterface;
use Magento\Customer\Model\AccountConfirmation;
use Magento\Customer\Model\Customer\NotificationStorage;
+use Magento\Eav\Model\Entity\Context;
+use Magento\Eav\Model\Entity\VersionControl\AbstractEntity;
+use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\ObjectManager;
+use Magento\Framework\DataObject;
+use Magento\Framework\DB\Select;
use Magento\Framework\Exception\AlreadyExistsException;
+use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite;
+use Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot;
+use Magento\Framework\Stdlib\DateTime;
use Magento\Framework\Validator\Exception as ValidatorException;
use Magento\Framework\Encryption\EncryptorInterface;
+use Magento\Framework\Validator\Factory;
+use Magento\Store\Model\StoreManagerInterface;
/**
* Customer entity resource model
@@ -21,27 +34,27 @@
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @since 100.0.2
*/
-class Customer extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity
+class Customer extends AbstractEntity
{
/**
- * @var \Magento\Framework\Validator\Factory
+ * @var Factory
*/
protected $_validatorFactory;
/**
* Core store config
*
- * @var \Magento\Framework\App\Config\ScopeConfigInterface
+ * @var ScopeConfigInterface
*/
protected $_scopeConfig;
/**
- * @var \Magento\Framework\Stdlib\DateTime
+ * @var DateTime
*/
protected $dateTime;
/**
- * @var \Magento\Store\Model\StoreManagerInterface
+ * @var StoreManagerInterface
*/
protected $storeManager;
@@ -63,26 +76,26 @@ class Customer extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity
/**
* 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
- * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
- * @param \Magento\Framework\Validator\Factory $validatorFactory
- * @param \Magento\Framework\Stdlib\DateTime $dateTime
- * @param \Magento\Store\Model\StoreManagerInterface $storeManager
+ * @param Context $context
+ * @param Snapshot $entitySnapshot
+ * @param RelationComposite $entityRelationComposite
+ * @param ScopeConfigInterface $scopeConfig
+ * @param Factory $validatorFactory
+ * @param DateTime $dateTime
+ * @param StoreManagerInterface $storeManager
* @param array $data
- * @param AccountConfirmation $accountConfirmation
+ * @param AccountConfirmation|null $accountConfirmation
* @param EncryptorInterface|null $encryptor
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
- \Magento\Eav\Model\Entity\Context $context,
- \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot,
- \Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite $entityRelationComposite,
- \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
- \Magento\Framework\Validator\Factory $validatorFactory,
- \Magento\Framework\Stdlib\DateTime $dateTime,
- \Magento\Store\Model\StoreManagerInterface $storeManager,
+ Context $context,
+ Snapshot $entitySnapshot,
+ RelationComposite $entityRelationComposite,
+ ScopeConfigInterface $scopeConfig,
+ Factory $validatorFactory,
+ DateTime $dateTime,
+ StoreManagerInterface $storeManager,
$data = [],
AccountConfirmation $accountConfirmation = null,
EncryptorInterface $encryptor = null
@@ -120,16 +133,16 @@ protected function _getDefaultAttributes()
/**
* Check customer scope, email and confirmation key before saving
*
- * @param \Magento\Framework\DataObject|\Magento\Customer\Api\Data\CustomerInterface $customer
+ * @param DataObject|CustomerInterface $customer
*
* @return $this
* @throws AlreadyExistsException
* @throws ValidatorException
- * @throws \Magento\Framework\Exception\NoSuchEntityException
+ * @throws NoSuchEntityException
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
- protected function _beforeSave(\Magento\Framework\DataObject $customer)
+ protected function _beforeSave(DataObject $customer)
{
/** @var \Magento\Customer\Model\Customer $customer */
if ($customer->getStoreId() === null) {
@@ -169,13 +182,7 @@ protected function _beforeSave(\Magento\Framework\DataObject $customer)
}
// set confirmation key logic
- if (!$customer->getId() &&
- $this->accountConfirmation->isConfirmationRequired(
- $customer->getWebsiteId(),
- $customer->getId(),
- $customer->getEmail()
- )
- ) {
+ if ($this->isConfirmationRequired($customer)) {
$customer->setConfirmation($customer->getRandomConfirmationKey());
}
// remove customer confirmation key from database, if empty
@@ -195,6 +202,51 @@ protected function _beforeSave(\Magento\Framework\DataObject $customer)
return $this;
}
+ /**
+ * Checks if customer email verification is required
+ *
+ * @param DataObject|CustomerInterface $customer
+ * @return bool
+ */
+ private function isConfirmationRequired(DataObject $customer): bool
+ {
+ return $this->isNewCustomerConfirmationRequired($customer)
+ || $this->isExistingCustomerConfirmationRequired($customer);
+ }
+
+ /**
+ * Checks if customer email verification is required for a new customer
+ *
+ * @param DataObject|CustomerInterface $customer
+ * @return bool
+ */
+ private function isNewCustomerConfirmationRequired(DataObject $customer): bool
+ {
+ return !$customer->getId()
+ && $this->accountConfirmation->isConfirmationRequired(
+ $customer->getWebsiteId(),
+ $customer->getId(),
+ $customer->getEmail()
+ );
+ }
+
+ /**
+ * Checks if customer email verification is required for an existing customer
+ *
+ * @param DataObject|CustomerInterface $customer
+ * @return bool
+ */
+ private function isExistingCustomerConfirmationRequired(DataObject $customer): bool
+ {
+ return $customer->getId()
+ && $customer->dataHasChangedFor('email')
+ && $this->accountConfirmation->isEmailChangedConfirmationRequired(
+ (int)$customer->getWebsiteId(),
+ (int)$customer->getId(),
+ $customer->getEmail()
+ );
+ }
+
/**
* Validate customer entity
*
@@ -231,10 +283,10 @@ private function getNotificationStorage()
/**
* Save customer addresses and set default addresses in attributes backend
*
- * @param \Magento\Framework\DataObject $customer
+ * @param DataObject $customer
* @return $this
*/
- protected function _afterSave(\Magento\Framework\DataObject $customer)
+ protected function _afterSave(DataObject $customer)
{
$this->getNotificationStorage()->add(
NotificationStorage::UPDATE_CUSTOMER_SESSION,
@@ -250,9 +302,9 @@ protected function _afterSave(\Magento\Framework\DataObject $customer)
/**
* Retrieve select object for loading base entity row
*
- * @param \Magento\Framework\DataObject $object
+ * @param DataObject $object
* @param string|int $rowId
- * @return \Magento\Framework\DB\Select
+ * @return Select
*/
protected function _getLoadRowSelect($object, $rowId)
{
@@ -270,7 +322,7 @@ protected function _getLoadRowSelect($object, $rowId)
* @param \Magento\Customer\Model\Customer $customer
* @param string $email
* @return $this
- * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws LocalizedException
*/
public function loadByEmail(\Magento\Customer\Model\Customer $customer, $email)
{
@@ -285,7 +337,7 @@ public function loadByEmail(\Magento\Customer\Model\Customer $customer, $email)
if ($customer->getSharingConfig()->isWebsiteScope()) {
if (!$customer->hasData('website_id')) {
- throw new \Magento\Framework\Exception\LocalizedException(
+ throw new LocalizedException(
__("A customer website ID wasn't specified. The ID must be specified to use the website scope.")
);
}
@@ -390,10 +442,10 @@ public function getWebsiteId($customerId)
/**
* Custom setter of increment ID if its needed
*
- * @param \Magento\Framework\DataObject $object
+ * @param DataObject $object
* @return $this
*/
- public function setNewIncrementId(\Magento\Framework\DataObject $object)
+ public function setNewIncrementId(DataObject $object)
{
if ($this->_scopeConfig->getValue(
\Magento\Customer\Model\Customer::XML_PATH_GENERATE_HUMAN_FRIENDLY_ID,
@@ -419,7 +471,7 @@ public function changeResetPasswordLinkToken(\Magento\Customer\Model\Customer $c
if (is_string($passwordLinkToken) && !empty($passwordLinkToken)) {
$customer->setRpToken($passwordLinkToken);
$customer->setRpTokenCreatedAt(
- (new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT)
+ (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT)
);
}
return $this;
@@ -469,7 +521,7 @@ public function updateSessionCutOff(int $customerId, int $timestamp): void
/**
* @inheritDoc
*/
- protected function _afterLoad(\Magento\Framework\DataObject $customer)
+ protected function _afterLoad(DataObject $customer)
{
if ($customer->getData('rp_token')) {
$rpToken = $customer->getData('rp_token');
diff --git a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php
index 1be4f684e9b9d..99720afc9829f 100644
--- a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php
+++ b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php
@@ -3,7 +3,6 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-
namespace Magento\Customer\Model\ResourceModel;
use Magento\Customer\Api\CustomerMetadataInterface;
@@ -407,8 +406,8 @@ public function getById($customerId)
* Retrieve customers which match a specified criteria.
*
* This call returns an array of objects, but detailed information about each object’s attributes might not be
- * included. See https://developer.adobe.com/commerce/webapi/rest/attributes#CustomerRepositoryInterface to determine
- * which call to use to get detailed information about all attributes for an object.
+ * included. See https://developer.adobe.com/commerce/webapi/rest/attributes#CustomerRepositoryInterface
+ * to determine which call to use to get detailed information about all attributes for an object.
*
* @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
* @return \Magento\Customer\Api\Data\CustomerSearchResultsInterface
@@ -540,6 +539,14 @@ private function prepareCustomerData(array $customerData): array
{
if (isset($customerData[CustomerInterface::CUSTOM_ATTRIBUTES])) {
foreach ($customerData[CustomerInterface::CUSTOM_ATTRIBUTES] as $attribute) {
+ if (empty($attribute['value'])
+ && !empty($attribute['selected_options'])
+ && is_array($attribute['selected_options'])
+ ) {
+ $attribute['value'] = implode(',', array_map(function ($option): string {
+ return $option['value'] ?? '';
+ }, $attribute['selected_options']));
+ }
$customerData[$attribute['attribute_code']] = $attribute['value'];
}
unset($customerData[CustomerInterface::CUSTOM_ATTRIBUTES]);
diff --git a/app/code/Magento/Customer/Model/Session.php b/app/code/Magento/Customer/Model/Session.php
index d0115dbee72bb..ec47afe8e065a 100644
--- a/app/code/Magento/Customer/Model/Session.php
+++ b/app/code/Magento/Customer/Model/Session.php
@@ -393,6 +393,19 @@ public function getCustomerGroupId()
return Group::NOT_LOGGED_IN_ID;
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->_customer = null;
+ $this->_customerModel = null;
+ $this->setCustomerId(null);
+ $this->setCustomerGroupId($this->groupManagement->getNotLoggedInGroup()->getId());
+ $this->_isCustomerIdChecked = null;
+ parent::_resetState();
+ }
+
/**
* Checking customer login status
*
diff --git a/app/code/Magento/Customer/Model/Vat.php b/app/code/Magento/Customer/Model/Vat.php
index ec2d90c4a7db1..6e69681a845f7 100644
--- a/app/code/Magento/Customer/Model/Vat.php
+++ b/app/code/Magento/Customer/Model/Vat.php
@@ -212,6 +212,11 @@ public function checkVatNumber($countryCode, $vatNumber, $requesterCountryCode =
$gatewayResponse->setRequestMessage(__('Please enter a valid VAT number.'));
}
} catch (\Exception $exception) {
+ $this->logger->error(
+ sprintf('VAT Number validation failed with message: %s', $exception->getMessage()),
+ ['exception' => $exception]
+ );
+
$gatewayResponse->setIsValid(false);
$gatewayResponse->setRequestDate('');
$gatewayResponse->setRequestIdentifier('');
diff --git a/app/code/Magento/Customer/Observer/Visitor/InitByRequestObserver.php b/app/code/Magento/Customer/Observer/Visitor/InitByRequestObserver.php
index 165c411a46336..4b6630c0e7a34 100644
--- a/app/code/Magento/Customer/Observer/Visitor/InitByRequestObserver.php
+++ b/app/code/Magento/Customer/Observer/Visitor/InitByRequestObserver.php
@@ -6,21 +6,44 @@
namespace Magento\Customer\Observer\Visitor;
+use Magento\Customer\Model\Visitor;
use Magento\Framework\Event\Observer;
+use Magento\Framework\Session\SessionManagerInterface;
/**
* Visitor Observer
+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
*/
class InitByRequestObserver extends AbstractVisitorObserver
{
/**
- * initByRequest
+ * @var SessionManagerInterface
+ */
+ private $sessionManager;
+
+ /**
+ * @param Visitor $visitor
+ * @param SessionManagerInterface $sessionManager
+ */
+ public function __construct(
+ Visitor $visitor,
+ SessionManagerInterface $sessionManager
+ ) {
+ parent::__construct($visitor);
+ $this->sessionManager = $sessionManager;
+ }
+
+ /**
+ * Init visitor by request
*
* @param Observer $observer
* @return void
*/
public function execute(Observer $observer)
{
+ if ($observer->getRequest()->getFullActionName() === 'customer_account_loginPost') {
+ $this->sessionManager->setVisitorData(['do_customer_login' => true]);
+ }
$this->visitor->initByRequest($observer);
}
}
diff --git a/app/code/Magento/Customer/Plugin/AsyncRequestCustomerGroupAuthorization.php b/app/code/Magento/Customer/Plugin/AsyncRequestCustomerGroupAuthorization.php
new file mode 100644
index 0000000000000..5b5c8ce1fc0ca
--- /dev/null
+++ b/app/code/Magento/Customer/Plugin/AsyncRequestCustomerGroupAuthorization.php
@@ -0,0 +1,78 @@
+authorization = $authorization;
+ }
+
+ /**
+ * Validate groupId for anonymous request
+ *
+ * @param MassSchedule $massSchedule
+ * @param string $topic
+ * @param array $entitiesArray
+ * @param string|null $groupId
+ * @param string|null $userId
+ * @return null
+ * @throws AuthorizationException
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function beforePublishMass(
+ MassSchedule $massSchedule,
+ string $topic,
+ array $entitiesArray,
+ string $groupId = null,
+ string $userId = null
+ ) {
+ foreach ($entitiesArray as $entityParams) {
+ foreach ($entityParams as $entity) {
+ if ($entity instanceof CustomerInterface) {
+ $groupId = $entity->getGroupId();
+ if (isset($groupId) && !$this->authorization->isAllowed(self::ADMIN_RESOURCE)) {
+ $params = ['resources' => self::ADMIN_RESOURCE];
+ throw new AuthorizationException(
+ __("The consumer isn't authorized to access %resources.", $params)
+ );
+ }
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/app/code/Magento/Customer/Plugin/Webapi/Controller/Rest/ValidateCustomerData.php b/app/code/Magento/Customer/Plugin/Webapi/Controller/Rest/ValidateCustomerData.php
new file mode 100644
index 0000000000000..ad2d8ed1cf974
--- /dev/null
+++ b/app/code/Magento/Customer/Plugin/Webapi/Controller/Rest/ValidateCustomerData.php
@@ -0,0 +1,56 @@
+validateInputData($inputData[self:: CUSTOMER_KEY]);
+ }
+ return [$inputData, $parameters];
+ }
+
+ /**
+ * Validates InputData
+ *
+ * @param array $inputData
+ * @return array
+ */
+ private function validateInputData(array $inputData): array
+ {
+ $result = [];
+
+ $data = array_filter($inputData, function ($k) use (&$result) {
+ $key = is_string($k) ? strtolower($k) : $k;
+ return !isset($result[$key]) && ($result[$key] = true);
+ }, ARRAY_FILTER_USE_KEY);
+
+ return array_map(function ($value) {
+ return is_array($value) ? $this->validateInputData($value) : $value;
+ }, $data);
+ }
+}
diff --git a/app/code/Magento/Customer/README.md b/app/code/Magento/Customer/README.md
index 4b2b1a4d6211d..f63b7f0633279 100644
--- a/app/code/Magento/Customer/README.md
+++ b/app/code/Magento/Customer/README.md
@@ -1,7 +1,7 @@
# Magento_Customer module
-This module serves to handle the customer data (Customer, Customer Address and Customer Group entities) both in the admin panel and the storefront.
-For customer passwords, the module implements upgrading hashes.
+This module serves to handle the customer data (Customer, Customer Address and Customer Group entities) both in the admin panel and the storefront.
+For customer passwords, the module implements upgrading hashes.
## Installation
@@ -12,6 +12,7 @@ This module is dependent on the following modules:
- `Magento_Directory`
The following modules depend on this module:
+
- `Magento_Captcha`
- `Magento_Catalog`
- `Magento_CatalogCustomerGraphQl`
@@ -31,6 +32,7 @@ The following modules depend on this module:
- `Magento_WishlistGraphQl`
The Magento_Customer module creates the following tables in the database:
+
- `customer_entity`
- `customer_entity_datetime`
- `customer_entity_decimal`
@@ -65,17 +67,19 @@ A lot of functionality in the module is on JavaScript, use [mixins](https://deve
The module dispatches the following events:
#### Block
+
- `adminhtml_block_html_before` event in the `\Magento\Customer\Block\Adminhtml\Edit\Tab\Carts::_toHtml` method. Parameters:
- `block` is a `$this` object (`Magento\Customer\Block\Adminhtml\Edit\Tab\Carts` class)
-
+
#### Controller
+
- `customer_register_success` event in the `\Magento\Customer\Controller\Account\CreatePost::execute` method. Parameters:
- `account_controller` is a `$this` object (`\Magento\Customer\Controller\Account\CreatePost` class)
- `customer` is a customer object (`\Magento\Customer\Model\Data\Customer` class)
-
+
- `customer_account_edited` event in the `\Magento\Customer\Controller\Account\EditPost::dispatchSuccessEvent` method. Parameters:
- `email` is a customer email (`string` type)
-
+
- `adminhtml_customer_prepare_save` event in the `\Magento\Customer\Controller\Adminhtml\Index\Save::execute` method. Parameters:
- `customer` is a customer object to be saved (`\Magento\Customer\Model\Data\Customer` class)
- `request` is a request object with the `\Magento\Framework\App\RequestInterface` interface.
@@ -85,6 +89,7 @@ The module dispatches the following events:
- `request` is a request object with the `\Magento\Framework\App\RequestInterface` interface.
#### Model
+
- `customer_customer_authenticated` event in the `\Magento\Customer\Model\AccountManagement::authenticate` method. Parameters:
- `model` is a customer object (`\Magento\Customer\Model\Customer` class)
- `password` is a customer password (`string` type)
@@ -134,6 +139,7 @@ For information about an event in Magento 2, see [Events and observers](https://
### Layouts
This module introduces the following layouts in the `view/frontend/layout` and `view/adminhtml/layout` directories:
+
- `view/adminhtml/layout`:
- `customer_address_edit`
- `customer_group_index`
@@ -146,7 +152,7 @@ This module introduces the following layouts in the `view/frontend/layout` and `
- `customer_index_viewcart`
- `customer_index_viewwishlist`
- `customer_online_index`
-
+
- `view/frontend/layout`:
- `customer_account`
- `customer_account_confirmation`
@@ -202,25 +208,25 @@ For more information about a layout in Magento 2, see the [Layout documentation]
#### Metadata
- `\Magento\Customer\Api\MetadataInterface`:
- - retrieve all attributes filtered by form code
+ - retrieve all attributes filtered by form code
- retrieve attribute metadata by attribute code
- get all attribute metadata
- get custom attributes metadata for the given data interface
-
+
- `\Magento\Customer\Api\MetadataManagementInterface`:
- check whether attribute is searchable in admin grid and it is allowed
- check whether attribute is filterable in admin grid and it is allowed
-
+
#### Customer address
- `\Magento\Customer\Api\AddressMetadataInterface`:
- retrieve information about customer address attributes metadata
- extends `Magento\Customer\MetadataInterface`
-
+
- `\Magento\Customer\Api\AddressMetadataManagementInterface`:
- manage customer address attributes metadata
- extends `Magento\Customer\Api\MetadataManagementInterface`
-
+
- `\Magento\Customer\Api\AddressRepositoryInterface`:
- save customer address
- get customer address by address ID
@@ -237,7 +243,7 @@ For more information about a layout in Magento 2, see the [Layout documentation]
- `\Magento\Customer\Model\Address\CustomAttributeListInterface`
- retrieve list of customer addresses custom attributes
-
+
#### Customer
- `\Magento\Customer\Api\AccountManagementInterface`:
@@ -260,21 +266,21 @@ For more information about a layout in Magento 2, see the [Layout documentation]
- retrieve default billing address for the given customer ID
- retrieve default shipping address for the given customer ID
- get hashed password
-
+
- `\Magento\Customer\Api\CustomerManagementInterface`:
- provide the number of customer count
-
+
- `\Magento\Customer\Api\CustomerMetadataInterface`:
- retrieve information about customer attributes metadata
- extends `Magento\Customer\MetadataInterface`
-
+
- `\Magento\Customer\Api\CustomerMetadataManagementInterface`:
- manage customer attributes metadata
- extends `Magento\Customer\Api\MetadataManagementInterface`
-
+
- `\Magento\Customer\Api\CustomerNameGenerationInterface`:
- concatenate all customer name parts into full customer name
-
+
- `\Magento\Customer\Api\CustomerRepositoryInterface`:
- create or update a customer
- get customer by customer EMAIL
@@ -294,19 +300,19 @@ For more information about a layout in Magento 2, see the [Layout documentation]
- send email with new customer password
- send email with reset password confirmation link
- send email with new account related information
-
+
#### Customer group
- `\Magento\Customer\Api\CustomerGroupConfigInterface`:
- set system default customer group
-
+
- `\Magento\Customer\Api\GroupManagementInterface`:
- check if customer group can be deleted
- get default customer group
- get customer group representing customers not logged in
- get all customer groups except group representing customers not logged in
- get customer group representing all customers
-
+
- `\Magento\Customer\Api\GroupRepositoryInterface`:
- save customer group
- get customer group by group ID
@@ -319,12 +325,13 @@ For more information about a layout in Magento 2, see the [Layout documentation]
- `\Magento\Customer\Model\Customer\Source\GroupSourceLoggedInOnlyInterface`
- get customer group attribute source
-
+
For information about a public API in Magento 2, see [Public interfaces & APIs](https://developer.adobe.com/commerce/php/development/components/api-concepts/).
### UI components
You can extend customer and customer address updates using the configuration files located in the `view/adminhtml/ui_component` and `view/base/ui_component` directories:
+
- `view/adminhtml/ui_component`:
- `customer_address_form`
- `customer_address_listing`
@@ -334,12 +341,13 @@ You can extend customer and customer address updates using the configuration fil
- `view/base/ui_component`:
- `customer_form`
-
+
For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/).
## Additional information
More information can get at articles:
+
- [Customer Configurations](https://docs.magento.com/user-guide/configuration/customers/customer-configuration.html)
- [Customer Attributes](https://docs.magento.com/user-guide/stores/attributes-customer.html)
- [Customer Address Attributes](https://docs.magento.com/user-guide/stores/attributes-customer-address.html)
@@ -349,11 +357,13 @@ More information can get at articles:
### Console commands
Magento_Customer provides console commands:
+
- `bin/magento customer:hash:upgrade` - upgrades a customer password hash to the latest hash algorithm
### Cron options
Cron group configuration can be set at `etc/crontab.xml`:
+
- `visitor_clean` - clean visitor's outdated records
[Learn how to configure and run cron in Magento.](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configure-cron-jobs.html).
@@ -361,6 +371,7 @@ Cron group configuration can be set at `etc/crontab.xml`:
### Indexers
This module introduces the following indexers:
+
- `customer_grid` - customer grid indexer
[Learn how to manage the indexers](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/manage-indexers.html).
diff --git a/app/code/Magento/Customer/Test/Fixture/CustomerAttribute.php b/app/code/Magento/Customer/Test/Fixture/CustomerAttribute.php
new file mode 100644
index 0000000000000..eae1c83e9f3c4
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Fixture/CustomerAttribute.php
@@ -0,0 +1,111 @@
+dataMerger = $dataMerger;
+ $this->processor = $processor;
+ $this->attributeFactory = $attributeFactory;
+ $this->resourceModelAttribute = $resourceModelAttribute;
+ $this->attributeRepository = $attributeRepository;
+ $this->customerAttributeDefaultData = $customerAttributeDefaultData;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function apply(array $data = []): ?DataObject
+ {
+ $defaultData = $this->customerAttributeDefaultData->getData();
+ if (empty($data['entity_type_id'])) {
+ throw new InvalidArgumentException(
+ __(
+ '"%field" value is required to create an attribute',
+ [
+ 'field' => 'entity_type_id'
+ ]
+ )
+ );
+ }
+
+ /** @var Attribute $attr */
+ $attr = $this->attributeFactory->createAttribute(Attribute::class, $defaultData);
+ $mergedData = $this->processor->process($this, $this->dataMerger->merge($defaultData, $data));
+ $attr->setData($mergedData);
+ if (isset($data['website_id'])) {
+ $attr->setWebsite($data['website_id']);
+ }
+ $this->resourceModelAttribute->save($attr);
+ return $attr;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function revert(DataObject $data): void
+ {
+ $this->attributeRepository->deleteById($data['attribute_id']);
+ }
+}
diff --git a/app/code/Magento/Customer/Test/Fixture/CustomerAttributeDefaultData.php b/app/code/Magento/Customer/Test/Fixture/CustomerAttributeDefaultData.php
new file mode 100644
index 0000000000000..c95cdce2d20f1
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Fixture/CustomerAttributeDefaultData.php
@@ -0,0 +1,60 @@
+ null,
+ 'attribute_id' => null,
+ 'attribute_code' => 'attribute%uniqid%',
+ 'default_frontend_label' => 'Attribute%uniqid%',
+ 'frontend_labels' => [],
+ 'frontend_input' => 'text',
+ 'backend_type' => 'varchar',
+ 'is_required' => false,
+ 'is_user_defined' => true,
+ 'note' => null,
+ 'backend_model' => null,
+ 'source_model' => null,
+ 'default_value' => null,
+ 'is_unique' => '0',
+ 'frontend_class' => null,
+ 'used_in_forms' => [],
+ 'sort_order' => 0,
+ 'attribute_set_id' => null,
+ 'attribute_group_id' => null,
+ 'input_filter' => null,
+ 'multiline_count' => 0,
+ 'validate_rules' => null,
+ 'website_id' => null,
+ 'is_visible' => 1,
+ 'scope_is_visible' => 1,
+ ];
+
+ /**
+ * @var array
+ */
+ private $defaultData;
+
+ /**
+ * @param array $defaultData
+ */
+ public function __construct(array $defaultData = [])
+ {
+ $this->defaultData = array_merge(self::DEFAULT_DATA, $defaultData);
+ }
+
+ /**
+ * Return default data
+ */
+ public function getData(): array
+ {
+ return $this->defaultData;
+ }
+}
diff --git a/app/code/Magento/Customer/Test/Fixture/CustomerGroup.php b/app/code/Magento/Customer/Test/Fixture/CustomerGroup.php
index a9a96e42a5d2a..b3649b0546c47 100644
--- a/app/code/Magento/Customer/Test/Fixture/CustomerGroup.php
+++ b/app/code/Magento/Customer/Test/Fixture/CustomerGroup.php
@@ -3,20 +3,17 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-
declare(strict_types=1);
namespace Magento\Customer\Test\Fixture;
use Magento\Customer\Api\Data\GroupInterface;
use Magento\Customer\Api\GroupRepositoryInterface;
-use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\DataObject;
use Magento\Framework\EntityManager\Hydrator;
-use Magento\Framework\Exception\LocalizedException;
-use Magento\Framework\Exception\NoSuchEntityException;
-use Magento\Tax\Api\TaxClassRepositoryInterface;
+use Magento\TestFramework\Fixture\Api\DataMerger;
use Magento\TestFramework\Fixture\Api\ServiceFactory;
+use Magento\TestFramework\Fixture\Data\ProcessorInterface;
use Magento\TestFramework\Fixture\RevertibleDataFixtureInterface;
/**
@@ -35,50 +32,46 @@ class CustomerGroup implements RevertibleDataFixtureInterface
private ServiceFactory $serviceFactory;
/**
- * @var TaxClassRepositoryInterface
+ * @var Hydrator
*/
- private TaxClassRepositoryInterface $taxClassRepository;
-
- /** @var Hydrator */
private Hydrator $hydrator;
+ /**
+ * @var DataMerger
+ */
+ private DataMerger $dataMerger;
+
+ /**
+ * @var ProcessorInterface
+ */
+ private ProcessorInterface $dataProcessor;
+
/**
* @param ServiceFactory $serviceFactory
- * @param TaxClassRepositoryInterface $taxClassRepository
- * @param SearchCriteriaBuilder $searchCriteriaBuilder
* @param Hydrator $hydrator
+ * @param DataMerger $dataMerger
+ * @param ProcessorInterface $dataProcessor
*/
public function __construct(
ServiceFactory $serviceFactory,
- TaxClassRepositoryInterface $taxClassRepository,
- Hydrator $hydrator
+ Hydrator $hydrator,
+ DataMerger $dataMerger,
+ ProcessorInterface $dataProcessor
) {
$this->serviceFactory = $serviceFactory;
- $this->taxClassRepository = $taxClassRepository;
$this->hydrator = $hydrator;
+ $this->dataMerger = $dataMerger;
+ $this->dataProcessor = $dataProcessor;
}
/**
- * {@inheritdoc}
- * @param array $data Parameters. Same format as Customer::DEFAULT_DATA.
- * @return DataObject|null
- * @throws LocalizedException
- * @throws NoSuchEntityException
+ * @inheritdoc
*/
public function apply(array $data = []): ?DataObject
{
- $customerGroupSaveService = $this->serviceFactory->create(
- GroupRepositoryInterface::class,
- 'save'
- );
- $data = self::DEFAULT_DATA;
- if (!empty($data['tax_class_id'])) {
- $data[GroupInterface::TAX_CLASS_ID] = $this->taxClassRepository->get($data['tax_class_id'])->getClassId();
- }
-
- $customerGroup = $customerGroupSaveService->execute(
+ $customerGroup = $this->serviceFactory->create(GroupRepositoryInterface::class, 'save')->execute(
[
- 'group' => $data,
+ 'group' => $this->dataProcessor->process($this, $this->dataMerger->merge(self::DEFAULT_DATA, $data))
]
);
@@ -90,8 +83,7 @@ public function apply(array $data = []): ?DataObject
*/
public function revert(DataObject $data): void
{
- $service = $this->serviceFactory->create(GroupRepositoryInterface::class, 'deleteById');
- $service->execute(
+ $this->serviceFactory->create(GroupRepositoryInterface::class, 'deleteById')->execute(
[
'id' => $data->getId()
]
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerShowCompanyActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerShowCompanyActionGroup.xml
new file mode 100644
index 0000000000000..afa6b44f6ee61
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerShowCompanyActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Goes to the customer configuration. Set "Show Company" with provided value.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFillAndSaveCustomerAddressWithoutRegionActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFillAndSaveCustomerAddressWithoutRegionActionGroup.xml
new file mode 100644
index 0000000000000..a71d0767835b4
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminFillAndSaveCustomerAddressWithoutRegionActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Fill and save customer address information omitting the region.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminMarketingInviteeCustomerGroupActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminMarketingInviteeCustomerGroupActionGroup.xml
new file mode 100644
index 0000000000000..49ef1ad9cede9
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminMarketingInviteeCustomerGroupActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminRemoveRegionFromCustomerAddressInformationActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminRemoveRegionFromCustomerAddressInformationActionGroup.xml
new file mode 100644
index 0000000000000..219bc9f4482ae
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminRemoveRegionFromCustomerAddressInformationActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ Remove region from customer address information.
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/EnterAddressDetailsActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/EnterAddressDetailsActionGroup.xml
new file mode 100644
index 0000000000000..074b93d860f19
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/EnterAddressDetailsActionGroup.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+ Removed specific page. Fills in the required details
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/FillNewCustomerAddressFieldsActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/FillNewCustomerAddressFieldsActionGroup.xml
new file mode 100644
index 0000000000000..d31bacf807ec3
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/FillNewCustomerAddressFieldsActionGroup.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ Select country before select state
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontDeleteStoredPaymentMethodActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontDeleteStoredPaymentMethodActionGroup.xml
new file mode 100644
index 0000000000000..567122c45317e
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontDeleteStoredPaymentMethodActionGroup.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ Goes to the Stored Payment Method and delete the 2nd card
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerCreateAnAccountActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerCreateAnAccountActionGroup.xml
new file mode 100644
index 0000000000000..f08a2422e0230
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontFillCustomerCreateAnAccountActionGroup.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ Fills in the provided Customer details on the Storefront Customer creation page.
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontRemoveRegionFromCustomerAddressFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontRemoveRegionFromCustomerAddressFormActionGroup.xml
new file mode 100644
index 0000000000000..245ed2c8ca465
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontRemoveRegionFromCustomerAddressFormActionGroup.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ Remove region from customer address form.
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml
index d47409cb0953d..c6eb85aacfb20 100644
--- a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml
@@ -248,6 +248,24 @@
true
true
+
+
+ Fn
+ Ln
+
+ - 7700 West Parmer Lane
+
+ Austin
+ Texas
+ US
+ United States
+ 78729
+ 512-345-6789
+ 47458714
+ Yes
+ Yes
+ RegionTX
+
John
Doe
@@ -478,4 +496,19 @@
true
613-582-4782
+
+ John
+ Doe
+ Magento
+
+ - Kapelle St.
+ - Niklaus 3
+
+ Baden
+ CH
+ Switzerland
+ Aargau
+ 5555
+ 555-55-555-55
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Data/AdminCustomerConfigData.xml b/app/code/Magento/Customer/Test/Mftf/Data/AdminCustomerConfigData.xml
index 354ff72f62c48..47b37a4a8e264 100644
--- a/app/code/Magento/Customer/Test/Mftf/Data/AdminCustomerConfigData.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Data/AdminCustomerConfigData.xml
@@ -18,6 +18,11 @@
Optional
Required
+
+ No
+ Optional
+ Required
+
customer_address-head
Name and Address Options
diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml
index 1752cb6d04c3d..fd1fc32996ae7 100644
--- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml
@@ -20,6 +20,7 @@
+
@@ -38,5 +39,6 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml
index 791ac991bb8c5..a8246586fd3cc 100644
--- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerConfigSection.xml
@@ -15,5 +15,7 @@
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection/StoreFrontCustomerAdvancedAttributesSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection/StoreFrontCustomerAdvancedAttributesSection.xml
index 099c8da065525..2ad315fa840e8 100644
--- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection/StoreFrontCustomerAdvancedAttributesSection.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerCreateFormSection/StoreFrontCustomerAdvancedAttributesSection.xml
@@ -15,7 +15,7 @@
-
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection/StorefrontCustomerSignInPopupFormSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection/StorefrontCustomerSignInPopupFormSection.xml
index f6587a757ff3e..b1eeeec6de62e 100644
--- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection/StorefrontCustomerSignInPopupFormSection.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerSignInFormSection/StorefrontCustomerSignInPopupFormSection.xml
@@ -13,6 +13,6 @@
-
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerStoredPaymentMethodsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerStoredPaymentMethodsSection.xml
index d6b586e42f28c..ba8159948701d 100644
--- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerStoredPaymentMethodsSection.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerStoredPaymentMethodsSection.xml
@@ -11,5 +11,9 @@
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingAndShippingCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingAndShippingCustomerAddressTest.xml
index e213185f28f23..0fff3a7827545 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingAndShippingCustomerAddressTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingAndShippingCustomerAddressTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerGenderInCustomersGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerGenderInCustomersGridTest.xml
index b7096625aca85..bd8d0807c3092 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerGenderInCustomersGridTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerGenderInCustomersGridTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeSingleCustomerGroupViaGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeSingleCustomerGroupViaGridTest.xml
index 205da22833cca..43459c4ecced2 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeSingleCustomerGroupViaGridTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeSingleCustomerGroupViaGridTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml
index 543f26a1aaf65..b4e21e7cba02f 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerGroupAlreadyExistsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerGroupAlreadyExistsTest.xml
index 6c57fd4dfb4b8..b88faaa63904e 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerGroupAlreadyExistsTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerGroupAlreadyExistsTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerInSecondWebsiteWithGlobalAccountSharingEnabled.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerInSecondWebsiteWithGlobalAccountSharingEnabled.xml
index a1595cfebb9f3..912a9933af35c 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerInSecondWebsiteWithGlobalAccountSharingEnabled.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerInSecondWebsiteWithGlobalAccountSharingEnabled.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml
index 78adcd9058ec2..da4552fef31e4 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml
index 7bc01cab564cc..61d39579a011f 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml
index 631349cb61960..da2da9e21e05a 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml
index cffa1bc95ac6c..d939c6919224b 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml
index 29941d7223c08..d1ce96f77a256 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml
index 7f66b657180f1..86182b230830d 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml
index 8f2e20e90d758..55bbe09b017a2 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml
index 2cd231d0bf396..e5784c1a72d04 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontTest.xml
index 6d917d3d18b43..b3c73b3d08b0e 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml
index c3dbad6708156..161cd55d8a79e 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateRetailCustomerGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateRetailCustomerGroupTest.xml
index e8198cb79262e..04636d5614a5c 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateRetailCustomerGroupTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateRetailCustomerGroupTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateTaxClassCustomerGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateTaxClassCustomerGroupTest.xml
index 3416c64a7e9d7..640f068ad8e59 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateTaxClassCustomerGroupTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateTaxClassCustomerGroupTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerAttributeChangeUpdateFromRequiredToNoDefaultScopeTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerAttributeChangeUpdateFromRequiredToNoDefaultScopeTest.xml
new file mode 100644
index 0000000000000..6a8430df0cd88
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerAttributeChangeUpdateFromRequiredToNoDefaultScopeTest.xml
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerSubscribeNewsletterPerWebsiteTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerSubscribeNewsletterPerWebsiteTest.xml
index d54977b2e2ab1..275fb21b70083 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerSubscribeNewsletterPerWebsiteTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerSubscribeNewsletterPerWebsiteTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersAllCustomersNavigateMenuTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersAllCustomersNavigateMenuTest.xml
index 207430c7bc7b9..8d3c6c50d055e 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersAllCustomersNavigateMenuTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersAllCustomersNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersCustomerGroupsNavigateMenuTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersCustomerGroupsNavigateMenuTest.xml
index bc0c3e00d75aa..952dc38fca84b 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersCustomerGroupsNavigateMenuTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersCustomerGroupsNavigateMenuTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersDeleteSystemCustomerGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersDeleteSystemCustomerGroupTest.xml
index eb10a9bf469c7..080b933cdcfdd 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersDeleteSystemCustomerGroupTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersDeleteSystemCustomerGroupTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersNowOnlineNavigateMenuTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersNowOnlineNavigateMenuTest.xml
index 8d5535a48f8a3..7303d2b083b27 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersNowOnlineNavigateMenuTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomersNowOnlineNavigateMenuTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml
index fe4a3ea39313b..fc8cc30f609cf 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml
index 5ba49cbcefba4..9e263d88b2b8f 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerTest.xml
index 059216036280a..af9a9db5809e7 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml
index 5683a75f2a382..d3830c0f1f048 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminEditCustomerWithAssociatedNewsletterQueueNewTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminEditCustomerWithAssociatedNewsletterQueueNewTest.xml
index 7929f91e778f7..12d3dda949ccc 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminEditCustomerWithAssociatedNewsletterQueueNewTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminEditCustomerWithAssociatedNewsletterQueueNewTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminEditDefaultBillingShippingCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminEditDefaultBillingShippingCustomerAddressTest.xml
index f8f3dfe19d6e2..2536fce2b9885 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminEditDefaultBillingShippingCustomerAddressTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminEditDefaultBillingShippingCustomerAddressTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSearchSelectAllTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSearchSelectAllTest.xml
index 7f1b1dfee7ce0..508d64cf18e9b 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSearchSelectAllTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSearchSelectAllTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSelectAllOnPageTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSelectAllOnPageTest.xml
index ef4dc560d4fee..9655303856a4e 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSelectAllOnPageTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSelectAllOnPageTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminPanelIsFrozenIfStorefrontIsOpenedViaCustomerViewTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminPanelIsFrozenIfStorefrontIsOpenedViaCustomerViewTest.xml
index 5f20eb9cd5e67..21b21ad772c9c 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminPanelIsFrozenIfStorefrontIsOpenedViaCustomerViewTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminPanelIsFrozenIfStorefrontIsOpenedViaCustomerViewTest.xml
@@ -17,6 +17,7 @@
+
@@ -30,7 +31,7 @@
-
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminPlaceOrderWhenCountryAllowedOnlyOnCurrentWebsiteScopeTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminPlaceOrderWhenCountryAllowedOnlyOnCurrentWebsiteScopeTest.xml
index 28ddac690a5e5..ef01fad2790dd 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminPlaceOrderWhenCountryAllowedOnlyOnCurrentWebsiteScopeTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminPlaceOrderWhenCountryAllowedOnlyOnCurrentWebsiteScopeTest.xml
@@ -15,6 +15,7 @@
+
@@ -33,7 +34,7 @@
-
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminProductBackRedirectNavigateFromCustomerViewCartProductTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminProductBackRedirectNavigateFromCustomerViewCartProductTest.xml
index 4c4175bb32198..f1a98d8705c0e 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminProductBackRedirectNavigateFromCustomerViewCartProductTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminProductBackRedirectNavigateFromCustomerViewCartProductTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml
index e1cd7146856de..beb140129b27e 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultBillingAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultBillingAddressTest.xml
index 4d833cce920ec..df9ba8cb02ac8 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultBillingAddressTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultBillingAddressTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultShippingAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultShippingAddressTest.xml
index a273d9e7431d9..f84aed64fdd49 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultShippingAddressTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultShippingAddressTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminDeleteCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminDeleteCustomerAddressTest.xml
index 3fa29aef9908e..fc6da1df7b30e 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminDeleteCustomerAddressTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminDeleteCustomerAddressTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoBillingNoShippingTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoBillingNoShippingTest.xml
index c6e370fb6a76b..847e144286c19 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoBillingNoShippingTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoBillingNoShippingTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoZipNoStateTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoZipNoStateTest.xml
index d81d7da6b5b07..9be754b34e109 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoZipNoStateTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerAddressNoZipNoStateTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerInfoFromDefaultToNonDefaultTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerInfoFromDefaultToNonDefaultTest.xml
index 09ff169b1fac8..701d2fe0d00fe 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerInfoFromDefaultToNonDefaultTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerInfoFromDefaultToNonDefaultTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml
index 7a68f48d2ab93..1a1185a083f38 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressRequiredFieldsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressRequiredFieldsTest.xml
index c990c9ff659af..12817b17563a7 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressRequiredFieldsTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressRequiredFieldsTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressStateContainValuesOnceTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressStateContainValuesOnceTest.xml
index 04bdc4e6a608c..01cb4b16c8bc4 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressStateContainValuesOnceTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressStateContainValuesOnceTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerOnGridAfterDeletingWebsiteTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerOnGridAfterDeletingWebsiteTest.xml
index c4bcc4e3854d9..c533e352f1110 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerOnGridAfterDeletingWebsiteTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerOnGridAfterDeletingWebsiteTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyDisabledCustomerGroupFieldTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyDisabledCustomerGroupFieldTest.xml
index ff60ac92853c5..977d00696d3d2 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyDisabledCustomerGroupFieldTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyDisabledCustomerGroupFieldTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml
index 000db5d79f76a..1a4edd95c4a4d 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/DeleteCustomerGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/DeleteCustomerGroupTest.xml
index e32ae04495fe5..4d9cbb2a1837c 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/DeleteCustomerGroupTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/DeleteCustomerGroupTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml
index 6e7fe4e259d7a..cee34fb258aa3 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml
@@ -23,6 +23,7 @@
+
@@ -32,6 +33,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingGlobalTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingGlobalTest.xml
index c98d20a32ba16..cdce67f5f9441 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingGlobalTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingGlobalTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingPerWebsiteTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingPerWebsiteTest.xml
index dd982077ccb69..a5bc3f744214d 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingPerWebsiteTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/ExcludeWebsiteFromCustomerGroupCustomerAccountSharingPerWebsiteTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerDefaultAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerDefaultAddressTest.xml
index d4f851ee21c25..4b2d51aaf8cde 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerDefaultAddressTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerDefaultAddressTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerNonDefaultAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerNonDefaultAddressTest.xml
index cec7f8460de5a..936ad2bc015ba 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerNonDefaultAddressTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddCustomerNonDefaultAddressTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddNewCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddNewCustomerAddressTest.xml
index c3c8bd5d7c40e..82a72506b896e 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddNewCustomerAddressTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddCustomerAddressTest/StorefrontAddNewCustomerAddressTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddProductToCartWithExpiredSessionTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddProductToCartWithExpiredSessionTest.xml
index d9349dae29329..c90934d256e7f 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddProductToCartWithExpiredSessionTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontAddProductToCartWithExpiredSessionTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontChangePasswordFormShowPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontChangePasswordFormShowPasswordTest.xml
index fe7a54bb23554..72883af621192 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontChangePasswordFormShowPasswordTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontChangePasswordFormShowPasswordTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml
index a71d4944617ae..a225c89255831 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerFormShowPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerFormShowPasswordTest.xml
index 5834772a41fab..2da490002fc29 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerFormShowPasswordTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerFormShowPasswordTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml
index 0d64ceb545831..f0f47e416ea28 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithDateOfBirthTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithDateOfBirthTest.xml
index d9e665ec7a2ad..ad1b6d0fb74ed 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithDateOfBirthTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithDateOfBirthTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml
index ef610831a721d..490b85c4176d5 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateExistingCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateExistingCustomerTest.xml
index 07ac295e5cce0..8703116303fe4 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateExistingCustomerTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateExistingCustomerTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml
index 547e0c648f43d..1a9930b2b0ef7 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml
@@ -16,6 +16,7 @@
+
@@ -106,7 +107,7 @@
-
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAddressSecurityTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAddressSecurityTest.xml
index 4309076df2146..b4e1d8134199f 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAddressSecurityTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAddressSecurityTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerRedirectToAccountDashboardAfterLoggingInTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerRedirectToAccountDashboardAfterLoggingInTest.xml
index faf03ad666bd1..be0d6736470bc 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerRedirectToAccountDashboardAfterLoggingInTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerRedirectToAccountDashboardAfterLoggingInTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterAndVerifyInAdminTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterAndVerifyInAdminTest.xml
index 2b0da367a9ec5..366d1805705b9 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterAndVerifyInAdminTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterAndVerifyInAdminTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterTest.xml
index 62bab5669307b..95981aa11418c 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerSubscribeToNewsletterTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontDeleteCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontDeleteCustomerAddressTest.xml
index 51efd4e23f5d3..479f4312a9d6b 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontDeleteCustomerAddressTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontDeleteCustomerAddressTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLockCustomerOnLoginPageTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLockCustomerOnLoginPageTest.xml
index c69c4dd071e38..d7b62c65ef97d 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLockCustomerOnLoginPageTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLockCustomerOnLoginPageTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginFormCheckDuplicateValidateMessageTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginFormCheckDuplicateValidateMessageTest.xml
index 7d7218c59d149..833386fa71162 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginFormCheckDuplicateValidateMessageTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginFormCheckDuplicateValidateMessageTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginWithIncorrectCredentialsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginWithIncorrectCredentialsTest.xml
index a7dc3c7fde7f4..f72cba7b9f336 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginWithIncorrectCredentialsTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontLoginWithIncorrectCredentialsTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontPersistedCustomerLoginTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontPersistedCustomerLoginTest.xml
index 7845d3cee44ef..5cd261c6ef2d1 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontPersistedCustomerLoginTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontPersistedCustomerLoginTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml
index 1f92b429603e6..723f8b62b9980 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressFranceTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerAddressFromGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerAddressFromGridTest.xml
index d41b1cf86da59..2f1b41fb9f13b 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerAddressFromGridTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerAddressFromGridTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerDefaultShippingAddressFromBlockTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerDefaultShippingAddressFromBlockTest.xml
index 0539b50dcaac4..5265349acd030 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerDefaultShippingAddressFromBlockTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressTest/StorefrontUpdateCustomerDefaultShippingAddressFromBlockTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressUKTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressUKTest.xml
index 7b5ad9d70fd7c..78ecb05eceebc 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressUKTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerAddressUKTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidConfirmationPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidConfirmationPasswordTest.xml
index 9e5be5abe95a3..e0685bb46fe7f 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidConfirmationPasswordTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidConfirmationPasswordTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidCurrentPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidCurrentPasswordTest.xml
index 1f2c07c325c15..f4612b58253e5 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidCurrentPasswordTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordInvalidCurrentPasswordTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordValidCurrentPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordValidCurrentPasswordTest.xml
index c977334c5f857..762ee9ef49e73 100644
--- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordValidCurrentPasswordTest.xml
+++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontUpdateCustomerPasswordTest/StorefrontUpdateCustomerPasswordValidCurrentPasswordTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Customer/Test/Mftf/Test/VerifyCustomerAddressRegionFieldTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/VerifyCustomerAddressRegionFieldTest.xml
new file mode 100644
index 0000000000000..f4b8198c097ce
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Mftf/Test/VerifyCustomerAddressRegionFieldTest.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Tab/NewsletterTest.php b/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Tab/NewsletterTest.php
index 9799470472534..1915f17238490 100644
--- a/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Tab/NewsletterTest.php
+++ b/app/code/Magento/Customer/Test/Unit/Block/Adminhtml/Edit/Tab/NewsletterTest.php
@@ -21,6 +21,7 @@
use Magento\Framework\Data\Form\Element\Select;
use Magento\Framework\Data\FormFactory;
use Magento\Framework\Registry;
+use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
use Magento\Framework\UrlInterface;
use Magento\Newsletter\Model\Subscriber;
@@ -50,8 +51,6 @@ class NewsletterTest extends TestCase
private $contextMock;
/**
- * Store manager
- *
* @var StoreManagerInterface|MockObject
*/
private $storeManager;
@@ -101,12 +100,20 @@ class NewsletterTest extends TestCase
*/
private $shareConfig;
+ /** @var TimezoneInterface|MockObject */
+ protected $localeDateMock;
+
/**
* @inheritdoc
*/
protected function setUp(): void
{
$this->contextMock = $this->createMock(Context::class);
+ $this->localeDateMock = $this->getMockBuilder(TimezoneInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['formatDateTime'])
+ ->getMockForAbstractClass();
+ $this->contextMock->expects($this->any())->method('getLocaleDate')->willReturn($this->localeDateMock);
$this->registryMock = $this->createMock(Registry::class);
$this->formFactoryMock = $this->createMock(FormFactory::class);
$this->subscriberFactoryMock = $this->createPartialMock(
@@ -161,6 +168,56 @@ public function testInitFormCanNotShowTab()
$this->assertSame($this->model, $this->model->initForm());
}
+ /**
+ * Test getSubscriberStatusChangedDate
+ *
+ * @dataProvider getChangeStatusAtDataProvider
+ */
+ public function testGetSubscriberStatusChangedDate($statusDate, $dateExpected)
+ {
+ $customerId = 999;
+ $websiteId = 1;
+ $storeId = 1;
+ $isSubscribed = true;
+
+ $this->registryMock->method('registry')->with(RegistryConstants::CURRENT_CUSTOMER_ID)
+ ->willReturn($customerId);
+
+ $customer = $this->getMockForAbstractClass(CustomerInterface::class);
+ $customer->method('getWebsiteId')->willReturn($websiteId);
+ $customer->method('getStoreId')->willReturn($storeId);
+ $customer->method('getId')->willReturn($customerId);
+ $this->customerRepository->method('getById')->with($customerId)->willReturn($customer);
+
+ $subscriberMock = $this->getMockBuilder(Subscriber::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['loadByCustomer', 'getChangeStatusAt', 'isSubscribed', 'getData'])
+ ->getMock();
+ $statusDate = new \DateTime($statusDate);
+ $this->localeDateMock->method('formatDateTime')->with($statusDate)->willReturn($dateExpected);
+
+ $subscriberMock->method('loadByCustomer')->with($customerId, $websiteId)->willReturnSelf();
+ $subscriberMock->method('getChangeStatusAt')->willReturn($statusDate);
+ $subscriberMock->method('isSubscribed')->willReturn($isSubscribed);
+ $subscriberMock->method('getData')->willReturn([]);
+ $this->subscriberFactoryMock->expects($this->any())->method('create')->willReturn($subscriberMock);
+ $this->assertEquals($dateExpected, $this->model->getStatusChangedDate());
+ }
+
+ /**
+ * Data provider for testGetSubscriberStatusChangedDate
+ *
+ * @return array
+ */
+ public function getChangeStatusAtDataProvider()
+ {
+ return
+ [
+ ['',''],
+ ['Nov 22, 2023, 1:00:00 AM','Nov 23, 2023, 2:00:00 AM']
+ ];
+ }
+
/**
* Test to initialize the form
*/
diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php
index e0e872d8fd13b..f30fd7facebbe 100644
--- a/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php
+++ b/app/code/Magento/Customer/Test/Unit/Controller/Account/ConfirmTest.php
@@ -12,6 +12,8 @@
use Magento\Customer\Api\Data\CustomerInterface;
use Magento\Customer\Controller\Account\Confirm;
use Magento\Customer\Helper\Address;
+use Magento\Customer\Model\Logger as CustomerLogger;
+use Magento\Customer\Model\Log;
use Magento\Customer\Model\Session;
use Magento\Customer\Model\Url;
use Magento\Framework\App\Action\Context;
@@ -123,6 +125,16 @@ class ConfirmTest extends TestCase
*/
protected $redirectResultMock;
+ /**
+ * @var CustomerLogger|MockObject
+ */
+ private $customerLoggerMock;
+
+ /**
+ * @var Log|MockObject
+ */
+ private $logMock;
+
/**
* @inheritdoc
*/
@@ -143,6 +155,9 @@ protected function setUp(): void
->method('create')
->willReturn($this->urlMock);
+ $this->customerLoggerMock = $this->createMock(CustomerLogger::class);
+ $this->logMock = $this->createMock(Log::class);
+
$this->customerAccountManagementMock =
$this->getMockForAbstractClass(AccountManagementInterface::class);
$this->customerDataMock = $this->getMockForAbstractClass(CustomerInterface::class);
@@ -195,7 +210,9 @@ protected function setUp(): void
'customerAccountManagement' => $this->customerAccountManagementMock,
'customerRepository' => $this->customerRepositoryMock,
'addressHelper' => $this->addressHelperMock,
- 'urlFactory' => $urlFactoryMock
+ 'urlFactory' => $urlFactoryMock,
+ 'customerLogger' => $this->customerLoggerMock,
+ 'cookieMetadataManager' => $objectManagerHelper->getObject(PhpCookieManager::class),
]
);
}
@@ -218,6 +235,8 @@ public function testIsLoggedIn(): void
}
/**
+ * @param $customerId
+ * @param $key
* @return void
* @dataProvider getParametersDataProvider
*/
@@ -271,7 +290,8 @@ public function getParametersDataProvider(): array
* @param $key
* @param $vatValidationEnabled
* @param $addressType
- * @param Phrase $successMessage
+ * @param $lastLoginAt
+ * @param $successMessage
*
* @return void
* @dataProvider getSuccessMessageDataProvider
@@ -282,7 +302,8 @@ public function testSuccessMessage(
$key,
$vatValidationEnabled,
$addressType,
- Phrase $successMessage
+ $lastLoginAt,
+ $successMessage
): void {
$this->customerSessionMock->expects($this->once())
->method('isLoggedIn')
@@ -292,7 +313,7 @@ public function testSuccessMessage(
->method('getParam')
->willReturnMap(
[
- ['id', false, $customerId],
+ ['id', 0, $customerId],
['key', false, $key]
]
);
@@ -333,6 +354,14 @@ public function testSuccessMessage(
['*/*/admin', ['_secure' => true], 'http://store.web/back']
]);
+ $this->logMock->expects($vatValidationEnabled ? $this->never() : $this->once())
+ ->method('getLastLoginAt')
+ ->willReturn($lastLoginAt);
+ $this->customerLoggerMock->expects($vatValidationEnabled ? $this->never() : $this->once())
+ ->method('get')
+ ->with(1)
+ ->willReturn($this->logMock);
+
$this->addressHelperMock->expects($this->once())
->method('isVatValidationEnabled')
->willReturn($vatValidationEnabled);
@@ -356,12 +385,14 @@ public function testSuccessMessage(
public function getSuccessMessageDataProvider(): array
{
return [
- [1, 1, false, null, __('Thank you for registering with %1.', 'frontend')],
+ [1, 1, false, null, 'some-datetime', null],
+ [1, 1, false, null, null, __('Thank you for registering with %1.', 'frontend')],
[
1,
1,
true,
Address::TYPE_BILLING,
+ null,
__(
'If you are a registered VAT customer, please click here '
. ' to enter your billing address for proper VAT calculation.',
@@ -373,12 +404,13 @@ public function getSuccessMessageDataProvider(): array
1,
true,
Address::TYPE_SHIPPING,
+ null,
__(
'If you are a registered VAT customer, please click here '
. ' to enter your shipping address for proper VAT calculation.',
'http://store.web/customer/address/edit'
)
- ]
+ ],
];
}
@@ -389,7 +421,8 @@ public function getSuccessMessageDataProvider(): array
* @param $successUrl
* @param $resultUrl
* @param $isSetFlag
- * @param Phrase $successMessage
+ * @param $successMessage
+ * @param $lastLoginAt
*
* @return void
* @dataProvider getSuccessRedirectDataProvider
@@ -401,7 +434,8 @@ public function testSuccessRedirect(
$successUrl,
$resultUrl,
$isSetFlag,
- Phrase $successMessage
+ $lastLoginAt,
+ $successMessage
): void {
$this->customerSessionMock->expects($this->once())
->method('isLoggedIn')
@@ -411,7 +445,7 @@ public function testSuccessRedirect(
->method('getParam')
->willReturnMap(
[
- ['id', false, $customerId],
+ ['id', 0, $customerId],
['key', false, $key],
['back_url', false, $backUrl]
]
@@ -437,23 +471,28 @@ public function testSuccessRedirect(
->with($this->customerDataMock)
->willReturnSelf();
- $this->messageManagerMock
- ->method('addSuccess')
+ $this->messageManagerMock->method('addSuccess')
->with($successMessage)
->willReturnSelf();
- $this->messageManagerMock
- ->expects($this->never())
+ $this->messageManagerMock->expects($this->never())
->method('addException');
- $this->urlMock
- ->method('getUrl')
+ $this->urlMock->method('getUrl')
->willReturnMap([
['customer/address/edit', null, 'http://store.web/customer/address/edit'],
['*/*/admin', ['_secure' => true], 'http://store.web/back'],
['*/*/index', ['_secure' => true], $successUrl]
]);
+ $this->logMock->expects($this->once())
+ ->method('getLastLoginAt')
+ ->willReturn($lastLoginAt);
+ $this->customerLoggerMock->expects($this->once())
+ ->method('get')
+ ->with(1)
+ ->willReturn($this->logMock);
+
$this->storeMock->expects($this->any())
->method('getFrontendName')
->willReturn('frontend');
@@ -468,10 +507,7 @@ public function testSuccessRedirect(
$this->scopeConfigMock->expects($this->any())
->method('isSetFlag')
- ->with(
- Url::XML_PATH_CUSTOMER_STARTUP_REDIRECT_TO_DASHBOARD,
- ScopeInterface::SCOPE_STORE
- )
+ ->with(Url::XML_PATH_CUSTOMER_STARTUP_REDIRECT_TO_DASHBOARD, ScopeInterface::SCOPE_STORE)
->willReturn($isSetFlag);
$this->model->execute();
@@ -490,6 +526,7 @@ public function getSuccessRedirectDataProvider(): array
null,
'http://example.com/back',
true,
+ null,
__('Thank you for registering with %1.', 'frontend'),
],
[
@@ -499,6 +536,7 @@ public function getSuccessRedirectDataProvider(): array
'http://example.com/success',
'http://example.com/success',
true,
+ null,
__('Thank you for registering with %1.', 'frontend'),
],
[
@@ -508,7 +546,18 @@ public function getSuccessRedirectDataProvider(): array
'http://example.com/success',
'http://example.com/success',
false,
+ null,
__('Thank you for registering with %1.', 'frontend'),
+ ],
+ [
+ 1,
+ 1,
+ null,
+ 'http://example.com/success',
+ 'http://example.com/success',
+ false,
+ 'some data',
+ null,
]
];
}
diff --git a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementApiTest.php b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementApiTest.php
new file mode 100644
index 0000000000000..074d40021a184
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementApiTest.php
@@ -0,0 +1,421 @@
+customerFactory = $this->createPartialMock(CustomerFactory::class, ['create']);
+ $this->manager = $this->getMockForAbstractClass(ManagerInterface::class);
+ $this->storeManager = $this->getMockForAbstractClass(StoreManagerInterface::class);
+ $this->random = $this->createMock(Random::class);
+ $this->validator = $this->createMock(Validator::class);
+ $this->validationResultsInterfaceFactory = $this->createMock(
+ ValidationResultsInterfaceFactory::class
+ );
+ $this->addressRepository = $this->getMockForAbstractClass(AddressRepositoryInterface::class);
+ $this->customerMetadata = $this->getMockForAbstractClass(CustomerMetadataInterface::class);
+ $this->customerRegistry = $this->createMock(CustomerRegistry::class);
+
+ $this->logger = $this->getMockForAbstractClass(LoggerInterface::class);
+ $this->encryptor = $this->getMockForAbstractClass(EncryptorInterface::class);
+ $this->share = $this->createMock(Share::class);
+ $this->string = $this->createMock(StringUtils::class);
+ $this->customerRepository = $this->getMockForAbstractClass(CustomerRepositoryInterface::class);
+ $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->transportBuilder = $this->createMock(TransportBuilder::class);
+ $this->dataObjectProcessor = $this->createMock(DataObjectProcessor::class);
+ $this->registry = $this->createMock(Registry::class);
+ $this->customerViewHelper = $this->createMock(View::class);
+ $this->dateTime = $this->createMock(\Magento\Framework\Stdlib\DateTime::class);
+ $this->customer = $this->createMock(\Magento\Customer\Model\Customer::class);
+ $this->objectFactory = $this->createMock(DataObjectFactory::class);
+ $this->addressRegistryMock = $this->createMock(AddressRegistry::class);
+ $this->extensibleDataObjectConverter = $this->createMock(
+ ExtensibleDataObjectConverter::class
+ );
+ $this->allowedCountriesReader = $this->createMock(AllowedCountries::class);
+ $this->customerSecure = $this->getMockBuilder(CustomerSecure::class)
+ ->onlyMethods(['addData', 'setData'])
+ ->addMethods(['setRpToken', 'setRpTokenCreatedAt'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->dateTimeFactory = $this->createMock(DateTimeFactory::class);
+ $this->accountConfirmation = $this->createMock(AccountConfirmation::class);
+ $this->searchCriteriaBuilderMock = $this->createMock(SearchCriteriaBuilder::class);
+
+ $this->visitorCollectionFactory = $this->getMockBuilder(CollectionFactory::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['create'])
+ ->getMock();
+ $this->sessionManager = $this->getMockBuilder(SessionManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->saveHandler = $this->getMockBuilder(SaveHandlerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->authorizationMock = $this->createMock(Authorization::class);
+ $this->objectManagerHelper = new ObjectManagerHelper($this);
+ $this->accountManagement = $this->objectManagerHelper->getObject(
+ AccountManagementApi::class,
+ [
+ 'customerFactory' => $this->customerFactory,
+ 'eventManager' => $this->manager,
+ 'storeManager' => $this->storeManager,
+ 'mathRandom' => $this->random,
+ 'validator' => $this->validator,
+ 'validationResultsDataFactory' => $this->validationResultsInterfaceFactory,
+ 'addressRepository' => $this->addressRepository,
+ 'customerMetadataService' => $this->customerMetadata,
+ 'customerRegistry' => $this->customerRegistry,
+ 'logger' => $this->logger,
+ 'encryptor' => $this->encryptor,
+ 'configShare' => $this->share,
+ 'stringHelper' => $this->string,
+ 'customerRepository' => $this->customerRepository,
+ 'scopeConfig' => $this->scopeConfig,
+ 'transportBuilder' => $this->transportBuilder,
+ 'dataProcessor' => $this->dataObjectProcessor,
+ 'registry' => $this->registry,
+ 'customerViewHelper' => $this->customerViewHelper,
+ 'dateTime' => $this->dateTime,
+ 'customerModel' => $this->customer,
+ 'objectFactory' => $this->objectFactory,
+ 'extensibleDataObjectConverter' => $this->extensibleDataObjectConverter,
+ 'dateTimeFactory' => $this->dateTimeFactory,
+ 'accountConfirmation' => $this->accountConfirmation,
+ 'sessionManager' => $this->sessionManager,
+ 'saveHandler' => $this->saveHandler,
+ 'visitorCollectionFactory' => $this->visitorCollectionFactory,
+ 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock,
+ 'addressRegistry' => $this->addressRegistryMock,
+ 'allowedCountriesReader' => $this->allowedCountriesReader,
+ 'authorization' => $this->authorizationMock
+ ]
+ );
+ $this->accountManagementMock = $this->createMock(AccountManagement::class);
+
+ $this->storeMock = $this->getMockBuilder(
+ StoreInterface::class
+ )->disableOriginalConstructor()
+ ->getMock();
+ }
+
+ /**
+ * Verify that only authorized request will be able to change groupId
+ *
+ * @param int $groupId
+ * @param int $customerId
+ * @param bool $isAllowed
+ * @param int $willThrowException
+ * @return void
+ * @throws AuthorizationException
+ * @throws LocalizedException
+ * @dataProvider customerDataProvider
+ */
+ public function testBeforeCreateAccount(
+ int $groupId,
+ int $customerId,
+ bool $isAllowed,
+ int $willThrowException
+ ): void {
+ if ($willThrowException) {
+ $this->expectException(AuthorizationException::class);
+ } else {
+ $this->expectNotToPerformAssertions();
+ }
+ $this->authorizationMock
+ ->expects($this->once())
+ ->method('isAllowed')
+ ->with('Magento_Customer::manage')
+ ->willReturn($isAllowed);
+
+ $customer = $this->getMockBuilder(CustomerInterface::class)
+ ->addMethods(['setData'])
+ ->getMockForAbstractClass();
+ $customer->method('getGroupId')->willReturn($groupId);
+ $customer->method('getId')->willReturn($customerId);
+ $customer->method('getWebsiteId')->willReturn(2);
+ $customer->method('getStoreId')->willReturn(1);
+ $customer->method('setData')->willReturn(1);
+
+ $this->customerRepository->method('get')->willReturn($customer);
+ $this->customerRepository->method('getById')->with($customerId)->willReturn($customer);
+ $this->customerRepository->method('save')->willReturn($customer);
+
+ if (!$willThrowException) {
+ $this->accountManagementMock->method('createAccountWithPasswordHash')->willReturn($customer);
+ $this->storeMock->expects($this->any())->method('getId')->willReturnOnConsecutiveCalls(2, 1);
+ $this->random->method('getUniqueHash')->willReturn('testabc');
+ $date = $this->getMockBuilder(\DateTime::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->dateTimeFactory->expects(static::once())
+ ->method('create')
+ ->willReturn($date);
+ $date->expects(static::once())
+ ->method('format')
+ ->with('Y-m-d H:i:s')
+ ->willReturn('2015-01-01 00:00:00');
+ $this->customerRegistry->method('retrieveSecureData')->willReturn($this->customerSecure);
+ $this->storeManager->method('getStores')
+ ->willReturn([$this->storeMock]);
+ }
+ $this->accountManagement->createAccount($customer);
+ }
+
+ /**
+ * @return array
+ */
+ public function customerDataProvider(): array
+ {
+ return [
+ [3, 1, false, 1],
+ [3, 1, true, 0]
+ ];
+ }
+}
diff --git a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php
index 9e68d53fd5949..e2b507f6fe37d 100644
--- a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php
+++ b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php
@@ -1222,7 +1222,6 @@ public function testCreateAccountWithGroupId(): void
$minPasswordLength = 5;
$minCharacterSetsNum = 2;
$defaultGroupId = 1;
- $requestedGroupId = 3;
$datetime = $this->prepareDateTimeFactory();
@@ -1299,9 +1298,6 @@ public function testCreateAccountWithGroupId(): void
return null;
}
}));
- $customer->expects($this->atLeastOnce())
- ->method('getGroupId')
- ->willReturn($requestedGroupId);
$customer
->method('setGroupId')
->willReturnOnConsecutiveCalls(null, $defaultGroupId);
diff --git a/app/code/Magento/Customer/Test/Unit/Model/Address/AbstractAddressTest.php b/app/code/Magento/Customer/Test/Unit/Model/Address/AbstractAddressTest.php
index cee6a8aefd1a4..29b95cdf79cfe 100644
--- a/app/code/Magento/Customer/Test/Unit/Model/Address/AbstractAddressTest.php
+++ b/app/code/Magento/Customer/Test/Unit/Model/Address/AbstractAddressTest.php
@@ -407,6 +407,7 @@ public function getStreetFullDataProvider()
["first line\nsecond line", ['first line', 'second line']],
['single line', ['single line']],
['single line', 'single line'],
+ ['single line', ['single line', null]],
];
}
diff --git a/app/code/Magento/Customer/Test/Unit/Model/App/FrontController/DeleteCookieWhenCustomerNotExistPluginTest.php b/app/code/Magento/Customer/Test/Unit/Model/App/FrontController/DeleteCookieWhenCustomerNotExistPluginTest.php
deleted file mode 100644
index fd06dbf6b8004..0000000000000
--- a/app/code/Magento/Customer/Test/Unit/Model/App/FrontController/DeleteCookieWhenCustomerNotExistPluginTest.php
+++ /dev/null
@@ -1,56 +0,0 @@
-customerSessionMock = $this->createMock(Session::class);
- $this->responseHttpMock = $this->createMock(ResponseHttp::class);
- $this->plugin = new DeleteCookieWhenCustomerNotExistPlugin(
- $this->responseHttpMock,
- $this->customerSessionMock
- );
- }
-
- public function testBeforeDispatch()
- {
- $this->customerSessionMock->expects($this->once())
- ->method('getCustomerId')
- ->willReturn(0);
- $this->plugin->beforeDispatch();
- }
-}
diff --git a/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerNotificationTest.php b/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerNotificationTest.php
index 35f9b0b8371c3..c7ae84b1fa0c2 100644
--- a/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerNotificationTest.php
+++ b/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerNotificationTest.php
@@ -20,7 +20,13 @@
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
+use Magento\Framework\Session\StorageInterface;
+/**
+ * Unit test for CustomerNotification plugin
+ *
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ */
class CustomerNotificationTest extends TestCase
{
private const STUB_CUSTOMER_ID = 1;
@@ -65,6 +71,11 @@ class CustomerNotificationTest extends TestCase
*/
private $plugin;
+ /**
+ * @var StorageInterface|MockObject
+ */
+ private $storage;
+
protected function setUp(): void
{
$this->sessionMock = $this->createMock(Session::class);
@@ -87,19 +98,27 @@ protected function setUp(): void
->with(NotificationStorage::UPDATE_CUSTOMER_SESSION, self::STUB_CUSTOMER_ID)
->willReturn(true);
+ $this->storage = $this
+ ->getMockBuilder(StorageInterface::class)
+ ->addMethods(['getData', 'setData'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+
$this->plugin = new CustomerNotification(
$this->sessionMock,
$this->notificationStorageMock,
$this->appStateMock,
$this->customerRepositoryMock,
$this->loggerMock,
- $this->requestMock
+ $this->requestMock,
+ $this->storage
);
}
public function testBeforeExecute()
{
$customerGroupId = 1;
+ $testSessionId = [uniqid()];
$customerMock = $this->getMockForAbstractClass(CustomerInterface::class);
$customerMock->method('getGroupId')->willReturn($customerGroupId);
@@ -116,6 +135,10 @@ public function testBeforeExecute()
$this->sessionMock->expects($this->once())->method('setCustomerData')->with($customerMock);
$this->sessionMock->expects($this->once())->method('setCustomerGroupId')->with($customerGroupId);
$this->sessionMock->expects($this->once())->method('regenerateId');
+ $this->storage->expects($this->once())->method('getData')->willReturn($testSessionId);
+ $this->storage
+ ->expects($this->once())
+ ->method('setData');
$this->plugin->beforeExecute($this->actionMock);
}
diff --git a/app/code/Magento/Customer/Test/Unit/Plugin/AsyncRequestCustomerGroupAuthorizationTest.php b/app/code/Magento/Customer/Test/Unit/Plugin/AsyncRequestCustomerGroupAuthorizationTest.php
new file mode 100644
index 0000000000000..107df2c2863ef
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Unit/Plugin/AsyncRequestCustomerGroupAuthorizationTest.php
@@ -0,0 +1,112 @@
+authorizationMock = $this->createMock(Authorization::class);
+ $this->plugin = $objectManager->getObject(AsyncRequestCustomerGroupAuthorization::class, [
+ 'authorization' => $this->authorizationMock
+ ]);
+ $this->massScheduleMock = $this->createMock(MassSchedule::class);
+ $this->customerRepository = $this->getMockForAbstractClass(CustomerRepositoryInterface::class);
+ }
+
+ /**
+ * Verify that only authorized request will be able to change groupId
+ *
+ * @param int $groupId
+ * @param int $customerId
+ * @param bool $isAllowed
+ * @param int $willThrowException
+ * @return void
+ * @throws AuthorizationException
+ * @dataProvider customerDataProvider
+ */
+ public function testBeforePublishMass(
+ int $groupId,
+ int $customerId,
+ bool $isAllowed,
+ int $willThrowException
+ ): void {
+ if ($willThrowException) {
+ $this->expectException(AuthorizationException::class);
+ } else {
+ $this->expectNotToPerformAssertions();
+ }
+ $customer = $this->getMockForAbstractClass(CustomerInterface::class);
+ $customer->method('getGroupId')->willReturn($groupId);
+ $customer->method('getId')->willReturn($customerId);
+ $this->customerRepository->method('getById')->with($customerId)->willReturn($customer);
+ $entitiesArray = [
+ [$customer, 'Password1', '']
+ ];
+ $this->authorizationMock
+ ->expects($this->once())
+ ->method('isAllowed')
+ ->with('Magento_Customer::manage')
+ ->willReturn($isAllowed);
+ $this->plugin->beforePublishMass(
+ $this->massScheduleMock,
+ 'async.magento.customer.api.accountmanagementinterface.createaccount.post',
+ $entitiesArray,
+ '',
+ ''
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public function customerDataProvider(): array
+ {
+ return [
+ [3, 1, false, 1],
+ [3, 1, true, 0]
+ ];
+ }
+}
diff --git a/app/code/Magento/Customer/Test/Unit/Plugin/Webapi/Controller/Rest/ValidateCustomerDataTest.php b/app/code/Magento/Customer/Test/Unit/Plugin/Webapi/Controller/Rest/ValidateCustomerDataTest.php
new file mode 100644
index 0000000000000..cda66041ab3c5
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Unit/Plugin/Webapi/Controller/Rest/ValidateCustomerDataTest.php
@@ -0,0 +1,115 @@
+validateCustomerDataObject = ObjectManager::getInstance()->get(ValidateCustomerData::class);
+ $this->reflectionObject = new ReflectionClass(get_class($this->validateCustomerDataObject));
+ }
+
+ /**
+ * Test if the customer Info is valid
+ *
+ * @param array $customerInfo
+ * @param array $result
+ * @dataProvider dataProviderInputData
+ * @throws Exception
+ */
+ public function testValidateInputData(array $customerInfo, array $result)
+ {
+ $this->assertEquals(
+ $result,
+ $this->invokeValidateInputData('validateInputData', [$customerInfo])
+ );
+ }
+
+ /**
+ * @param string $methodName
+ * @param array $arguments
+ * @return mixed
+ * @throws Exception
+ */
+ private function invokeValidateInputData(string $methodName, array $arguments = [])
+ {
+ $validateInputDataMethod = $this->reflectionObject->getMethod($methodName);
+ $validateInputDataMethod->setAccessible(true);
+ return $validateInputDataMethod->invokeArgs($this->validateCustomerDataObject, $arguments);
+ }
+
+ /**
+ * @return array
+ */
+ public function dataProviderInputData(): array
+ {
+ return [
+ [
+ ['customer' =>
+ [
+ 'id' => -1,
+ 'Id' => 1,
+ 'name' =>
+ [
+ 'firstName' => 'Test',
+ 'LastName' => 'user'
+ ],
+ 'isHavingOwnHouse' => 1,
+ 'address' =>
+ [
+ 'street' => '1st Street',
+ 'Street' => '3rd Street',
+ 'city' => 'London'
+ ],
+ ]
+ ],
+ ['customer' =>
+ [
+ 'id' => -1,
+ 'name' =>
+ [
+ 'firstName' => 'Test',
+ 'LastName' => 'user'
+ ],
+ 'isHavingOwnHouse' => 1,
+ 'address' =>
+ [
+ 'street' => '1st Street',
+ 'city' => 'London'
+ ],
+ ]
+ ],
+ ]
+ ];
+ }
+}
diff --git a/app/code/Magento/Customer/Test/Unit/ViewModel/Customer/AuthTest.php b/app/code/Magento/Customer/Test/Unit/ViewModel/Customer/AuthTest.php
new file mode 100644
index 0000000000000..b84e78ca35913
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Unit/ViewModel/Customer/AuthTest.php
@@ -0,0 +1,58 @@
+contextMock = $this->getMockBuilder(Context::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->model = new Auth(
+ $this->contextMock
+ );
+ parent::setUp();
+ }
+
+ /**
+ * Test is logged in value.
+ *
+ * @return void
+ */
+ public function testIsLoggedIn(): void
+ {
+ $this->contextMock->expects($this->once())
+ ->method('getValue')
+ ->willReturn(true);
+
+ $this->assertEquals(
+ true,
+ $this->model->isLoggedIn()
+ );
+ }
+}
diff --git a/app/code/Magento/Customer/Test/Unit/ViewModel/Customer/JsonSerializerTest.php b/app/code/Magento/Customer/Test/Unit/ViewModel/Customer/JsonSerializerTest.php
new file mode 100644
index 0000000000000..bf259040aaf91
--- /dev/null
+++ b/app/code/Magento/Customer/Test/Unit/ViewModel/Customer/JsonSerializerTest.php
@@ -0,0 +1,66 @@
+jsonEncoderMock = $this->getMockBuilder(Json::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->model = new JsonSerializer(
+ $this->jsonEncoderMock
+ );
+ parent::setUp();
+ }
+
+ /**
+ * Test serialize value.
+ *
+ * @return void
+ */
+ public function testSerialize(): void
+ {
+ $this->jsonEncoderMock->expects($this->once())
+ ->method('serialize')
+ ->willReturnCallback(
+ function ($value) {
+ return json_encode($value);
+ }
+ );
+
+ $this->assertEquals(
+ json_encode(
+ [
+ 'http://example.com/customer/section/load/'
+ ]
+ ),
+ $this->model->serialize(['http://example.com/customer/section/load/'])
+ );
+ }
+}
diff --git a/app/code/Magento/Customer/ViewModel/Customer/Auth.php b/app/code/Magento/Customer/ViewModel/Customer/Auth.php
new file mode 100644
index 0000000000000..e8c9210d32e12
--- /dev/null
+++ b/app/code/Magento/Customer/ViewModel/Customer/Auth.php
@@ -0,0 +1,36 @@
+httpContext->getValue(Context::CONTEXT_AUTH) ?? false;
+ }
+}
diff --git a/app/code/Magento/Customer/ViewModel/Customer/Data.php b/app/code/Magento/Customer/ViewModel/Customer/Data.php
deleted file mode 100644
index 8c285b368c961..0000000000000
--- a/app/code/Magento/Customer/ViewModel/Customer/Data.php
+++ /dev/null
@@ -1,63 +0,0 @@
-httpContext = $httpContext;
- $this->jsonEncoder = $jsonEncoder;
- }
-
- /**
- * Check is user login
- *
- * @return bool
- */
- public function isLoggedIn()
- {
- return $this->httpContext->getValue(Context::CONTEXT_AUTH);
- }
-
- /**
- * Encode the mixed $valueToEncode into the JSON format
- *
- * @param mixed $valueToEncode
- * @return string
- */
- public function jsonEncode($valueToEncode)
- {
- return $this->jsonEncoder->serialize($valueToEncode);
- }
-}
diff --git a/app/code/Magento/Customer/ViewModel/Customer/JsonSerializer.php b/app/code/Magento/Customer/ViewModel/Customer/JsonSerializer.php
new file mode 100644
index 0000000000000..c7a7be29a2943
--- /dev/null
+++ b/app/code/Magento/Customer/ViewModel/Customer/JsonSerializer.php
@@ -0,0 +1,36 @@
+jsonEncoder->serialize($value);
+ }
+}
diff --git a/app/code/Magento/Customer/composer.json b/app/code/Magento/Customer/composer.json
index ef2047644759b..39c82c20f2ec8 100644
--- a/app/code/Magento/Customer/composer.json
+++ b/app/code/Magento/Customer/composer.json
@@ -29,7 +29,8 @@
"suggest": {
"magento/module-cookie": "*",
"magento/module-customer-sample-data": "*",
- "magento/module-webapi": "*"
+ "magento/module-webapi": "*",
+ "magento/module-asynchronous-operations": "*"
},
"type": "magento2-module",
"license": [
diff --git a/app/code/Magento/Customer/etc/adminhtml/system.xml b/app/code/Magento/Customer/etc/adminhtml/system.xml
index 569f9d09c2087..ec76e09fdf459 100644
--- a/app/code/Magento/Customer/etc/adminhtml/system.xml
+++ b/app/code/Magento/Customer/etc/adminhtml/system.xml
@@ -193,6 +193,10 @@
Email template chosen based on theme fallback when "Default" option is selected.
Magento\Config\Model\Config\Source\Email\Template
+
+ Require email confirmation if email has been changed
+ Magento\Config\Model\Config\Source\Yesno
+
Name and Address Options
diff --git a/app/code/Magento/Customer/etc/config.xml b/app/code/Magento/Customer/etc/config.xml
index 22596e0b901b2..23a7c9ebb4034 100644
--- a/app/code/Magento/Customer/etc/config.xml
+++ b/app/code/Magento/Customer/etc/config.xml
@@ -32,6 +32,7 @@
customer_account_information_change_email_template
customer_account_information_change_email_and_password_template
+ 0
support
diff --git a/app/code/Magento/Customer/etc/di.xml b/app/code/Magento/Customer/etc/di.xml
index b178f51f89199..96fd4b86be702 100644
--- a/app/code/Magento/Customer/etc/di.xml
+++ b/app/code/Magento/Customer/etc/di.xml
@@ -585,4 +585,9 @@
+
+
+
diff --git a/app/code/Magento/Customer/etc/frontend/di.xml b/app/code/Magento/Customer/etc/frontend/di.xml
index 04ffc5d684b1a..827a153e94674 100644
--- a/app/code/Magento/Customer/etc/frontend/di.xml
+++ b/app/code/Magento/Customer/etc/frontend/di.xml
@@ -127,7 +127,7 @@
-
-
+
+
diff --git a/app/code/Magento/Customer/etc/webapi_rest/di.xml b/app/code/Magento/Customer/etc/webapi_rest/di.xml
index 18627b68320ed..c5d7a28a3651d 100644
--- a/app/code/Magento/Customer/etc/webapi_rest/di.xml
+++ b/app/code/Magento/Customer/etc/webapi_rest/di.xml
@@ -31,6 +31,9 @@
+
+
+
diff --git a/app/code/Magento/Customer/view/adminhtml/web/js/form/element/region.js b/app/code/Magento/Customer/view/adminhtml/web/js/form/element/region.js
index 755a8e6df3dbe..3d1132332d70a 100644
--- a/app/code/Magento/Customer/view/adminhtml/web/js/form/element/region.js
+++ b/app/code/Magento/Customer/view/adminhtml/web/js/form/element/region.js
@@ -21,9 +21,16 @@ define([
setDifferedFromDefault: function (value) {
this._super();
- if (parseFloat(value)) {
- this.source.set(this.regionScope, this.indexedOptions[value].label);
- }
+ const indexedOptionsArray = Object.values(this.indexedOptions),
+ countryId = this.source.data.country_id,
+ hasRegionList = indexedOptionsArray.some(option => option.country_id === countryId);
+
+ this.source.set(
+ this.regionScope,
+ hasRegionList
+ ? parseFloat(value) ? this.indexedOptions?.[value]?.label || '' : ''
+ : this.source.data?.region || ''
+ );
}
});
});
diff --git a/app/code/Magento/Customer/view/frontend/layout/default.xml b/app/code/Magento/Customer/view/frontend/layout/default.xml
index eba504a12a1e5..11285070e002e 100644
--- a/app/code/Magento/Customer/view/frontend/layout/default.xml
+++ b/app/code/Magento/Customer/view/frontend/layout/default.xml
@@ -50,7 +50,8 @@
- Magento\Customer\ViewModel\Customer\Data
+ Magento\Customer\ViewModel\Customer\Auth
+ Magento\Customer\ViewModel\Customer\JsonSerializer
getViewModel() ?? ObjectManager::getInstance()->get(Data::class);
+/** @var Auth $auth */
+$auth = $block->getAuth() ?? ObjectManager::getInstance()->get(Auth::class);
+/** @var JsonSerializer $jsonSerializer */
+$jsonSerializer = $block->getJsonSerializer() ??
+ ObjectManager::getInstance()->get(JsonSerializer::class);
$customerDataUrl = $block->getCustomerDataUrl('customer/account/updateSession');
$expirableSectionNames = $block->getExpirableSectionNames();
?>
@@ -20,10 +23,12 @@ $expirableSectionNames = $block->getExpirableSectionNames();
"Magento_Customer/js/customer-data": {
"sectionLoadUrl": "= $block->escapeJs($block->getCustomerDataUrl('customer/section/load')) ?>",
"expirableSectionLifetime": = (int)$block->getExpirableSectionLifetime() ?>,
- "expirableSectionNames": = /* @noEscape */ $viewModel->jsonEncode($expirableSectionNames) ?>,
+ "expirableSectionNames": = /* @noEscape */ $jsonSerializer->serialize(
+ $expirableSectionNames
+ ) ?>,
"cookieLifeTime": "= $block->escapeJs($block->getCookieLifeTime()) ?>",
"updateSessionUrl": "= $block->escapeJs($customerDataUrl) ?>",
- "isLoggedIn": "= /* @noEscape */ $viewModel->isLoggedIn() ?>"
+ "isLoggedIn": "= /* @noEscape */ $auth->isLoggedIn() ?>"
}
}
}
diff --git a/app/code/Magento/CustomerAnalytics/README.md b/app/code/Magento/CustomerAnalytics/README.md
index b9cc560cea7e0..153379cd97679 100644
--- a/app/code/Magento/CustomerAnalytics/README.md
+++ b/app/code/Magento/CustomerAnalytics/README.md
@@ -14,5 +14,6 @@ For information about a module installation in Magento 2, see [Enable or disable
## Additional data
More information can get at articles:
+
- [Advanced Reporting](https://developer.adobe.com/commerce/php/development/advanced-reporting/)
- [Data collection for advanced reporting](https://developer.adobe.com/commerce/php/development/advanced-reporting/data-collection/)
diff --git a/app/code/Magento/CustomerDownloadableGraphQl/README.md b/app/code/Magento/CustomerDownloadableGraphQl/README.md
index 2a3729b36007e..28d777e27cb09 100644
--- a/app/code/Magento/CustomerDownloadableGraphQl/README.md
+++ b/app/code/Magento/CustomerDownloadableGraphQl/README.md
@@ -19,7 +19,7 @@ Extension developers can interact with the Magento_CatalogGraphQl module. For mo
## Additional information
-You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql).
+You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/).
### GraphQl Query
diff --git a/app/code/Magento/CustomerGraphQl/Model/Context/AddUserInfoToContext.php b/app/code/Magento/CustomerGraphQl/Model/Context/AddUserInfoToContext.php
index b3ae57e0ff994..0140bcd3739cb 100644
--- a/app/code/Magento/CustomerGraphQl/Model/Context/AddUserInfoToContext.php
+++ b/app/code/Magento/CustomerGraphQl/Model/Context/AddUserInfoToContext.php
@@ -34,11 +34,6 @@ class AddUserInfoToContext implements UserContextParametersProcessorInterface
*/
private $customerRepository;
- /**
- * @var CustomerInterface|null
- */
- private $loggedInCustomerData = null;
-
/**
* @param UserContextInterface $userContext
* @param Session $session
@@ -82,10 +77,6 @@ public function execute(ContextParametersInterface $contextParameters): ContextP
$isCustomer = $this->isCustomer($currentUserId, $currentUserType);
$contextParameters->addExtensionAttribute('is_customer', $isCustomer);
- if ($this->session->isLoggedIn()) {
- $this->loggedInCustomerData = $this->session->getCustomerData();
- }
-
if ($isCustomer) {
$customer = $this->customerRepository->getById($currentUserId);
$this->session->setCustomerData($customer);
@@ -101,7 +92,7 @@ public function execute(ContextParametersInterface $contextParameters): ContextP
*/
public function getLoggedInCustomerData(): ?CustomerInterface
{
- return $this->loggedInCustomerData;
+ return $this->session->isLoggedIn() ? $this->session->getCustomerData() : null;
}
/**
diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php
index 5a302f4c3df27..6bb4a81182521 100644
--- a/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php
+++ b/app/code/Magento/CustomerGraphQl/Model/Customer/Address/ExtractCustomerAddressData.php
@@ -7,14 +7,16 @@
namespace Magento\CustomerGraphQl\Model\Customer\Address;
+use Magento\Customer\Api\AddressMetadataInterface;
+use Magento\Customer\Api\AddressRepositoryInterface;
use Magento\Customer\Api\Data\AddressInterface;
use Magento\Customer\Api\Data\CustomerInterface;
-use Magento\Framework\Api\CustomAttributesDataInterface;
-use Magento\Customer\Api\AddressRepositoryInterface;
-use Magento\Customer\Model\ResourceModel\Customer as CustomerResourceModel;
use Magento\Customer\Model\CustomerFactory;
-use Magento\Framework\Webapi\ServiceOutputProcessor;
+use Magento\Customer\Model\ResourceModel\Customer as CustomerResourceModel;
+use Magento\EavGraphQl\Model\Output\Value\GetAttributeValueInterface;
+use Magento\Framework\Api\CustomAttributesDataInterface;
use Magento\Framework\Serialize\SerializerInterface;
+use Magento\Framework\Webapi\ServiceOutputProcessor;
/**
* Transform single customer address data from object to in array format
@@ -41,22 +43,30 @@ class ExtractCustomerAddressData
*/
private $customerFactory;
+ /**
+ * @var GetAttributeValueInterface
+ */
+ private GetAttributeValueInterface $getAttributeValue;
+
/**
* @param ServiceOutputProcessor $serviceOutputProcessor
* @param SerializerInterface $jsonSerializer
* @param CustomerResourceModel $customerResourceModel
* @param CustomerFactory $customerFactory
+ * @param GetAttributeValueInterface $getAttributeValue
*/
public function __construct(
ServiceOutputProcessor $serviceOutputProcessor,
SerializerInterface $jsonSerializer,
CustomerResourceModel $customerResourceModel,
- CustomerFactory $customerFactory
+ CustomerFactory $customerFactory,
+ GetAttributeValueInterface $getAttributeValue
) {
$this->serviceOutputProcessor = $serviceOutputProcessor;
$this->jsonSerializer = $jsonSerializer;
$this->customerResourceModel = $customerResourceModel;
$this->customerFactory = $customerFactory;
+ $this->getAttributeValue = $getAttributeValue;
}
/**
@@ -100,31 +110,11 @@ public function execute(AddressInterface $address): array
$addressData[CustomAttributesDataInterface::EXTENSION_ATTRIBUTES_KEY]
);
}
- $customAttributes = [];
- if (isset($addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES])) {
- foreach ($addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES] as $attribute) {
- $isArray = false;
- if (is_array($attribute['value'])) {
- // @ignoreCoverageStart
- $isArray = true;
- foreach ($attribute['value'] as $attributeValue) {
- if (is_array($attributeValue)) {
- $customAttributes[$attribute['attribute_code']] = $this->jsonSerializer->serialize(
- $attribute['value']
- );
- continue;
- }
- $customAttributes[$attribute['attribute_code']] = implode(',', $attribute['value']);
- continue;
- }
- // @ignoreCoverageEnd
- }
- if ($isArray) {
- continue;
- }
- $customAttributes[$attribute['attribute_code']] = $attribute['value'];
- }
- }
+
+ $customAttributes = isset($addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES])
+ ? $this->formatCustomAttributes($addressData[CustomAttributesDataInterface::CUSTOM_ATTRIBUTES])
+ : ['custom_attributesV2' => []];
+
$addressData = array_merge($addressData, $customAttributes);
$addressData['customer_id'] = null;
@@ -135,4 +125,54 @@ public function execute(AddressInterface $address): array
return $addressData;
}
+
+ /**
+ * Retrieve formatted custom attributes
+ *
+ * @param array $attributes
+ * @return array
+ */
+ private function formatCustomAttributes(array $attributes)
+ {
+ foreach ($attributes as $attribute) {
+ $isArray = false;
+ if (is_array($attribute['value'])) {
+ // @ignoreCoverageStart
+ $isArray = true;
+ foreach ($attribute['value'] as $attributeValue) {
+ if (is_array($attributeValue)) {
+ $customAttributes[$attribute['attribute_code']] = $this->jsonSerializer->serialize(
+ $attribute['value']
+ );
+ continue;
+ }
+ $customAttributes[$attribute['attribute_code']] = implode(',', $attribute['value']);
+ continue;
+ }
+ // @ignoreCoverageEnd
+ }
+ if ($isArray) {
+ continue;
+ }
+ $customAttributes[$attribute['attribute_code']] = $attribute['value'];
+ }
+
+ $customAttributes['custom_attributesV2'] = array_map(
+ function (array $customAttribute) {
+ return $this->getAttributeValue->execute(
+ AddressMetadataInterface::ENTITY_TYPE_ADDRESS,
+ $customAttribute['attribute_code'],
+ $customAttribute['value']
+ );
+ },
+ $attributes
+ );
+ usort($customAttributes['custom_attributesV2'], function (array $a, array $b) {
+ $aPosition = $a['sort_order'];
+ $bPosition = $b['sort_order'];
+ return $aPosition <=> $bPosition;
+ });
+
+ return $customAttributes;
+ }
}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/CreateCustomerAccount.php b/app/code/Magento/CustomerGraphQl/Model/Customer/CreateCustomerAccount.php
index a631b7ba86194..971b0352b8931 100644
--- a/app/code/Magento/CustomerGraphQl/Model/Customer/CreateCustomerAccount.php
+++ b/app/code/Magento/CustomerGraphQl/Model/Customer/CreateCustomerAccount.php
@@ -122,6 +122,7 @@ private function createAccount(array $data, StoreInterface $store): CustomerInte
$customerDataObject,
CustomerInterface::class
);
+
$data = array_merge($requiredDataAttributes, $data);
$this->validateCustomerData->execute($data);
$this->dataObjectHelper->populateWithArray(
diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php
index c62a931809644..01bb007ef618a 100644
--- a/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php
+++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ExtractCustomerData.php
@@ -7,11 +7,12 @@
namespace Magento\CustomerGraphQl\Model\Customer;
+use Magento\Customer\Api\CustomerMetadataInterface;
use Magento\Customer\Api\CustomerRepositoryInterface;
+use Magento\Customer\Api\Data\CustomerInterface;
+use Magento\EavGraphQl\Model\GetAttributeValueComposite;
use Magento\Framework\Exception\LocalizedException;
-use Magento\Framework\Serialize\SerializerInterface;
use Magento\Framework\Webapi\ServiceOutputProcessor;
-use Magento\Customer\Api\Data\CustomerInterface;
/**
* Transform single customer data from object to in array format
@@ -24,20 +25,20 @@ class ExtractCustomerData
private $serviceOutputProcessor;
/**
- * @var SerializerInterface
+ * @var GetAttributeValueComposite
*/
- private $serializer;
+ private GetAttributeValueComposite $getAttributeValueComposite;
/**
* @param ServiceOutputProcessor $serviceOutputProcessor
- * @param SerializerInterface $serializer
+ * @param GetAttributeValueComposite $getAttributeValueComposite
*/
public function __construct(
ServiceOutputProcessor $serviceOutputProcessor,
- SerializerInterface $serializer
+ GetAttributeValueComposite $getAttributeValueComposite
) {
$this->serviceOutputProcessor = $serviceOutputProcessor;
- $this->serializer = $serializer;
+ $this->getAttributeValueComposite = $getAttributeValueComposite;
}
/**
@@ -77,30 +78,24 @@ public function execute(CustomerInterface $customer): array
if (isset($customerData['extension_attributes'])) {
$customerData = array_merge($customerData, $customerData['extension_attributes']);
}
- $customAttributes = [];
if (isset($customerData['custom_attributes'])) {
- foreach ($customerData['custom_attributes'] as $attribute) {
- $isArray = false;
- if (is_array($attribute['value'])) {
- $isArray = true;
- foreach ($attribute['value'] as $attributeValue) {
- if (is_array($attributeValue)) {
- $customAttributes[$attribute['attribute_code']] = $this->serializer->serialize(
- $attribute['value']
- );
- continue;
- }
- $customAttributes[$attribute['attribute_code']] = implode(',', $attribute['value']);
- continue;
- }
- }
- if ($isArray) {
- continue;
- }
- $customAttributes[$attribute['attribute_code']] = $attribute['value'];
- }
+ $customerData['custom_attributes'] = array_map(
+ function (array $customAttribute) {
+ return $this->getAttributeValueComposite->execute(
+ CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER,
+ $customAttribute
+ );
+ },
+ $customerData['custom_attributes']
+ );
+ usort($customerData['custom_attributes'], function (array $a, array $b) {
+ $aPosition = $a['sort_order'];
+ $bPosition = $b['sort_order'];
+ return $aPosition <=> $bPosition;
+ });
+ } else {
+ $customerData['custom_attributes'] = [];
}
- $customerData = array_merge($customerData, $customAttributes);
//Fields are deprecated and should not be exposed on storefront.
$customerData['group_id'] = null;
$customerData['id'] = null;
diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/GetAttributesForm.php b/app/code/Magento/CustomerGraphQl/Model/Customer/GetAttributesForm.php
new file mode 100644
index 0000000000000..8c476abba90bc
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Customer/GetAttributesForm.php
@@ -0,0 +1,49 @@
+entity = $metadata;
+ $this->type = $type;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function execute(string $formCode): ?array
+ {
+ $attributes = [];
+ foreach ($this->entity->getAttributes($formCode) as $attribute) {
+ $attributes[] = ['entity_type' => $this->type, 'attribute_code' => $attribute->getAttributeCode()];
+ }
+ return $attributes;
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomAttributes.php b/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomAttributes.php
new file mode 100644
index 0000000000000..dc46f08fd0434
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomAttributes.php
@@ -0,0 +1,75 @@
+attributeRepository = $attributeRepository;
+ $this->frontendInputs = $frontendInputs;
+ $this->attributeSelectedOptionComposite = $attributeSelectedOptionComposite;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function execute(string $entityType, array $customAttribute): ?array
+ {
+ $attr = $this->attributeRepository->get(
+ $entityType,
+ $customAttribute['attribute_code']
+ );
+
+ $result = [
+ 'entity_type' => $entityType,
+ 'code' => $customAttribute['attribute_code'],
+ 'sort_order' => $attr->getSortOrder() ?? ''
+ ];
+
+ if (in_array($attr->getFrontendInput(), $this->frontendInputs)) {
+ $result['selected_options'] = $this->attributeSelectedOptionComposite->execute(
+ $entityType,
+ $customAttribute
+ );
+ } else {
+ $result['value'] = $customAttribute['value'];
+ }
+ return $result;
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomSelectedOptionAttributes.php b/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomSelectedOptionAttributes.php
new file mode 100644
index 0000000000000..8724e57ff11c5
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Customer/GetCustomSelectedOptionAttributes.php
@@ -0,0 +1,65 @@
+uid = $uid;
+ $this->attributeRepository = $attributeRepository;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function execute(string $entityType, array $customAttribute): ?array
+ {
+ $attr = $this->attributeRepository->get(
+ $entityType,
+ $customAttribute['attribute_code']
+ );
+
+ $result = [];
+ $selectedValues = explode(',', $customAttribute['value']);
+ foreach ($attr->getOptions() as $option) {
+ if (!in_array($option->getValue(), $selectedValues)) {
+ continue;
+ }
+ $result[] = [
+ 'uid' => $this->uid->encode($option->getValue()),
+ 'value' => $option->getValue(),
+ 'label' => $option->getLabel()
+ ];
+ }
+ return $result;
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerAccount.php b/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerAccount.php
index d82b8c6f941fa..8ff74178f2be8 100644
--- a/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerAccount.php
+++ b/app/code/Magento/CustomerGraphQl/Model/Customer/UpdateCustomerAccount.php
@@ -8,6 +8,7 @@
namespace Magento\CustomerGraphQl\Model\Customer;
use Magento\Customer\Api\Data\CustomerInterface;
+use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\GraphQl\Exception\GraphQlAlreadyExistsException;
use Magento\Framework\GraphQl\Exception\GraphQlAuthenticationException;
@@ -89,17 +90,20 @@ public function __construct(
* @throws GraphQlAuthenticationException
* @throws GraphQlInputException
* @throws GraphQlNoSuchEntityException
+ * @throws NoSuchEntityException
+ * @throws LocalizedException
*/
public function execute(CustomerInterface $customer, array $data, StoreInterface $store): void
{
if (isset($data['email']) && $customer->getEmail() !== $data['email']) {
- if (!isset($data['password']) || empty($data['password'])) {
+ if (empty($data['password'])) {
throw new GraphQlInputException(__('Provide the current "password" to change "email".'));
}
$this->checkCustomerPassword->execute($data['password'], (int)$customer->getId());
$customer->setEmail($data['email']);
}
+
$this->validateCustomerData->execute($data);
$filteredData = array_diff_key($data, array_flip($this->restrictedKeys));
$this->dataObjectHelper->populateWithArray($customer, $filteredData, CustomerInterface::class);
diff --git a/app/code/Magento/CustomerGraphQl/Model/Output/CustomerAttributeMetadata.php b/app/code/Magento/CustomerGraphQl/Model/Output/CustomerAttributeMetadata.php
new file mode 100644
index 0000000000000..bbc8b2eaab041
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Output/CustomerAttributeMetadata.php
@@ -0,0 +1,103 @@
+enumLookup = $enumLookup;
+ $this->metadata = $metadata;
+ $this->entityType = $entityType;
+ }
+
+ /**
+ * Retrieve formatted attribute data
+ *
+ * @param AttributeInterface $attribute
+ * @param string $entityType
+ * @param int $storeId
+ * @return array
+ * @throws LocalizedException
+ * @throws NoSuchEntityException
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function execute(
+ AttributeInterface $attribute,
+ string $entityType,
+ int $storeId
+ ): array {
+ if ($entityType !== $this->entityType) {
+ return [];
+ }
+
+ $attributeMetadata = $this->metadata->getAttributeMetadata($attribute->getAttributeCode());
+ $data = [];
+
+ $validationRules = array_map(function (ValidationRule $validationRule) {
+ return [
+ 'name' => $this->enumLookup->getEnumValueFromField(
+ 'ValidationRuleEnum',
+ strtoupper($validationRule->getName())
+ ),
+ 'value' => $validationRule->getValue()
+ ];
+ }, $attributeMetadata->getValidationRules());
+
+ if ($attributeMetadata->isVisible()) {
+ $data = [
+ 'input_filter' => empty($attributeMetadata->getInputFilter())
+ ? 'NONE'
+ : $this->enumLookup->getEnumValueFromField(
+ 'InputFilterEnum',
+ strtoupper($attributeMetadata->getInputFilter())
+ ),
+ 'multiline_count' => $attributeMetadata->getMultilineCount(),
+ 'sort_order' => $attributeMetadata->getSortOrder(),
+ 'validate_rules' => $validationRules,
+ 'attributeMetadata' => $attributeMetadata
+ ];
+ }
+
+ return $data;
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/Address/TagsStrategy.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/Address/TagsStrategy.php
new file mode 100644
index 0000000000000..03332301706ce
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/Address/TagsStrategy.php
@@ -0,0 +1,25 @@
+getCustomerId())];
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ModelDehydrator.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ModelDehydrator.php
new file mode 100644
index 0000000000000..db67d2e860c44
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ModelDehydrator.php
@@ -0,0 +1,57 @@
+typeResolver = $typeResolver;
+ $this->hydratorPool = $hydratorPool;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function dehydrate(array &$resolvedValue): void
+ {
+ if (isset($resolvedValue['model']) && $resolvedValue['model'] instanceof Customer) {
+ /** @var Customer $model */
+ $model = $resolvedValue['model'];
+ $entityType = $this->typeResolver->resolve($model);
+ $resolvedValue['model_data'] = $this->hydratorPool->getHydrator($entityType)
+ ->extract($model);
+ $resolvedValue['model_entity_type'] = $entityType;
+ $resolvedValue['model_id'] = $model->getId();
+ }
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ModelHydrator.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ModelHydrator.php
new file mode 100644
index 0000000000000..4b4c187bbd949
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ModelHydrator.php
@@ -0,0 +1,62 @@
+hydratorPool = $hydratorPool;
+ $this->customerFactory = $customerFactory;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function hydrate(array &$resolverData): void
+ {
+ if (isset($this->customerModels[$resolverData['model_id']])) {
+ $resolverData['model'] = $this->customerModels[$resolverData['model_id']];
+ } else {
+ $hydrator = $this->hydratorPool->getHydrator($resolverData['model_entity_type']);
+ $model = $this->customerFactory->create();
+ $hydrator->hydrate($model, $resolverData['model_data']);
+ $this->customerModels[$resolverData['model_id']] = $model;
+ $resolverData['model'] = $this->customerModels[$resolverData['model_id']];
+ }
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ResolverCacheIdentity.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ResolverCacheIdentity.php
new file mode 100644
index 0000000000000..85f659cc0adce
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/ResolverCacheIdentity.php
@@ -0,0 +1,31 @@
+getId()) ?
+ [] : [sprintf('%s_%s', $this->cacheTag, $resolvedData['model']->getId())];
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/TagsStrategy.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/TagsStrategy.php
new file mode 100644
index 0000000000000..f1d6406295c53
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Customer/TagsStrategy.php
@@ -0,0 +1,25 @@
+getId())];
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Subscriber/ResolverCacheIdentity.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Subscriber/ResolverCacheIdentity.php
new file mode 100644
index 0000000000000..f5e10440ddddf
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Subscriber/ResolverCacheIdentity.php
@@ -0,0 +1,30 @@
+getId()) ?
+ [] : [sprintf('%s_%s', $this->cacheTag, $parentResolvedData['model']->getId())];
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Subscriber/TagsStrategy.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Subscriber/TagsStrategy.php
new file mode 100644
index 0000000000000..7b953b2534513
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/Cache/Subscriber/TagsStrategy.php
@@ -0,0 +1,25 @@
+getCustomerId())];
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CurrentCustomerId.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CurrentCustomerId.php
new file mode 100644
index 0000000000000..75493acba88a1
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CurrentCustomerId.php
@@ -0,0 +1,38 @@
+getUserId();
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CustomerGroup.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CustomerGroup.php
new file mode 100644
index 0000000000000..33333eb8cf686
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CustomerGroup.php
@@ -0,0 +1,37 @@
+getExtensionAttributes()->getCustomerGroupId()
+ ?? GroupInterface::NOT_LOGGED_IN_ID);
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CustomerTaxRate.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CustomerTaxRate.php
new file mode 100644
index 0000000000000..5463c90894ef8
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/CustomerTaxRate.php
@@ -0,0 +1,83 @@
+groupRepository = $groupRepository;
+ $this->calculationModel = $calculationModel;
+ $this->calculationResource = $calculationResource;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getFactorName(): string
+ {
+ return static::NAME;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getFactorValue(ContextInterface $context): string
+ {
+ $customerId = $context->getExtensionAttributes()->getIsCustomer()
+ ? (int)$context->getUserId()
+ : 0;
+ $customerTaxClassId = $this->groupRepository->getById(
+ $context->getExtensionAttributes()->getCustomerGroupId() ?? GroupInterface::NOT_LOGGED_IN_ID
+ )->getTaxClassId();
+ $rateRequest = $this->calculationModel->getRateRequest(
+ null,
+ null,
+ $customerTaxClassId,
+ $context->getExtensionAttributes()->getStore(),
+ $customerId
+ );
+ $rateInfo = $this->calculationResource->getRateInfo($rateRequest);
+ return (string)$rateInfo['value'];
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/IsLoggedIn.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/IsLoggedIn.php
new file mode 100644
index 0000000000000..a8207232b177b
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/IsLoggedIn.php
@@ -0,0 +1,35 @@
+getExtensionAttributes()->getIsCustomer() ? "true" : "false";
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/ParentCustomerEntityId.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/ParentCustomerEntityId.php
new file mode 100644
index 0000000000000..2030c24fb1840
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CacheKey/FactorProvider/ParentCustomerEntityId.php
@@ -0,0 +1,53 @@
+getId();
+ }
+ throw new \InvalidArgumentException(__CLASS__ . " factor provider requires parent value " .
+ "to contain customer model id or customer model.");
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isRequiredOrigData(): bool
+ {
+ return false;
+ }
+}
diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomAttributeFilter.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomAttributeFilter.php
new file mode 100755
index 0000000000000..7850134e45f38
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/CustomAttributeFilter.php
@@ -0,0 +1,40 @@
+getExtensionAttributes()->getIsCustomer()) {
throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.'));
}
-
$isSecure = $this->registry->registry('isSecureArea');
$this->registry->unregister('isSecureArea');
diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/IsSubscribed.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/IsSubscribed.php
index b16ce7ee710ad..e39ae2ba17db4 100644
--- a/app/code/Magento/CustomerGraphQl/Model/Resolver/IsSubscribed.php
+++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/IsSubscribed.php
@@ -56,9 +56,7 @@ public function resolve(
if (!isset($value['model'])) {
throw new LocalizedException(__('"model" value should be specified'));
}
- /** @var CustomerInterface $customer */
- $customer = $value['model'];
- $customerId = (int)$customer->getId();
+ $customerId = (int)$value['model']->getId();
$extensionAttributes = $context->getExtensionAttributes();
if (!$extensionAttributes) {
diff --git a/app/code/Magento/CustomerGraphQl/README.md b/app/code/Magento/CustomerGraphQl/README.md
index ae374c045bae0..8f5df3db3b647 100644
--- a/app/code/Magento/CustomerGraphQl/README.md
+++ b/app/code/Magento/CustomerGraphQl/README.md
@@ -23,7 +23,7 @@ Extension developers can interact with the Magento_CustomerGraphQl module. For m
## Additional information
-You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql).
+You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/).
### GraphQl Query
diff --git a/app/code/Magento/CustomerGraphQl/Test/Unit/Model/Context/AddUserInfoToContextTest.php b/app/code/Magento/CustomerGraphQl/Test/Unit/Model/Context/AddUserInfoToContextTest.php
index 4699af2b9c389..4854efb5a1cd1 100644
--- a/app/code/Magento/CustomerGraphQl/Test/Unit/Model/Context/AddUserInfoToContextTest.php
+++ b/app/code/Magento/CustomerGraphQl/Test/Unit/Model/Context/AddUserInfoToContextTest.php
@@ -84,14 +84,6 @@ public function testExecuteForCustomer(): void
$this->contextParametersMock
->expects($this->once())
->method('setUserType');
- $this->sessionMock
- ->expects($this->once())
- ->method('isLoggedIn')
- ->willReturn(true);
- $this->sessionMock
- ->expects($this->once())
- ->method('getCustomerData')
- ->willReturn($this->customerMock);
$this->customerRepositoryMock
->expects($this->once())
->method('getById')
diff --git a/app/code/Magento/CustomerGraphQl/composer.json b/app/code/Magento/CustomerGraphQl/composer.json
index 5967d2e9f8ac7..9fb9668de0e77 100644
--- a/app/code/Magento/CustomerGraphQl/composer.json
+++ b/app/code/Magento/CustomerGraphQl/composer.json
@@ -7,6 +7,7 @@
"magento/module-authorization": "*",
"magento/module-customer": "*",
"magento/module-eav": "*",
+ "magento/module-eav-graph-ql": "*",
"magento/module-graph-ql": "*",
"magento/module-newsletter": "*",
"magento/module-integration": "*",
@@ -14,7 +15,8 @@
"magento/framework": "*",
"magento/module-directory": "*",
"magento/module-tax": "*",
- "magento/module-graph-ql-cache": "*"
+ "magento/module-graph-ql-cache": "*",
+ "magento/module-graph-ql-resolver-cache": "*"
},
"license": [
"OSL-3.0",
diff --git a/app/code/Magento/CustomerGraphQl/etc/di.xml b/app/code/Magento/CustomerGraphQl/etc/di.xml
new file mode 100644
index 0000000000000..6fbc996079086
--- /dev/null
+++ b/app/code/Magento/CustomerGraphQl/etc/di.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+ -
+ Magento\Customer\Model\Customer
+
+ -
+ Magento\Customer\Model\Address
+
+ -
+ Magento\Newsletter\Model\Subscriber
+
+
+
+
+
+
+
+ -
+ Magento\CustomerGraphQl\Model\Resolver\Cache\Customer\TagsStrategy
+
+ -
+ Magento\CustomerGraphQl\Model\Resolver\Cache\Customer\Address\TagsStrategy
+
+ -
+ Magento\CustomerGraphQl\Model\Resolver\Cache\Subscriber\TagsStrategy
+
+
+
+
+
diff --git a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml
index 1e616e37a12f5..be4f38d80e546 100644
--- a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml
+++ b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml
@@ -40,6 +40,34 @@
+
+
+
+ - CustomerAttributeMetadata
+ - CustomerAttributeMetadata
+
+
+
+
+
+
+ - GetCustomerAttributesMetadata
+ - GetCustomerAddressAttributesMetadata
+
+
+
+
+
+ Magento\Customer\Model\Metadata\CustomerMetadata
+ customer
+
+
+
+
+ Magento\Customer\Model\Metadata\AddressMetadata
+ customer_address
+
+
@@ -62,4 +90,137 @@
+
+
+
+ - Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\CustomerGroup
+ - Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\CustomerTaxRate
+ - Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\IsLoggedIn
+
+
+
+
+
+
+ -
+
- customer
+ - customer_address
+
+ -
+
- NONE
+ - DATE
+ - TRIM
+ - STRIPTAGS
+ - ESCAPEHTML
+
+ -
+
- DATE_RANGE_MAX
+ - DATE_RANGE_MIN
+ - FILE_EXTENSIONS
+ - INPUT_VALIDATION
+ - MAX_TEXT_LENGTH
+ - MIN_TEXT_LENGTH
+ - MAX_FILE_SIZE
+ - MAX_IMAGE_HEGHT
+ - MAX_IMAGE_WIDTH
+
+
+
+
+
+
+
+ - GetCustomerAttributesForm
+ - GetCustomerAddressAttributesForm
+
+
+
+
+
+ Magento\Customer\Api\CustomerMetadataInterface
+ customer
+
+
+
+
+ Magento\Customer\Api\AddressMetadataInterface
+ customer_address
+
+
+
+
+
+ - Magento\CustomerGraphQl\Model\Customer\GetCustomAttributes
+ - Magento\CustomerGraphQl\Model\Customer\GetCustomAttributes
+
+
+
+
+
+
+ - multiselect
+ - select
+
+
+
+
+
+
+ - Magento\CustomerGraphQl\Model\Customer\GetCustomSelectedOptionAttributes
+ - Magento\CustomerGraphQl\Model\Customer\GetCustomSelectedOptionAttributes
+
+
+
+
+
+
+ - multiselect
+ - select
+
+
+
+
+
+
+ -
+ Magento\CustomerGraphQl\Model\Resolver\Cache\Customer\ResolverCacheIdentity
+
+ -
+ Magento\CustomerGraphQl\Model\Resolver\Cache\Subscriber\ResolverCacheIdentity
+
+
+
+
+
+
+
+ -
+
-
+
- 10
+ - Magento\CustomerGraphQl\Model\Resolver\Cache\Customer\ModelHydrator
+
+
+
+
+ -
+
-
+
- 10
+ - Magento\CustomerGraphQl\Model\Resolver\Cache\Customer\ModelDehydrator
+
+
+
+
+
+
+
+
+ -
+
- Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\CurrentCustomerId
+
+ -
+
- Magento\CustomerGraphQl\Model\Resolver\CacheKey\FactorProvider\ParentCustomerEntityId
+
+
+
+
diff --git a/app/code/Magento/CustomerGraphQl/etc/module.xml b/app/code/Magento/CustomerGraphQl/etc/module.xml
index b15df7fc0be6b..bdbbaa3e7f432 100644
--- a/app/code/Magento/CustomerGraphQl/etc/module.xml
+++ b/app/code/Magento/CustomerGraphQl/etc/module.xml
@@ -9,6 +9,8 @@
+
+
diff --git a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls
index 439ce4742ca3b..e7e9a1484bb26 100644
--- a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls
@@ -49,7 +49,8 @@ input CustomerAddressInput @doc(description: "Contains details about a billing o
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: "Deprecated: Custom attributes should not be put into container.")
+ custom_attributes: [CustomerAddressAttributeInput] @doc(description: "Deprecated. Use custom_attributesV2 instead.") @deprecated(reason: "Use custom_attributesV2 instead.")
+ custom_attributesV2: [AttributeValueInput] @doc(description: "Custom attributes assigned to the customer address.")
}
input CustomerAddressRegionInput @doc(description: "Defines the customer's state or province.") {
@@ -95,6 +96,7 @@ input CustomerCreateInput @doc(description: "An input object for creating a cus
gender: Int @doc(description: "The customer's gender (Male - 1, Female - 2).")
password: String @doc(description: "The customer's password.")
is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter.")
+ custom_attributes: [AttributeValueInput!] @doc(description: "The customer's custom attributes.")
}
input CustomerUpdateInput @doc(description: "An input object for updating a customer.") {
@@ -108,6 +110,7 @@ input CustomerUpdateInput @doc(description: "An input object for updating a cust
prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.")
suffix: String @doc(description: "A value such as Sr., Jr., or III.")
taxvat: String @doc(description: "The customer's Tax/VAT number (for corporate customers).")
+ custom_attributes: [AttributeValueInput!] @doc(description: "The customer's custom attributes.")
}
type CustomerOutput @doc(description: "Contains details about a newly-created or updated customer.") {
@@ -136,6 +139,7 @@ type Customer @doc(description: "Defines the customer name, addresses, and other
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).")
+ custom_attributes(attributeCodes: [ID!]): [AttributeValueInterface] @doc(description: "Customer's custom attributes.") @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\CustomAttributeFilter")
}
type CustomerAddress @doc(description: "Contains detailed information about a customer's billing or shipping address."){
@@ -159,7 +163,8 @@ type CustomerAddress @doc(description: "Contains detailed information about a cu
vat_id: String @doc(description: "The customer's Value-added tax (VAT) number (for corporate customers).")
default_shipping: Boolean @doc(description: "Indicates whether the address is the customer's default shipping address.")
default_billing: Boolean @doc(description: "Indicates whether the address is the customer's default billing address.")
- custom_attributes: [CustomerAddressAttribute] @deprecated(reason: "Custom attributes should not be put into a container.")
+ custom_attributes: [CustomerAddressAttribute] @deprecated(reason: "Use custom_attributesV2 instead.")
+ custom_attributesV2(attributeCodes: [ID!]): [AttributeValueInterface!]! @doc(description: "Custom attributes assigned to the customer address.") @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\CustomerAddressCustomAttributeFilter")
extension_attributes: [CustomerAddressAttribute] @doc(description: "Contains any extension attributes for the address.")
}
@@ -171,7 +176,7 @@ type CustomerAddressRegion @doc(description: "Defines the customer's state or pr
type CustomerAddressAttribute @doc(description: "Specifies the attribute code and value of a customer address attribute.") {
attribute_code: String @doc(description: "The name assigned to the customer address attribute.")
- value: String @doc(description: "The valuue assigned to the customer address attribute.")
+ value: String @doc(description: "The value assigned to the customer address attribute.")
}
type IsEmailAvailableOutput @doc(description: "Contains the result of the `isEmailAvailable` query.") {
@@ -425,3 +430,40 @@ enum CountryCodeEnum @doc(description: "The list of country codes.") {
ZM @doc(description: "Zambia")
ZW @doc(description: "Zimbabwe")
}
+
+enum AttributeEntityTypeEnum {
+ CUSTOMER
+ CUSTOMER_ADDRESS
+}
+
+type CustomerAttributeMetadata implements CustomAttributeMetadataInterface @doc(description: "Customer attribute metadata.") {
+ input_filter: InputFilterEnum @doc(description: "The template used for the input of the attribute (e.g., 'date').")
+ multiline_count: Int @doc(description: "The number of lines of the attribute value.")
+ sort_order: Int @doc(description: "The position of the attribute in the form.")
+ validate_rules: [ValidationRule] @doc(description: "The validation rules of the attribute value.")
+}
+
+enum InputFilterEnum @doc(description: "List of templates/filters applied to customer attribute input.") {
+ NONE @doc(description: "There are no templates or filters to be applied.")
+ DATE @doc(description: "Forces attribute input to follow the date format.")
+ TRIM @doc(description: "Strip whitespace (or other characters) from the beginning and end of the input.")
+ STRIPTAGS @doc(description: "Strip HTML Tags.")
+ ESCAPEHTML @doc(description: "Escape HTML Entities.")
+}
+
+type ValidationRule @doc(description: "Defines a customer attribute validation rule.") {
+ name: ValidationRuleEnum @doc(description: "Validation rule name applied to a customer attribute.")
+ value: String @doc(description: "Validation rule value.")
+}
+
+enum ValidationRuleEnum @doc(description: "List of validation rule names applied to a customer attribute.") {
+ DATE_RANGE_MAX
+ DATE_RANGE_MIN
+ FILE_EXTENSIONS
+ INPUT_VALIDATION
+ MAX_TEXT_LENGTH
+ MIN_TEXT_LENGTH
+ MAX_FILE_SIZE
+ MAX_IMAGE_HEIGHT
+ MAX_IMAGE_WIDTH
+}
diff --git a/app/code/Magento/CustomerImportExport/README.md b/app/code/Magento/CustomerImportExport/README.md
index 16c4189acfe63..50c978eae1a7a 100644
--- a/app/code/Magento/CustomerImportExport/README.md
+++ b/app/code/Magento/CustomerImportExport/README.md
@@ -15,6 +15,7 @@ Extension developers can interact with the Magento_CustomerImportExport module.
### Layouts
This module introduces the following layouts in the `view/adminhtml/layout` directory:
+
- `customer_import_export_index_exportcsv`
- `customer_import_export_index_exportxml`
- `customer_index_grid_block`
@@ -24,5 +25,6 @@ For more information about a layout in Magento 2, see the [Layout documentation]
## Additional information
You can get more information about import/export processes in magento at the articles:
+
- [Import](https://docs.magento.com/user-guide/system/data-import.html)
- [Export](https://docs.magento.com/user-guide/system/data-export.html)
diff --git a/app/code/Magento/CustomerImportExport/Test/Unit/Model/Export/AddressTest.php b/app/code/Magento/CustomerImportExport/Test/Unit/Model/Export/AddressTest.php
index 2d8c105d2b29c..10e3b5efcbdb0 100644
--- a/app/code/Magento/CustomerImportExport/Test/Unit/Model/Export/AddressTest.php
+++ b/app/code/Magento/CustomerImportExport/Test/Unit/Model/Export/AddressTest.php
@@ -38,7 +38,7 @@ class AddressTest extends TestCase
/**
* Test attribute code
*/
- const ATTRIBUTE_CODE = 'code1';
+ public const ATTRIBUTE_CODE = 'code1';
/**
* Websites array (website id => code)
@@ -52,10 +52,16 @@ class AddressTest extends TestCase
*
* @var array
*/
- protected $_attributes = [['attribute_id' => 1, 'attribute_code' => self::ATTRIBUTE_CODE]];
+ protected $_attributes = [
+ [
+ 'attribute_id' => 1,
+ 'attribute_code' => self::ATTRIBUTE_CODE,
+ 'frontend_input' => 'multiselect'
+ ]
+ ];
/**
- * Customer data
+ * Customer details
*
* @var array
*/
@@ -166,8 +172,11 @@ protected function _getModelDependencies()
true,
true,
true,
- ['_construct']
+ ['_construct', 'getSource']
);
+
+ $attributeSource = $this->createMock(\Magento\Eav\Model\Entity\Attribute\Source\AbstractSource::class);
+ $attribute->expects($this->once())->method('getSource')->willReturn($attributeSource);
$attributeCollection->addItem($attribute);
}
diff --git a/app/code/Magento/Deploy/README.md b/app/code/Magento/Deploy/README.md
index 0e4bdb11e0bb8..1d55d55b54e30 100644
--- a/app/code/Magento/Deploy/README.md
+++ b/app/code/Magento/Deploy/README.md
@@ -1,19 +1,23 @@
# Overview
+
## Purpose of module
-Deploy is a module that holds collection of services and command line tools to help with Magento application deployment.
+Deploy is a module that holds collection of services and command line tools to help with Magento application deployment.
To execute this command, please, run "bin/magento setup:static-content:deploy" from the Magento root directory.
-Deploy module contains 2 additional commands that allows switching between application modes (for instance from
+Deploy module contains 2 additional commands that allows switching between application modes (for instance from
development to
production) and show current application mode. To change the mode run "bin/magento deploy:mode:set [mode]".
Where mode can be one of the following:
+
- development
- production
-When switching to production mode, you can pass optional parameter skip-compilation to do not compile static files, CSS
+When switching to production mode, you can pass optional parameter skip-compilation to do not compile static files, CSS
and do not run the compilation process.
# Deployment
+
## System requirements
## Install
+
The Magento_Deploy module is installed automatically (using the native Magento install mechanism) without any additional actions.
diff --git a/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoDeveloperModeOnlyTestSuite.xml b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoDeveloperModeOnlyTestSuite.xml
index 3a7d3663c8875..8c3e0f750debd 100644
--- a/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoDeveloperModeOnlyTestSuite.xml
+++ b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoDeveloperModeOnlyTestSuite.xml
@@ -14,9 +14,7 @@
-
-
-
+
diff --git a/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoProductionModeOnlyTestSuite.xml b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoProductionModeOnlyTestSuite.xml
index bf7014cdbb49d..82ba4102736f7 100644
--- a/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoProductionModeOnlyTestSuite.xml
+++ b/app/code/Magento/Deploy/Test/Mftf/Suite/MagentoProductionModeOnlyTestSuite.xml
@@ -7,16 +7,8 @@
-->
-
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php b/app/code/Magento/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php
index b752eaa111fa4..1525637c017a1 100644
--- a/app/code/Magento/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php
+++ b/app/code/Magento/Developer/Model/Setup/Declaration/Schema/WhitelistGenerator.php
@@ -136,6 +136,7 @@ private function persistModule(Schema $schema, string $moduleName)
. Diff::GENERATED_WHITELIST_FILE_NAME;
//We need to load whitelist file and update it with new revision of code.
+ // phpcs:disable Magento2.Functions.DiscouragedFunction
if (file_exists($whiteListFileName)) {
$content = json_decode(file_get_contents($whiteListFileName), true);
}
@@ -183,6 +184,7 @@ private function getElementsWithFixedName(array $tableData): array
* @param string $tableName
* @param array $tableData
* @return array
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
private function getElementsWithAutogeneratedName(Schema $schema, string $tableName, array $tableData) : array
{
@@ -192,35 +194,42 @@ private function getElementsWithAutogeneratedName(Schema $schema, string $tableN
$elementType = 'index';
if (!empty($tableData[$elementType])) {
foreach ($tableData[$elementType] as $tableElementData) {
- $indexName = $this->elementNameResolver->getFullIndexName(
- $table,
- $tableElementData['column'],
- $tableElementData['indexType'] ?? null
- );
- $declaredStructure[$elementType][$indexName] = true;
+ if (isset($tableElementData['column'])) {
+ $indexName = $this->elementNameResolver->getFullIndexName(
+ $table,
+ $tableElementData['column'],
+ $tableElementData['indexType'] ?? null
+ );
+ $declaredStructure[$elementType][$indexName] = true;
+ }
}
}
$elementType = 'constraint';
if (!empty($tableData[$elementType])) {
foreach ($tableData[$elementType] as $tableElementData) {
- if ($tableElementData['type'] === 'foreign') {
- $referenceTable = $schema->getTableByName($tableElementData['referenceTable']);
- $column = $table->getColumnByName($tableElementData['column']);
- $referenceColumn = $referenceTable->getColumnByName($tableElementData['referenceColumn']);
- $constraintName = ($column !== false && $referenceColumn !== false) ?
- $this->elementNameResolver->getFullFKName(
+ $constraintName = null;
+ if (isset($tableElementData['type'], $tableElementData['column'])) {
+ if ($tableElementData['type'] === 'foreign') {
+ $column = $table->getColumnByName($tableElementData['column']);
+ $referenceTable = $schema->getTableByName($tableElementData['referenceTable'] ?? null);
+ $referenceColumn = ($referenceTable !== false)
+ ? $referenceTable->getColumnByName($tableElementData['referenceColumn'] ?? null) : false;
+
+ $constraintName = ($column !== false && $referenceColumn !== false) ?
+ $this->elementNameResolver->getFullFKName(
+ $table,
+ $column,
+ $referenceTable,
+ $referenceColumn
+ ) : null;
+ } else {
+ $constraintName = $this->elementNameResolver->getFullIndexName(
$table,
- $column,
- $referenceTable,
- $referenceColumn
- ) : null;
- } else {
- $constraintName = $this->elementNameResolver->getFullIndexName(
- $table,
- $tableElementData['column'],
- $tableElementData['type']
- );
+ $tableElementData['column'],
+ $tableElementData['type']
+ );
+ }
}
if ($constraintName) {
$declaredStructure[$elementType][$constraintName] = true;
diff --git a/app/code/Magento/Developer/README.md b/app/code/Magento/Developer/README.md
index d5a6a2cee9d46..aa29586df140d 100644
--- a/app/code/Magento/Developer/README.md
+++ b/app/code/Magento/Developer/README.md
@@ -8,4 +8,4 @@ Extension developers can interact with the Magento_Developer module. For more in
[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Developer module.
-A lot of functionality in the module is on JavaScript, use [mixins](https://developer.adobe.com/commerce/frontend-core/javascript/mixins/) to extend it.
\ No newline at end of file
+A lot of functionality in the module is on JavaScript, use [mixins](https://developer.adobe.com/commerce/frontend-core/javascript/mixins/) to extend it.
diff --git a/app/code/Magento/Dhl/Model/Carrier.php b/app/code/Magento/Dhl/Model/Carrier.php
index e64c7f1ae377b..76d91f924fd39 100644
--- a/app/code/Magento/Dhl/Model/Carrier.php
+++ b/app/code/Magento/Dhl/Model/Carrier.php
@@ -6,28 +6,61 @@
namespace Magento\Dhl\Model;
+use Exception;
use Laminas\Http\Request as HttpRequest;
use Magento\Catalog\Model\Product\Type;
+use Magento\CatalogInventory\Api\StockRegistryInterface;
use Magento\Dhl\Model\Validator\XmlValidator;
+use Magento\Directory\Helper\Data;
+use Magento\Directory\Model\CountryFactory;
+use Magento\Directory\Model\Currency;
+use Magento\Directory\Model\CurrencyFactory;
+use Magento\Directory\Model\RegionFactory;
+use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\ProductMetadataInterface;
use Magento\Framework\Async\CallbackDeferred;
+use Magento\Framework\DataObject;
+use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\Filesystem\Directory\ReadFactory;
use Magento\Framework\HTTP\AsyncClient\HttpException;
use Magento\Framework\HTTP\AsyncClient\HttpResponseDeferredInterface;
use Magento\Framework\HTTP\AsyncClient\Request;
use Magento\Framework\HTTP\AsyncClientInterface;
use Magento\Framework\HTTP\LaminasClient;
+use Magento\Framework\HTTP\LaminasClientFactory;
+use Magento\Framework\Math\Division;
use Magento\Framework\Measure\Length;
use Magento\Framework\Measure\Weight;
+use Magento\Framework\Model\AbstractModel;
use Magento\Framework\Module\Dir;
+use Magento\Framework\Module\Dir\Reader;
+use Magento\Framework\Stdlib\DateTime;
+use Magento\Framework\Stdlib\StringUtils;
use Magento\Framework\Xml\Security;
use Magento\Quote\Model\Quote\Address\RateRequest;
use Magento\Quote\Model\Quote\Address\RateResult\Error;
+use Magento\Quote\Model\Quote\Address\RateResult\Method;
+use Magento\Quote\Model\Quote\Address\RateResult\MethodFactory;
use Magento\Sales\Exception\DocumentValidationException;
use Magento\Sales\Model\Order\Shipment;
use Magento\Shipping\Model\Carrier\AbstractCarrier;
+use Magento\Shipping\Model\Carrier\CarrierInterface;
use Magento\Shipping\Model\Rate\Result;
use Magento\Shipping\Model\Rate\Result\ProxyDeferredFactory;
+use Magento\Shipping\Model\Shipment\Request as ShipmentRequest;
+use Magento\Shipping\Model\Simplexml\Element;
+use Magento\Shipping\Model\Simplexml\ElementFactory;
+use Magento\Shipping\Model\Tracking\Result\ErrorFactory;
+use Magento\Shipping\Model\Tracking\Result\StatusFactory;
+use Magento\Shipping\Model\Tracking\ResultFactory;
+use Magento\Store\Model\Information;
+use Magento\Store\Model\ScopeInterface;
+use Magento\Store\Model\StoreManagerInterface;
+use Psr\Log\LoggerInterface;
+use SimpleXMLElement;
+use Throwable;
+use const DATE_RFC3339;
/**
* DHL International (API v1.4)
@@ -35,7 +68,7 @@
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
-class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shipping\Model\Carrier\CarrierInterface
+class Carrier extends AbstractDhl implements CarrierInterface
{
/**#@+
* Carrier Product indicator
@@ -92,7 +125,7 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin
/**
* Countries parameters data
*
- * @var \Magento\Shipping\Model\Simplexml\Element|null
+ * @var Element|null
*/
protected $_countryParams;
@@ -165,7 +198,7 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin
/**
* Core string
*
- * @var \Magento\Framework\Stdlib\StringUtils
+ * @var StringUtils
*/
protected $string;
@@ -180,32 +213,32 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin
protected $_coreDate;
/**
- * @var \Magento\Store\Model\StoreManagerInterface
+ * @var StoreManagerInterface
*/
protected $_storeManager;
/**
- * @var \Magento\Framework\Module\Dir\Reader
+ * @var Reader
*/
protected $_configReader;
/**
- * @var \Magento\Framework\Math\Division
+ * @var Division
*/
protected $mathDivision;
/**
- * @var \Magento\Framework\Filesystem\Directory\ReadFactory
+ * @var ReadFactory
*/
protected $readFactory;
/**
- * @var \Magento\Framework\Stdlib\DateTime
+ * @var DateTime
*/
protected $_dateTime;
/**
- * @var \Magento\Framework\HTTP\LaminasClientFactory
+ * @var LaminasClientFactory
* phpcs:ignore Magento2.Commenting.ClassAndInterfacePHPDocFormatting
* @deprecated Use asynchronous client.
* @see $httpClient
@@ -222,7 +255,7 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin
/**
* Xml response validator
*
- * @var \Magento\Dhl\Model\Validator\XmlValidator
+ * @var XmlValidator
*/
private $xmlValidator;
@@ -242,64 +275,64 @@ class Carrier extends \Magento\Dhl\Model\AbstractDhl implements \Magento\Shippin
private $proxyDeferredFactory;
/**
- * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
+ * @param ScopeConfigInterface $scopeConfig
* @param \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory
- * @param \Psr\Log\LoggerInterface $logger
+ * @param LoggerInterface $logger
* @param Security $xmlSecurity
- * @param \Magento\Shipping\Model\Simplexml\ElementFactory $xmlElFactory
+ * @param ElementFactory $xmlElFactory
* @param \Magento\Shipping\Model\Rate\ResultFactory $rateFactory
- * @param \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory $rateMethodFactory
- * @param \Magento\Shipping\Model\Tracking\ResultFactory $trackFactory
- * @param \Magento\Shipping\Model\Tracking\Result\ErrorFactory $trackErrorFactory
- * @param \Magento\Shipping\Model\Tracking\Result\StatusFactory $trackStatusFactory
- * @param \Magento\Directory\Model\RegionFactory $regionFactory
- * @param \Magento\Directory\Model\CountryFactory $countryFactory
- * @param \Magento\Directory\Model\CurrencyFactory $currencyFactory
- * @param \Magento\Directory\Helper\Data $directoryData
- * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry
+ * @param MethodFactory $rateMethodFactory
+ * @param ResultFactory $trackFactory
+ * @param ErrorFactory $trackErrorFactory
+ * @param StatusFactory $trackStatusFactory
+ * @param RegionFactory $regionFactory
+ * @param CountryFactory $countryFactory
+ * @param CurrencyFactory $currencyFactory
+ * @param Data $directoryData
+ * @param StockRegistryInterface $stockRegistry
* @param \Magento\Shipping\Helper\Carrier $carrierHelper
* @param \Magento\Framework\Stdlib\DateTime\DateTime $coreDate
- * @param \Magento\Framework\Module\Dir\Reader $configReader
- * @param \Magento\Store\Model\StoreManagerInterface $storeManager
- * @param \Magento\Framework\Stdlib\StringUtils $string
- * @param \Magento\Framework\Math\Division $mathDivision
- * @param \Magento\Framework\Filesystem\Directory\ReadFactory $readFactory
- * @param \Magento\Framework\Stdlib\DateTime $dateTime
- * @param \Magento\Framework\HTTP\LaminasClientFactory $httpClientFactory
+ * @param Reader $configReader
+ * @param StoreManagerInterface $storeManager
+ * @param StringUtils $string
+ * @param Division $mathDivision
+ * @param ReadFactory $readFactory
+ * @param DateTime $dateTime
+ * @param LaminasClientFactory $httpClientFactory
* @param array $data
- * @param \Magento\Dhl\Model\Validator\XmlValidator|null $xmlValidator
+ * @param XmlValidator|null $xmlValidator
* @param ProductMetadataInterface|null $productMetadata
* @param AsyncClientInterface|null $httpClient
* @param ProxyDeferredFactory|null $proxyDeferredFactory
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
- \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
+ ScopeConfigInterface $scopeConfig,
\Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory,
- \Psr\Log\LoggerInterface $logger,
+ LoggerInterface $logger,
Security $xmlSecurity,
- \Magento\Shipping\Model\Simplexml\ElementFactory $xmlElFactory,
+ ElementFactory $xmlElFactory,
\Magento\Shipping\Model\Rate\ResultFactory $rateFactory,
- \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory $rateMethodFactory,
- \Magento\Shipping\Model\Tracking\ResultFactory $trackFactory,
- \Magento\Shipping\Model\Tracking\Result\ErrorFactory $trackErrorFactory,
- \Magento\Shipping\Model\Tracking\Result\StatusFactory $trackStatusFactory,
- \Magento\Directory\Model\RegionFactory $regionFactory,
- \Magento\Directory\Model\CountryFactory $countryFactory,
- \Magento\Directory\Model\CurrencyFactory $currencyFactory,
- \Magento\Directory\Helper\Data $directoryData,
- \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry,
+ MethodFactory $rateMethodFactory,
+ ResultFactory $trackFactory,
+ ErrorFactory $trackErrorFactory,
+ StatusFactory $trackStatusFactory,
+ RegionFactory $regionFactory,
+ CountryFactory $countryFactory,
+ CurrencyFactory $currencyFactory,
+ Data $directoryData,
+ StockRegistryInterface $stockRegistry,
\Magento\Shipping\Helper\Carrier $carrierHelper,
\Magento\Framework\Stdlib\DateTime\DateTime $coreDate,
- \Magento\Framework\Module\Dir\Reader $configReader,
- \Magento\Store\Model\StoreManagerInterface $storeManager,
- \Magento\Framework\Stdlib\StringUtils $string,
- \Magento\Framework\Math\Division $mathDivision,
- \Magento\Framework\Filesystem\Directory\ReadFactory $readFactory,
- \Magento\Framework\Stdlib\DateTime $dateTime,
- \Magento\Framework\HTTP\LaminasClientFactory $httpClientFactory,
+ Reader $configReader,
+ StoreManagerInterface $storeManager,
+ StringUtils $string,
+ Division $mathDivision,
+ ReadFactory $readFactory,
+ DateTime $dateTime,
+ LaminasClientFactory $httpClientFactory,
array $data = [],
- \Magento\Dhl\Model\Validator\XmlValidator $xmlValidator = null,
+ XmlValidator $xmlValidator = null,
ProductMetadataInterface $productMetadata = null,
?AsyncClientInterface $httpClient = null,
?ProxyDeferredFactory $proxyDeferredFactory = null
@@ -353,7 +386,7 @@ protected function _getDefaultValue($origValue, $pathToValue)
if (!$origValue) {
$origValue = $this->_scopeConfig->getValue(
$pathToValue,
- \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
+ ScopeInterface::SCOPE_STORE,
$this->getStore()
);
}
@@ -377,7 +410,7 @@ public function collectRates(RateRequest $request)
$this->setStore($requestDhl->getStoreId());
$origCompanyName = $this->_getDefaultValue(
$requestDhl->getOrigCompanyName(),
- \Magento\Store\Model\Information::XML_PATH_STORE_INFO_NAME
+ Information::XML_PATH_STORE_INFO_NAME
);
$origCountryId = $this->_getDefaultValue($requestDhl->getOrigCountryId(), Shipment::XML_PATH_STORE_COUNTRY_ID);
$origState = $this->_getDefaultValue($requestDhl->getOrigState(), Shipment::XML_PATH_STORE_REGION_ID);
@@ -434,10 +467,10 @@ public function getResult()
/**
* Fills request object with Dhl config parameters
*
- * @param \Magento\Framework\DataObject $requestObject
- * @return \Magento\Framework\DataObject
+ * @param DataObject $requestObject
+ * @return DataObject
*/
- protected function _addParams(\Magento\Framework\DataObject $requestObject)
+ protected function _addParams(DataObject $requestObject)
{
foreach ($this->_requestVariables as $code => $objectCode) {
if ($this->_request->getDhlId()) {
@@ -454,17 +487,17 @@ protected function _addParams(\Magento\Framework\DataObject $requestObject)
/**
* Prepare and set request in property of current instance
*
- * @param \Magento\Framework\DataObject $request
+ * @param DataObject $request
* @return $this
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
- public function setRequest(\Magento\Framework\DataObject $request)
+ public function setRequest(DataObject $request)
{
$this->_request = $request;
$this->setStore($request->getStoreId());
- $requestObject = new \Magento\Framework\DataObject();
+ $requestObject = new DataObject();
$requestObject->setIsGenerateLabelReturn($request->getIsGenerateLabelReturn());
@@ -502,7 +535,7 @@ public function setRequest(\Magento\Framework\DataObject $request)
->setOrigEmail(
$this->_scopeConfig->getValue(
'trans_email/ident_general/email',
- \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
+ ScopeInterface::SCOPE_STORE,
$requestObject->getStoreId()
)
)
@@ -515,7 +548,7 @@ public function setRequest(\Magento\Framework\DataObject $request)
$originStreet2 = $this->_scopeConfig->getValue(
Shipment::XML_PATH_STORE_ADDRESS2,
- \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
+ ScopeInterface::SCOPE_STORE,
$requestObject->getStoreId()
);
@@ -562,7 +595,7 @@ public function setRequest(\Magento\Framework\DataObject $request)
* Get allowed shipping methods
*
* @return string[]
- * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws LocalizedException
*/
public function getAllowedMethods()
{
@@ -582,7 +615,7 @@ public function getAllowedMethods()
$allowedMethods = explode(',', $this->getConfigData('nondoc_methods') ?? '');
break;
default:
- throw new \Magento\Framework\Exception\LocalizedException(__('Wrong Content Type'));
+ throw new LocalizedException(__('Wrong Content Type'));
}
}
$methods = [];
@@ -842,11 +875,11 @@ protected function _getAllItems()
/**
* Make pieces
*
- * @param \Magento\Shipping\Model\Simplexml\Element $nodeBkgDetails
+ * @param Element $nodeBkgDetails
* @return void
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
- protected function _makePieces(\Magento\Shipping\Model\Simplexml\Element $nodeBkgDetails)
+ protected function _makePieces(Element $nodeBkgDetails)
{
$divideOrderWeight = (string)$this->getConfigData('divide_order_weight');
$nodePieces = $nodeBkgDetails->addChild('Pieces', '', '');
@@ -951,7 +984,7 @@ protected function _getDimension($dimension, $configWeightUnit = false)
/**
* Add dimension to piece
*
- * @param \Magento\Shipping\Model\Simplexml\Element $nodePiece
+ * @param Element $nodePiece
* @return void
*/
protected function _addDimension($nodePiece)
@@ -1003,7 +1036,7 @@ function (array $a, array $b): int {
);
$unavailable = true;
}
- } catch (\Throwable $exception) {
+ } catch (Throwable $exception) {
//Failed to read response
$unavailable = true;
$this->_errors[$exception->getCode()] = $exception->getMessage();
@@ -1026,7 +1059,7 @@ function (array $a, array $b): int {
/**
* Get shipping quotes
*
- * @return \Magento\Framework\Model\AbstractModel|Result
+ * @return AbstractModel|Result
*/
protected function _getQuotes()
{
@@ -1047,7 +1080,7 @@ protected function _getQuotes()
(string)$this->getConfigData('gateway_url'),
Request::METHOD_POST,
['Content-Type' => 'application/xml'],
- utf8_encode($request)
+ mb_convert_encoding($request, 'UTF-8')
)
),
'date' => $date,
@@ -1105,7 +1138,7 @@ protected function _getQuotesFromServer($request)
$client = $this->_httpClientFactory->create();
$client->setUri($this->getGatewayURL());
$client->setOptions(['maxredirects' => 0, 'timeout' => 30]);
- $client->setRawBody(utf8_encode($request));
+ $client->setRawBody(mb_convert_encoding($request, 'UTF-8'));
$client->setMethod(HttpRequest::METHOD_POST);
return $client->send()->getBody();
@@ -1114,7 +1147,7 @@ protected function _getQuotesFromServer($request)
/**
* Build quotes request XML object
*
- * @return \SimpleXMLElement
+ * @return SimpleXMLElement
*/
protected function _buildQuotesRequestXml()
{
@@ -1152,7 +1185,7 @@ protected function _buildQuotesRequestXml()
$nodeBkgDetails->addChild('PaymentCountryCode', $rawRequest->getOrigCountryId());
$nodeBkgDetails->addChild(
'Date',
- (new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT)
+ (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT)
);
$nodeBkgDetails->addChild('ReadyTime', 'PT' . (int)(string)$this->getConfigData('ready_time') . 'H00M');
@@ -1185,11 +1218,11 @@ protected function _buildQuotesRequestXml()
/**
* Set pick-up date in request XML object
*
- * @param \SimpleXMLElement $requestXml
+ * @param SimpleXMLElement $requestXml
* @param string $date
- * @return \SimpleXMLElement
+ * @return SimpleXMLElement
*/
- protected function _setQuotesRequestXmlDate(\SimpleXMLElement $requestXml, $date)
+ protected function _setQuotesRequestXmlDate(SimpleXMLElement $requestXml, $date)
{
$requestXml->GetQuote->BkgDetails->Date = $date;
@@ -1200,8 +1233,8 @@ protected function _setQuotesRequestXmlDate(\SimpleXMLElement $requestXml, $date
* Parse response from DHL web service
*
* @param string $response
- * @return bool|\Magento\Framework\DataObject|Result|Error
- * @throws \Magento\Framework\Exception\LocalizedException
+ * @return bool|DataObject|Result|Error
+ * @throws LocalizedException
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
protected function _parseResponse($response)
@@ -1232,7 +1265,7 @@ protected function _parseResponse($response)
foreach ($this->_rates as $rate) {
$method = $rate['service'];
$data = $rate['data'];
- /* @var $rate \Magento\Quote\Model\Quote\Address\RateResult\Method */
+ /* @var $rate Method */
$rate = $this->_rateMethodFactory->create();
$rate->setCarrier(self::CODE);
$rate->setCarrierTitle($this->getConfigData('title'));
@@ -1245,7 +1278,7 @@ protected function _parseResponse($response)
} else {
if (!empty($this->_errors)) {
if ($this->_isShippingLabelFlag) {
- throw new \Magento\Framework\Exception\LocalizedException($responseError);
+ throw new LocalizedException($responseError);
}
$this->debugErrors($this->_errors);
}
@@ -1258,11 +1291,11 @@ protected function _parseResponse($response)
/**
* Add rate to DHL rates array
*
- * @param \SimpleXMLElement $shipmentDetails
+ * @param SimpleXMLElement $shipmentDetails
* @return $this
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
- protected function _addRate(\SimpleXMLElement $shipmentDetails)
+ protected function _addRate(SimpleXMLElement $shipmentDetails)
{
if (isset($shipmentDetails->ProductShortName)
&& isset($shipmentDetails->ShippingCharge)
@@ -1279,7 +1312,7 @@ protected function _addRate(\SimpleXMLElement $shipmentDetails)
$dhlProductDescription = $this->getDhlProductTitle($dhlProduct);
if ($currencyCode != $baseCurrencyCode) {
- /* @var $currency \Magento\Directory\Model\Currency */
+ /* @var $currency Currency */
$currency = $this->_currencyFactory->create();
$rates = $currency->getCurrencyRates($currencyCode, [$baseCurrencyCode]);
if (!empty($rates) && isset($rates[$baseCurrencyCode])) {
@@ -1334,14 +1367,14 @@ protected function _addRate(\SimpleXMLElement $shipmentDetails)
* Returns dimension unit (cm or inch)
*
* @return string
- * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws LocalizedException
*/
protected function _getDimensionUnit()
{
$countryId = $this->_rawRequest->getOrigCountryId();
$measureUnit = $this->getCountryParams($countryId)->getMeasureUnit();
if (empty($measureUnit)) {
- throw new \Magento\Framework\Exception\LocalizedException(
+ throw new LocalizedException(
__("Cannot identify measure unit for %1", $countryId)
);
}
@@ -1353,14 +1386,14 @@ protected function _getDimensionUnit()
* Returns weight unit (kg or pound)
*
* @return string
- * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws LocalizedException
*/
protected function _getWeightUnit()
{
$countryId = $this->_rawRequest->getOrigCountryId();
$weightUnit = $this->getCountryParams($countryId)->getWeightUnit();
if (empty($weightUnit)) {
- throw new \Magento\Framework\Exception\LocalizedException(
+ throw new LocalizedException(
__("Cannot identify weight unit for %1", $countryId)
);
}
@@ -1372,7 +1405,7 @@ protected function _getWeightUnit()
* Get Country Params by Country Code
*
* @param string $countryCode
- * @return \Magento\Framework\DataObject
+ * @return DataObject
*
* @see $countryCode ISO 3166 Codes (Countries) A2
*/
@@ -1385,19 +1418,20 @@ protected function getCountryParams($countryCode)
$this->_countryParams = $this->_xmlElFactory->create(['data' => $countriesXml]);
}
if (isset($this->_countryParams->{$countryCode})) {
- $countryParams = new \Magento\Framework\DataObject($this->_countryParams->{$countryCode}->asArray());
+ $countryParams = new DataObject($this->_countryParams->{$countryCode}->asArray());
}
- return $countryParams ?? new \Magento\Framework\DataObject();
+ return $countryParams ?? new DataObject();
}
/**
* Do shipment request to carrier web service, obtain Print Shipping Labels and process errors in response
*
- * @param \Magento\Framework\DataObject $request
- * @return \Magento\Framework\DataObject
+ * @param DataObject $request
+ * @return DataObject
*/
- protected function _doShipmentRequest(\Magento\Framework\DataObject $request)
+ protected function _doShipmentRequest(DataObject $request)
{
+
$this->_prepareShipmentRequest($request);
$this->_mapRequestToShipment($request);
$this->setRequest($request);
@@ -1408,13 +1442,13 @@ protected function _doShipmentRequest(\Magento\Framework\DataObject $request)
/**
* Processing additional validation to check is carrier applicable.
*
- * @param \Magento\Framework\DataObject $request
- * @return $this|\Magento\Framework\DataObject|boolean
+ * @param DataObject $request
+ * @return $this|DataObject|boolean
* phpcs:disable Magento2.Annotation.MethodAnnotationStructure
* @deprecated 100.2.3
* @see use processAdditionalValidation method instead
*/
- public function proccessAdditionalValidation(\Magento\Framework\DataObject $request)
+ public function proccessAdditionalValidation(DataObject $request)
{
return $this->processAdditionalValidation($request);
}
@@ -1422,10 +1456,10 @@ public function proccessAdditionalValidation(\Magento\Framework\DataObject $requ
/**
* Processing additional validation to check is carrier applicable.
*
- * @param \Magento\Framework\DataObject $request
- * @return $this|\Magento\Framework\DataObject|boolean
+ * @param DataObject $request
+ * @return $this|DataObject|boolean
*/
- public function processAdditionalValidation(\Magento\Framework\DataObject $request)
+ public function processAdditionalValidation(DataObject $request)
{
//Skip by item validation if there is no items in request
if (empty($this->getAllItems($request))) {
@@ -1435,7 +1469,7 @@ public function processAdditionalValidation(\Magento\Framework\DataObject $reque
$countryParams = $this->getCountryParams(
$this->_scopeConfig->getValue(
Shipment::XML_PATH_STORE_COUNTRY_ID,
- \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
+ ScopeInterface::SCOPE_STORE,
$request->getStoreId()
)
);
@@ -1455,11 +1489,11 @@ public function processAdditionalValidation(\Magento\Framework\DataObject $reque
/**
* Return container types of carrier
*
- * @param \Magento\Framework\DataObject|null $params
+ * @param DataObject|null $params
* @return array
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
- public function getContainerTypes(\Magento\Framework\DataObject $params = null)
+ public function getContainerTypes(DataObject $params = null)
{
return [
self::DHL_CONTENT_TYPE_DOC => __('Documents'),
@@ -1470,11 +1504,11 @@ public function getContainerTypes(\Magento\Framework\DataObject $params = null)
/**
* Map request to shipment
*
- * @param \Magento\Framework\DataObject $request
+ * @param DataObject $request
* @return void
- * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws LocalizedException
*/
- protected function _mapRequestToShipment(\Magento\Framework\DataObject $request)
+ protected function _mapRequestToShipment(DataObject $request)
{
$request->setOrigCountryId($request->getShipperAddressCountryCode());
$this->setRawRequest($request);
@@ -1487,7 +1521,7 @@ protected function _mapRequestToShipment(\Magento\Framework\DataObject $request)
$minValue = $this->_getMinDimension($params['dimension_units']);
if ($params['width'] < $minValue || $params['length'] < $minValue || $params['height'] < $minValue) {
$message = __('Height, width and length should be equal or greater than %1', $minValue);
- throw new \Magento\Framework\Exception\LocalizedException($message);
+ throw new LocalizedException($message);
}
}
@@ -1525,8 +1559,8 @@ protected function _getMinDimension($dimensionUnit)
/**
* Do rate request and handle errors
*
- * @return Result|\Magento\Framework\DataObject
- * @throws \Magento\Framework\Exception\LocalizedException
+ * @return Result|DataObject
+ * @throws LocalizedException
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
@@ -1562,7 +1596,7 @@ protected function _doRequest()
$originRegion = $this->getCountryParams(
$this->_scopeConfig->getValue(
Shipment::XML_PATH_STORE_COUNTRY_ID,
- \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
+ ScopeInterface::SCOPE_STORE,
$this->getStore()
)
)->getRegion();
@@ -1658,8 +1692,10 @@ protected function _doRequest()
$baseCurrencyCode = $this->_storeManager->getWebsite($rawRequest->getWebsiteId())->getBaseCurrencyCode();
$nodeDutiable->addChild('DeclaredCurrency', $baseCurrencyCode);
$nodeDutiable->addChild('TermsOfTrade', 'DAP');
- }
+ /** Export Declaration */
+ $this->addExportDeclaration($xml, $rawRequest);
+ }
/**
* Reference
* This element identifies the reference information. It is an optional field in the
@@ -1716,7 +1752,7 @@ protected function _doRequest()
$request = $xml->asXML();
if ($request && !(mb_detect_encoding($request) == 'UTF-8')) {
- $request = utf8_encode($request);
+ $request = mb_convert_encoding($request, 'UTF-8');
}
$responseBody = $this->_getCachedQuotes($request);
@@ -1731,10 +1767,10 @@ protected function _doRequest()
$request
)
);
- $responseBody = utf8_decode($response->get()->getBody());
+ $responseBody = mb_convert_encoding($response->get()->getBody(), 'ISO-8859-1', 'UTF-8');
$debugData['result'] = $this->filterDebugData($responseBody);
$this->_setCachedQuotes($request, $responseBody);
- } catch (\Exception $e) {
+ } catch (Exception $e) {
$this->_errors[$e->getCode()] = $e->getMessage();
$responseBody = '';
}
@@ -1747,7 +1783,7 @@ protected function _doRequest()
/**
* Generation Shipment Details Node according to origin region
*
- * @param \Magento\Shipping\Model\Simplexml\Element $xml
+ * @param Element $xml
* @param RateRequest $rawRequest
* @param string $originRegion
* @return void
@@ -1880,7 +1916,7 @@ protected function _getXMLTracking($trackings)
//$xml->addChild('PiecesEnabled', 'ALL_CHECK_POINTS');
$request = $xml->asXML();
- $request = utf8_encode($request);
+ $request = mb_convert_encoding($request, 'UTF-8');
$responseBody = $this->_getCachedQuotes($request);
if ($responseBody === null) {
@@ -1897,7 +1933,7 @@ protected function _getXMLTracking($trackings)
$responseBody = $response->get()->getBody();
$debugData['result'] = $this->filterDebugData($responseBody);
$this->_setCachedQuotes($request, $responseBody);
- } catch (\Exception $e) {
+ } catch (Exception $e) {
$this->_errors[$e->getCode()] = $e->getMessage();
$responseBody = '';
}
@@ -1922,7 +1958,7 @@ protected function _parseXmlTrackingResponse($trackings, $response)
$resultArr = [];
if (!empty(trim($response))) {
- $xml = $this->parseXml($response, \Magento\Shipping\Model\Simplexml\Element::class);
+ $xml = $this->parseXml($response, Element::class);
if (!is_object($xml)) {
$errorTitle = __('Response is in the wrong format');
}
@@ -2021,19 +2057,19 @@ protected function _getPerpackagePrice($cost, $handlingType, $handlingFee)
/**
* Do request to shipment
*
- * @param \Magento\Shipping\Model\Shipment\Request $request
- * @return array|\Magento\Framework\DataObject
- * @throws \Magento\Framework\Exception\LocalizedException
+ * @param ShipmentRequest $request
+ * @return array|DataObject
+ * @throws LocalizedException
*/
public function requestToShipment($request)
{
$packages = $request->getPackages();
if (!is_array($packages) || !$packages) {
- throw new \Magento\Framework\Exception\LocalizedException(__('No packages for request'));
+ throw new LocalizedException(__('No packages for request'));
}
$result = $this->_doShipmentRequest($request);
- $response = new \Magento\Framework\DataObject(
+ $response = new DataObject(
[
'info' => [
[
@@ -2078,23 +2114,23 @@ protected function _checkDomesticStatus($origCountryCode, $destCountryCode)
/**
* Prepare shipping label data
*
- * @param \SimpleXMLElement $xml
- * @return \Magento\Framework\DataObject
- * @throws \Magento\Framework\Exception\LocalizedException
+ * @param SimpleXMLElement $xml
+ * @return DataObject
+ * @throws LocalizedException
*/
- protected function _prepareShippingLabelContent(\SimpleXMLElement $xml)
+ protected function _prepareShippingLabelContent(SimpleXMLElement $xml)
{
- $result = new \Magento\Framework\DataObject();
+ $result = new DataObject();
try {
if (!isset($xml->AirwayBillNumber) || !isset($xml->LabelImage->OutputImage)) {
- throw new \Magento\Framework\Exception\LocalizedException(__('Unable to retrieve shipping label'));
+ throw new LocalizedException(__('Unable to retrieve shipping label'));
}
$result->setTrackingNumber((string)$xml->AirwayBillNumber);
$labelContent = (string)$xml->LabelImage->OutputImage;
// phpcs:ignore Magento2.Functions.DiscouragedFunction
$result->setShippingLabelContent(base64_decode($labelContent));
- } catch (\Exception $e) {
- throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage()));
+ } catch (Exception $e) {
+ throw new LocalizedException(__($e->getMessage()));
}
return $result;
@@ -2123,7 +2159,7 @@ protected function isDutiable($origCountryId, $destCountryId): bool
*/
private function buildMessageTimestamp(string $datetime = null): string
{
- return $this->_coreDate->date(\DATE_RFC3339, $datetime);
+ return $this->_coreDate->date(DATE_RFC3339, $datetime);
}
/**
@@ -2131,7 +2167,7 @@ private function buildMessageTimestamp(string $datetime = null): string
*
* @param string $servicePrefix
* @return string
- * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws LocalizedException
*/
private function buildMessageReference(string $servicePrefix): string
{
@@ -2142,7 +2178,7 @@ private function buildMessageReference(string $servicePrefix): string
];
if (!in_array($servicePrefix, $validPrefixes)) {
- throw new \Magento\Framework\Exception\LocalizedException(
+ throw new LocalizedException(
__("Invalid service prefix \"$servicePrefix\" provided while attempting to build MessageReference")
);
}
@@ -2183,4 +2219,65 @@ private function getGatewayURL(): string
return (string)$this->getConfigData('gateway_url');
}
}
+
+ /**
+ * Generating Export Declaration Details
+ *
+ * @param Element $xml
+ * @param ShipmentRequest $rawRequest
+ * @return void
+ */
+ private function addExportDeclaration(Element $xml, ShipmentRequest $rawRequest): void
+ {
+ $nodeExportDeclaration = $xml->addChild('ExportDeclaration', '', '');
+ $nodeExportDeclaration->addChild(
+ 'InvoiceNumber',
+ $rawRequest->getOrderShipment()->getOrder()->hasInvoices()
+ ? $this->getInvoiceNumbers($rawRequest)
+ : $rawRequest->getOrderShipment()->getOrder()->getIncrementId()
+ );
+ $nodeExportDeclaration->addChild(
+ 'InvoiceDate',
+ date("Y-m-d", strtotime((string)$rawRequest->getOrderShipment()->getOrder()->getCreatedAt()))
+ );
+ $exportItems = $rawRequest->getPackages();
+ foreach ($exportItems as $exportItem) {
+ $itemWeightUnit = $exportItem['params']['weight_units'] ? substr(
+ $exportItem['params']['weight_units'],
+ 0,
+ 1
+ ) : 'L';
+ foreach ($exportItem['items'] as $itemNo => $itemData) {
+ $nodeExportItem = $nodeExportDeclaration->addChild('ExportLineItem', '', '');
+ $nodeExportItem->addChild('LineNumber', $itemNo);
+ $nodeExportItem->addChild('Quantity', $itemData['qty']);
+ $nodeExportItem->addChild('QuantityUnit', 'PCS');
+ $nodeExportItem->addChild('Description', $itemData['name']);
+ $nodeExportItem->addChild('Value', $itemData['price']);
+ $nodeItemWeight = $nodeExportItem->addChild('Weight', '', '');
+ $nodeItemWeight->addChild('Weight', $itemData['weight']);
+ $nodeItemWeight->addChild('WeightUnit', $itemWeightUnit);
+ $nodeItemGrossWeight = $nodeExportItem->addChild('GrossWeight');
+ $nodeItemGrossWeight->addChild('Weight', $itemData['weight']);
+ $nodeItemGrossWeight->addChild('WeightUnit', $itemWeightUnit);
+ $nodeExportItem->addChild('ManufactureCountryCode', $rawRequest->getShipperAddressCountryCode());
+ }
+ }
+ }
+
+ /**
+ * Fetching Shipment Order Invoice No
+ *
+ * @param ShipmentRequest $rawRequest
+ * @return string
+ */
+ private function getInvoiceNumbers(ShipmentRequest $rawRequest): string
+ {
+ $invoiceNumbers = [];
+ $order = $rawRequest->getOrderShipment()->getOrder();
+ foreach ($order->getInvoiceCollection() as $invoice) {
+ $invoiceNumbers[] = $invoice->getIncrementId();
+ }
+ return implode(',', $invoiceNumbers);
+ }
}
diff --git a/app/code/Magento/Directory/Helper/Data.php b/app/code/Magento/Directory/Helper/Data.php
index 8473d0ae426ee..d0ad6f15705da 100644
--- a/app/code/Magento/Directory/Helper/Data.php
+++ b/app/code/Magento/Directory/Helper/Data.php
@@ -16,6 +16,7 @@
use Magento\Framework\App\Helper\AbstractHelper;
use Magento\Framework\App\Helper\Context;
use Magento\Framework\Json\Helper\Data as JsonData;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Store\Model\ScopeInterface;
use Magento\Store\Model\StoreManagerInterface;
@@ -26,7 +27,7 @@
* @since 100.0.2
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
-class Data extends AbstractHelper
+class Data extends AbstractHelper implements ResetAfterRequestInterface
{
private const STORE_ID = 'store_id';
@@ -435,4 +436,13 @@ private function getCurrentScope(): array
return $scope;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->_regionJson = null;
+ $this->_currencyCache = [];
+ }
}
diff --git a/app/code/Magento/Directory/Model/Country.php b/app/code/Magento/Directory/Model/Country.php
index bc18e9bbd9531..ff8d4514d225d 100644
--- a/app/code/Magento/Directory/Model/Country.php
+++ b/app/code/Magento/Directory/Model/Country.php
@@ -65,6 +65,8 @@ public function __construct(
}
/**
+ * Country model constructor
+ *
* @return void
*/
protected function _construct()
@@ -95,6 +97,8 @@ public function getRegions()
}
/**
+ * Get region collection with loaded data
+ *
* @return \Magento\Directory\Model\ResourceModel\Region\Collection
*/
public function getLoadedRegionCollection()
@@ -105,6 +109,8 @@ public function getLoadedRegionCollection()
}
/**
+ * Get region collection
+ *
* @return \Magento\Directory\Model\ResourceModel\Region\Collection
*/
public function getRegionCollection()
@@ -115,6 +121,8 @@ public function getRegionCollection()
}
/**
+ * Format address
+ *
* @param \Magento\Framework\DataObject $address
* @param bool $html
* @return string
@@ -175,6 +183,14 @@ public function getFormats()
return null;
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ self::$_format = [];
+ }
+
/**
* Retrieve country format
*
@@ -196,6 +212,7 @@ public function getFormat($type)
/**
* Get country name
*
+ * @param mixed $locale
* @return string
*/
public function getName($locale = null)
diff --git a/app/code/Magento/Directory/Model/ResourceModel/Currency.php b/app/code/Magento/Directory/Model/ResourceModel/Currency.php
index f84de7c3593fa..29499b0565a9a 100644
--- a/app/code/Magento/Directory/Model/ResourceModel/Currency.php
+++ b/app/code/Magento/Directory/Model/ResourceModel/Currency.php
@@ -6,16 +6,18 @@
namespace Magento\Directory\Model\ResourceModel;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
+
/**
* Currency Resource Model
*
* @api
* @since 100.0.2
*/
-class Currency extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
+class Currency extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb implements ResetAfterRequestInterface
{
/**
- * Currency rate table
+ * Currency rate table name
*
* @var string
*/
@@ -233,4 +235,12 @@ protected function _getRatesByCode($code, $toCurrencies = null)
return $result;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ self::$_rateCache = [];
+ }
}
diff --git a/app/code/Magento/Directory/Setup/Patch/Data/AddDataForCostaRica.php b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForCostaRica.php
new file mode 100644
index 0000000000000..23eb9eaa4c2f3
--- /dev/null
+++ b/app/code/Magento/Directory/Setup/Patch/Data/AddDataForCostaRica.php
@@ -0,0 +1,103 @@
+moduleDataSetup = $moduleDataSetup;
+ $this->dataInstallerFactory = $dataInstallerFactory;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function apply()
+ {
+ /** @var DataInstaller $dataInstaller */
+ $dataInstaller = $this->dataInstallerFactory->create();
+ $dataInstaller->addCountryRegions(
+ $this->moduleDataSetup->getConnection(),
+ $this->getDataForCostaRica()
+ );
+
+ return $this;
+ }
+
+ /**
+ * Costa Rica states data.Pura Vida :)
+ *
+ * @return array
+ */
+ private function getDataForCostaRica(): array
+ {
+ return [
+ ['CR', 'CR-SJ', 'San José'],
+ ['CR', 'CR-AL', 'Alajuela'],
+ ['CR', 'CR-CA', 'Cartago'],
+ ['CR', 'CR-HE', 'Heredia'],
+ ['CR', 'CR-GU', 'Guanacaste'],
+ ['CR', 'CR-PU', 'Puntarenas'],
+ ['CR', 'CR-LI', 'Limón']
+ ];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public static function getDependencies()
+ {
+ return [
+ InitializeDirectoryData::class,
+ ];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getAliases()
+ {
+ return [];
+ }
+
+ /**
+ * Get version
+ *
+ * @return string
+ */
+ public static function getVersion()
+ {
+ return '2.4.2';
+ }
+}
diff --git a/app/code/Magento/Directory/view/frontend/web/js/region-updater.js b/app/code/Magento/Directory/view/frontend/web/js/region-updater.js
index 9d22cf90f258e..e6e08fddacda4 100644
--- a/app/code/Magento/Directory/view/frontend/web/js/region-updater.js
+++ b/app/code/Magento/Directory/view/frontend/web/js/region-updater.js
@@ -40,6 +40,10 @@ define([
$(this.options.regionListId).on('change', $.proxy(function (e) {
this.setOption = false;
this.currentRegionOption = $(e.target).val();
+
+ if (!this.currentRegionOption) {
+ $(this.options.regionListId).add(this.options.regionInputId).val('');
+ }
}, this));
$(this.options.regionInputId).on('focusout', $.proxy(function () {
diff --git a/app/code/Magento/DirectoryGraphQl/Model/Cache/Tag/Strategy/Config/CountryTagGenerator.php b/app/code/Magento/DirectoryGraphQl/Model/Cache/Tag/Strategy/Config/CountryTagGenerator.php
new file mode 100644
index 0000000000000..16ba9dcab769b
--- /dev/null
+++ b/app/code/Magento/DirectoryGraphQl/Model/Cache/Tag/Strategy/Config/CountryTagGenerator.php
@@ -0,0 +1,65 @@
+storeManager = $storeManager;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function generateTags(ValueInterface $config): array
+ {
+ if (in_array($config->getPath(), $this->countryConfigPaths)) {
+ if ($config->getScope() == ScopeInterface::SCOPE_WEBSITES) {
+ $website = $this->storeManager->getWebsite($config->getScopeId());
+ $storeIds = $website->getStoreIds();
+ } elseif ($config->getScope() == ScopeInterface::SCOPE_STORES) {
+ $storeIds = [$config->getScopeId()];
+ } else {
+ $storeIds = array_keys($this->storeManager->getStores());
+ }
+ $tags = [];
+ foreach ($storeIds as $storeId) {
+ $tags[] = sprintf('%s_%s', Identity::CACHE_TAG, $storeId);
+ }
+ return $tags;
+ }
+ return [];
+ }
+}
diff --git a/app/code/Magento/DirectoryGraphQl/Model/Resolver/Country/Identity.php b/app/code/Magento/DirectoryGraphQl/Model/Resolver/Country/Identity.php
new file mode 100644
index 0000000000000..bc905b57b622e
--- /dev/null
+++ b/app/code/Magento/DirectoryGraphQl/Model/Resolver/Country/Identity.php
@@ -0,0 +1,44 @@
+storeManager = $storeManager;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getIdentities(array $resolvedData): array
+ {
+ if (empty($resolvedData)) {
+ return [];
+ }
+ $storeId = $this->storeManager->getStore()->getId();
+ return [self::CACHE_TAG, sprintf('%s_%s', self::CACHE_TAG, $storeId)];
+ }
+}
diff --git a/app/code/Magento/DirectoryGraphQl/etc/di.xml b/app/code/Magento/DirectoryGraphQl/etc/di.xml
index 3513eeeed2443..19b8495c66b67 100644
--- a/app/code/Magento/DirectoryGraphQl/etc/di.xml
+++ b/app/code/Magento/DirectoryGraphQl/etc/di.xml
@@ -12,6 +12,9 @@
-
Magento\DirectoryGraphQl\Model\Cache\Tag\Strategy\Config\CurrencyTagGenerator
+ -
+ Magento\DirectoryGraphQl\Model\Cache\Tag\Strategy\Config\CountryTagGenerator
+
diff --git a/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls b/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls
index 7b7f1b6eb47ac..b5176b88ee206 100644
--- a/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/DirectoryGraphQl/etc/schema.graphqls
@@ -3,8 +3,8 @@
type Query {
currency: Currency @resolver(class: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Currency") @doc(description: "Return information about the store's currency.") @cache(cacheIdentity: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Currency\\Identity")
- countries: [Country] @resolver(class: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Countries") @doc(description: "The countries query provides information for all countries.") @cache(cacheable: false)
- country (id: String): Country @resolver(class: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Country") @doc(description: "The countries query provides information for a single country.") @cache(cacheable: false)
+ countries: [Country] @resolver(class: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Countries") @doc(description: "The countries query provides information for all countries.") @cache(cacheIdentity: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Country\\Identity")
+ country (id: String): Country @resolver(class: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Country") @doc(description: "The countries query provides information for a single country.") @cache(cacheIdentity: "Magento\\DirectoryGraphQl\\Model\\Resolver\\Country\\Identity")
}
type Currency {
diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndEditDownloadableProductSettingsTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndEditDownloadableProductSettingsTest.xml
index 1760f2228bf05..ce2f7dd577efe 100644
--- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndEditDownloadableProductSettingsTest.xml
+++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateAndEditDownloadableProductSettingsTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml
index bfa0c77280f42..9e3cb81d42584 100644
--- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml
+++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToConfigurableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToConfigurableProductTest.xml
index bfa53f9beb4f8..089878f7e7a0a 100644
--- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToConfigurableProductTest.xml
+++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToConfigurableProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Downloadable/Test/Unit/Ui/DataProvider/Product/Form/Modifier/Data/LinksTest.php b/app/code/Magento/Downloadable/Test/Unit/Ui/DataProvider/Product/Form/Modifier/Data/LinksTest.php
index 8ded865057dc7..10e7ddbb86c22 100644
--- a/app/code/Magento/Downloadable/Test/Unit/Ui/DataProvider/Product/Form/Modifier/Data/LinksTest.php
+++ b/app/code/Magento/Downloadable/Test/Unit/Ui/DataProvider/Product/Form/Modifier/Data/LinksTest.php
@@ -9,11 +9,14 @@
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\Locator\LocatorInterface;
+use Magento\Catalog\Model\Product\Type as ProductType;
+use Magento\Downloadable\Api\Data\LinkInterface;
use Magento\Downloadable\Helper\File as DownloadableFile;
use Magento\Downloadable\Model\Link as LinkModel;
use Magento\Downloadable\Model\Product\Type;
use Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Data\Links;
use Magento\Framework\App\Config\ScopeConfigInterface;
+use Magento\Framework\DataObject;
use Magento\Framework\Escaper;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper;
use Magento\Framework\UrlInterface;
@@ -78,11 +81,14 @@ protected function setUp(): void
{
$this->objectManagerHelper = new ObjectManagerHelper($this);
$this->productMock = $this->getMockBuilder(ProductInterface::class)
- ->setMethods(['getLinksTitle', 'getId', 'getTypeId'])
+ ->onlyMethods(['getId', 'getTypeId'])
+ ->addMethods(['getLinksTitle', 'getTypeInstance', 'getStoreId'])
->getMockForAbstractClass();
$this->locatorMock = $this->getMockForAbstractClass(LocatorInterface::class);
$this->scopeConfigMock = $this->getMockForAbstractClass(ScopeConfigInterface::class);
- $this->escaperMock = $this->createMock(Escaper::class);
+ $this->escaperMock = $this->getMockBuilder(Escaper::class)
+ ->onlyMethods(['escapeHtml'])
+ ->getMockForAbstractClass();
$this->downloadableFileMock = $this->createMock(DownloadableFile::class);
$this->urlBuilderMock = $this->getMockForAbstractClass(UrlInterface::class);
$this->linkModelMock = $this->createMock(LinkModel::class);
@@ -100,6 +106,8 @@ protected function setUp(): void
}
/**
+ * Test case for getLinksTitle
+ *
* @param int|null $id
* @param string $typeId
* @param InvokedCount $expectedGetTitle
@@ -161,4 +169,183 @@ public function getLinksTitleDataProvider()
],
];
}
+
+ /**
+ * Test case for getLinksData
+ *
+ * @param $productTypeMock
+ * @param string $typeId
+ * @param int $storeId
+ * @param array $links
+ * @param array $expectedLinksData
+ * @return void
+ * @dataProvider getLinksDataProvider
+ */
+ public function testGetLinksData(
+ $productTypeMock,
+ string $typeId,
+ int $storeId,
+ array $links,
+ array $expectedLinksData
+ ): void {
+ $this->locatorMock->expects($this->any())
+ ->method('getProduct')
+ ->willReturn($this->productMock);
+ if (!empty($expectedLinksData)) {
+ $this->escaperMock->expects($this->any())
+ ->method('escapeHtml')
+ ->willReturn($expectedLinksData['title']);
+ }
+ $this->productMock->expects($this->any())
+ ->method('getTypeId')
+ ->willReturn($typeId);
+ $this->productMock->expects($this->any())
+ ->method('getTypeInstance')
+ ->willReturn($productTypeMock);
+ $this->productMock->expects($this->any())
+ ->method('getStoreId')
+ ->willReturn($storeId);
+ $productTypeMock->expects($this->any())
+ ->method('getLinks')
+ ->willReturn($links);
+ $getLinksData = $this->links->getLinksData();
+ if (!empty($getLinksData)) {
+ $actualResult = current($getLinksData);
+ } else {
+ $actualResult = $getLinksData;
+ }
+ $this->assertEquals($expectedLinksData, $actualResult);
+ }
+
+ /**
+ * Get Links data provider
+ *
+ * @return array
+ */
+ public function getLinksDataProvider()
+ {
+ $productData1 = [
+ 'link_id' => '1',
+ 'title' => 'test',
+ 'price' => '0.00',
+ 'number_of_downloads' => '0',
+ 'is_shareable' => '1',
+ 'link_url' => 'http://cdn.sourcebooks.com/test',
+ 'type' => 'url',
+ 'sample' =>
+ [
+ 'url' => null,
+ 'type' => null,
+ ],
+ 'sort_order' => '1',
+ 'is_unlimited' => '1',
+ 'use_default_price' => '0',
+ 'use_default_title' => '0',
+
+ ];
+ $productData2 = $productData1;
+ unset($productData2['use_default_price']);
+ unset($productData2['use_default_title']);
+ $productData3 = [
+ 'link_id' => '1',
+ 'title' => 'simple',
+ 'price' => '10.00',
+ 'number_of_downloads' => '0',
+ 'is_shareable' => '0',
+ 'link_url' => '',
+ 'type' => 'simple',
+ 'sample' =>
+ [
+ 'url' => null,
+ 'type' => null,
+ ],
+ 'sort_order' => '1',
+ 'is_unlimited' => '1',
+ 'use_default_price' => '0',
+ 'use_default_title' => '0',
+
+ ];
+ $linkMock1 = $this->getLinkMockObject($productData1, '1', '1');
+ $linkMock2 = $this->getLinkMockObject($productData1, '0', '0');
+ $linkMock3 = $this->getLinkMockObject($productData3, '0', '0');
+ return [
+ 'test case for downloadable product for default store' => [
+ 'type' => $this->createMock(Type::class),
+ 'type_id' => Type::TYPE_DOWNLOADABLE,
+ 'store_id' => 1,
+ 'links' => [$linkMock1],
+ 'expectedLinksData' => $productData1
+ ],
+ 'test case for downloadable product for all store' => [
+ 'type' => $this->createMock(Type::class),
+ 'type_id' => Type::TYPE_DOWNLOADABLE,
+ 'store_id' => 0,
+ 'links' => [$linkMock2],
+ 'expectedLinksData' => $productData2
+ ],
+ 'test case for simple product for default store' => [
+ 'type' => $this->createMock(Type::class),
+ 'type_id' => ProductType::TYPE_SIMPLE,
+ 'store_id' => 1,
+ 'links' => [$linkMock3],
+ 'expectedLinksData' => []
+ ],
+ ];
+ }
+
+ /**
+ * Data provider for getLinks
+ *
+ * @param array $productData
+ * @param string $useDefaultPrice
+ * @param string $useDefaultTitle
+ * @return MockObject
+ */
+ private function getLinkMockObject(
+ array $productData,
+ string $useDefaultPrice,
+ string $useDefaultTitle
+ ): MockObject {
+ $linkMock = $this->getMockBuilder(LinkInterface::class)
+ ->onlyMethods(['getId'])
+ ->addMethods(['getWebsitePrice', 'getStoreTitle'])
+ ->getMockForAbstractClass();
+ $linkMock->expects($this->any())
+ ->method('getId')
+ ->willReturn($productData['link_id']);
+ $linkMock->expects($this->any())
+ ->method('getTitle')
+ ->willReturn($productData['title']);
+ $linkMock->expects($this->any())
+ ->method('getPrice')
+ ->willReturn($productData['price']);
+ $linkMock->expects($this->any())
+ ->method('getNumberOfDownloads')
+ ->willReturn($productData['number_of_downloads']);
+ $linkMock->expects($this->any())
+ ->method('getIsShareable')
+ ->willReturn($productData['is_shareable']);
+ $linkMock->expects($this->any())
+ ->method('getLinkUrl')
+ ->willReturn($productData['link_url']);
+ $linkMock->expects($this->any())
+ ->method('getLinkType')
+ ->willReturn($productData['type']);
+ $linkMock->expects($this->any())
+ ->method('getSampleUrl')
+ ->willReturn($productData['sample']['url']);
+ $linkMock->expects($this->any())
+ ->method('getSampleType')
+ ->willReturn($productData['sample']['type']);
+ $linkMock->expects($this->any())
+ ->method('getSortOrder')
+ ->willReturn($productData['sort_order']);
+ $linkMock->expects($this->any())
+ ->method('getWebsitePrice')
+ ->willReturn($useDefaultPrice);
+ $linkMock->expects($this->any())
+ ->method('getStoreTitle')
+ ->willReturn($useDefaultTitle);
+ return $linkMock;
+ }
}
diff --git a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Data/Links.php b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Data/Links.php
index 3be1094f7a4b7..7c3c30482fd85 100644
--- a/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Data/Links.php
+++ b/app/code/Magento/Downloadable/Ui/DataProvider/Product/Form/Modifier/Data/Links.php
@@ -120,7 +120,7 @@ public function getLinksData()
$linkData = [];
$linkData['link_id'] = $link->getId();
$linkData['title'] = $this->escaper->escapeHtml($link->getTitle());
- $linkData['price'] = $this->getPriceValue($link->getPrice());
+ $linkData['price'] = $this->getPriceValue((float) $link->getPrice());
$linkData['number_of_downloads'] = $link->getNumberOfDownloads();
$linkData['is_shareable'] = $link->getIsShareable();
$linkData['link_url'] = $link->getLinkUrl();
diff --git a/app/code/Magento/Eav/Model/Attribute.php b/app/code/Magento/Eav/Model/Attribute.php
index 40f9a4ae4e934..7699577113211 100644
--- a/app/code/Magento/Eav/Model/Attribute.php
+++ b/app/code/Magento/Eav/Model/Attribute.php
@@ -3,15 +3,8 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
-/**
- * EAV attribute resource model (Using Forms)
- *
- * @method \Magento\Eav\Model\Attribute\Data\AbstractData|null getDataModel()
- * Get data model linked to attribute or null.
- *
- * @author Magento Core Team
- */
namespace Magento\Eav\Model;
use Magento\Store\Model\Website;
@@ -23,14 +16,7 @@
class Attribute extends \Magento\Eav\Model\Entity\Attribute
{
/**
- * Name of the module
- * Override it
- */
- //const MODULE_NAME = 'Magento_Eav';
-
- /**
- * Name of the module
- * Override it
+ * @var string
*/
protected $_eventObject = 'attribute';
@@ -80,7 +66,7 @@ public function afterSave()
}
/**
- * Return forms in which the attribute
+ * Return forms in which the attribute is being used
*
* @return array
*/
@@ -110,6 +96,18 @@ public function getValidateRules()
return [];
}
+ /**
+ * @inheritdoc
+ */
+ public function setData($key, $value = null): Attribute
+ {
+ if ($key === 'used_in_forms') {
+ $this->setOrigData('used_in_forms', $this->getData('used_in_forms') ?? []);
+ }
+ parent::setData($key, $value);
+ return $this;
+ }
+
/**
* Set validate rules
*
@@ -188,7 +186,7 @@ public function getMultilineCount()
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
public function afterDelete()
{
diff --git a/app/code/Magento/Eav/Model/Cache/AttributesFormIdentity.php b/app/code/Magento/Eav/Model/Cache/AttributesFormIdentity.php
new file mode 100644
index 0000000000000..9317426addadc
--- /dev/null
+++ b/app/code/Magento/Eav/Model/Cache/AttributesFormIdentity.php
@@ -0,0 +1,50 @@
+getAttributeId()
+ );
+ }
+ }
+ return $identities;
+ }
+}
diff --git a/app/code/Magento/Eav/Model/Config.php b/app/code/Magento/Eav/Model/Config.php
index a7e49b126f350..ab4cb121fd166 100644
--- a/app/code/Magento/Eav/Model/Config.php
+++ b/app/code/Magento/Eav/Model/Config.php
@@ -13,6 +13,7 @@
use Magento\Framework\App\ObjectManager;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Model\AbstractModel;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Framework\Serialize\SerializerInterface;
/**
@@ -24,7 +25,7 @@
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
* @since 100.0.2
*/
-class Config
+class Config implements ResetAfterRequestInterface
{
/**#@+
* EAV cache ids
@@ -217,7 +218,7 @@ public function clear()
$this->_cache->clean(
[
\Magento\Eav\Model\Cache\Type::CACHE_TAG,
- \Magento\Eav\Model\Entity\Attribute::CACHE_TAG,
+ \Magento\Eav\Model\Entity\Attribute::CACHE_TAG
]
);
return $this;
@@ -402,7 +403,7 @@ protected function _initEntityTypes()
$this->_entityTypeData[$typeCode] = $typeData;
}
- if ($this->isCacheEnabled()) {
+ if ($this->isCacheEnabled() && !empty($this->_entityTypeData)) {
$this->_cache->save(
$this->serializer->serialize($this->_entityTypeData),
self::ENTITIES_CACHE_ID,
@@ -981,4 +982,20 @@ private function getWebsiteId(Collection $attributeCollection): int
{
return $attributeCollection->getWebsite() ? (int)$attributeCollection->getWebsite()->getId() : 0;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->attributesPerSet = [];
+ $this->_attributeData = [];
+ foreach ($this->attributes ?? [] as $attributesGroupedByEntityTypeCode) {
+ foreach ($attributesGroupedByEntityTypeCode as $attribute) {
+ if ($attribute instanceof ResetAfterRequestInterface) {
+ $attribute->_resetState();
+ }
+ }
+ }
+ }
}
diff --git a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php
index f66ade9435813..23c74e5b8e80b 100644
--- a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php
+++ b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php
@@ -22,6 +22,7 @@
use Magento\Framework\Model\ResourceModel\AbstractResource;
use Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor;
use Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Entity/Attribute/Model - entity abstract
@@ -34,7 +35,10 @@
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @since 100.0.2
*/
-abstract class AbstractEntity extends AbstractResource implements EntityInterface, DefaultAttributesProvider
+abstract class AbstractEntity extends AbstractResource implements
+ EntityInterface,
+ DefaultAttributesProvider,
+ ResetAfterRequestInterface
{
/**
* @var \Magento\Eav\Model\Entity\AttributeLoaderInterface
@@ -2021,4 +2025,18 @@ protected function loadAttributesForObject($attributes, $object = null)
}
}
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->_attributesByCode = [];
+ $this->attributesByScope = [];
+ $this->_attributesByTable = [];
+ $this->_staticAttributes = [];
+ $this->_attributeValuesToDelete = [];
+ $this->_attributeValuesToSave = [];
+ self::$_attributeBackendTables = [];
+ }
}
diff --git a/app/code/Magento/Eav/Model/Entity/Attribute.php b/app/code/Magento/Eav/Model/Entity/Attribute.php
index e1094a331149e..ecdb2f55f4f46 100644
--- a/app/code/Magento/Eav/Model/Entity/Attribute.php
+++ b/app/code/Magento/Eav/Model/Entity/Attribute.php
@@ -1,8 +1,10 @@
getId()];
+ $identities = [self::CACHE_TAG . '_' . $this->getId()];
+
+ if (($this->hasDataChanges() || $this->isDeleted())) {
+ $identities[] = sprintf(
+ "%s_%s_ENTITY",
+ Config::ENTITIES_CACHE_ID,
+ strtoupper($this->getEntityType()->getEntityTypeCode())
+ );
+
+ $usedBeforeChange = $this->getOrigData('used_in_forms') ?? [];
+ $usedInForms = $this->getUsedInForms() ?? [];
+
+ if (is_array($usedBeforeChange) && is_array($usedInForms) && ($usedBeforeChange != $usedInForms)) {
+ $formsToInvalidate = array_merge(
+ array_diff($usedBeforeChange, $usedInForms),
+ array_diff($usedInForms, $usedBeforeChange)
+ );
+
+ foreach ($formsToInvalidate as $form) {
+ $identities[] = sprintf(
+ "%s_%s_FORM",
+ AttributesFormIdentity::CACHE_TAG,
+ $form
+ );
+ };
+ }
+ }
+
+ return $identities;
}
/**
diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php
index af621e17f4249..5628787f7debb 100644
--- a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php
+++ b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php
@@ -9,6 +9,7 @@
use Magento\Framework\Api\AttributeValueFactory;
use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Framework\Serialize\Serializer\Json;
/**
@@ -23,14 +24,15 @@
*/
abstract class AbstractAttribute extends \Magento\Framework\Model\AbstractExtensibleModel implements
AttributeInterface,
- \Magento\Eav\Api\Data\AttributeInterface
+ \Magento\Eav\Api\Data\AttributeInterface,
+ ResetAfterRequestInterface
{
- const TYPE_STATIC = 'static';
+ public const TYPE_STATIC = 'static';
/**
* Const for empty string value.
*/
- const EMPTY_STRING = '';
+ public const EMPTY_STRING = '';
/**
* Attribute name
@@ -68,8 +70,6 @@ abstract class AbstractAttribute extends \Magento\Framework\Model\AbstractExtens
protected $_source;
/**
- * Attribute id cache
- *
* @var array
*/
protected $_attributeIdCache = [];
@@ -219,8 +219,6 @@ public function __construct(
/**
* Get Serializer instance.
*
- * @deprecated 101.0.0
- *
* @return Json
* @since 101.0.0
*/
@@ -229,7 +227,6 @@ protected function getSerializer()
if ($this->serializer === null) {
$this->serializer = \Magento\Framework\App\ObjectManager::getInstance()->create(Json::class);
}
-
return $this->serializer;
}
@@ -930,6 +927,7 @@ public function _getFlatColumnsDdlDefinition()
* Used in database compatible mode
*
* @deprecated 101.0.0
+ * @see MMDB
* @return array
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
@@ -1444,4 +1442,22 @@ public function __wakeup()
$this->dataObjectProcessor = $objectManager->get(\Magento\Framework\Reflection\DataObjectProcessor::class);
$this->dataObjectHelper = $objectManager->get(\Magento\Framework\Api\DataObjectHelper::class);
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->unsetData('store_label'); // store specific
+ $this->unsetData(self::OPTIONS); // store specific
+ if ($this->_source instanceof ResetAfterRequestInterface) {
+ $this->_source->_resetState();
+ }
+ if ($this->_backend instanceof ResetAfterRequestInterface) {
+ $this->_backend->_resetState();
+ }
+ if ($this->_frontend instanceof ResetAfterRequestInterface) {
+ $this->_frontend->_resetState();
+ }
+ }
}
diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php b/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php
index 6f6dc0a47f5ae..2c9b6d68b0bbf 100644
--- a/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php
+++ b/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php
@@ -139,6 +139,7 @@ private function saveOption(
$options = [];
$options['value'][$optionId][0] = $optionLabel;
$options['order'][$optionId] = $option->getSortOrder();
+ $options['is_default'][$optionId] = $option->getIsDefault();
if (is_array($option->getStoreLabels())) {
foreach ($option->getStoreLabels() as $label) {
$options['value'][$optionId][$label->getStoreId()] = $label->getLabel();
diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php b/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php
index bdd2899b47b67..63c823e25affc 100644
--- a/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php
+++ b/app/code/Magento/Eav/Model/Entity/Attribute/Source/Table.php
@@ -6,6 +6,7 @@
namespace Magento\Eav\Model\Entity\Attribute\Source;
use Magento\Framework\App\ObjectManager;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Store\Model\StoreManagerInterface;
/**
@@ -14,7 +15,7 @@
* @api
* @since 100.0.2
*/
-class Table extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource
+class Table extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource implements ResetAfterRequestInterface
{
/**
* Default values for option cache
@@ -97,6 +98,7 @@ public function getAllOptions($withEmpty = true, $defaultValues = false)
*
* @return StoreManagerInterface
* @deprecated 100.1.6
+ * @see we don't recommend this approach anymore
*/
private function getStoreManager()
{
@@ -293,4 +295,13 @@ public function getFlatUpdateSelect($store)
{
return $this->_attrOptionFactory->create()->getFlatUpdateSelect($this->getAttribute(), $store);
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->_optionsDefault = [];
+ $this->_options = [];
+ }
}
diff --git a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php
index abd817940956f..1bc2ca7f63dfa 100644
--- a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php
+++ b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php
@@ -19,6 +19,7 @@
* @SuppressWarnings(PHPMD.TooManyFields)
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * @SuppressWarnings(PHPMD.ExcessivePublicCount)
* @since 100.0.2
*/
abstract class AbstractCollection extends AbstractDb implements SourceProviderInterface
@@ -180,6 +181,23 @@ protected function _construct()
{
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ parent::_resetState();
+ $this->_itemsById = [];
+ $this->_staticFields = [];
+ $this->_entity = null;
+ $this->_selectEntityTypes = [];
+ $this->_selectAttributes = [];
+ $this->_filterAttributes = [];
+ $this->_joinEntities = [];
+ $this->_joinAttributes = [];
+ $this->_joinFields = [];
+ }
+
/**
* Retrieve table name
*
@@ -1597,14 +1615,12 @@ protected function _afterLoad()
protected function _reset()
{
parent::_reset();
-
$this->_selectEntityTypes = [];
$this->_selectAttributes = [];
$this->_filterAttributes = [];
$this->_joinEntities = [];
$this->_joinAttributes = [];
$this->_joinFields = [];
-
return $this;
}
diff --git a/app/code/Magento/Eav/Model/Entity/VersionControl/Metadata.php b/app/code/Magento/Eav/Model/Entity/VersionControl/Metadata.php
index b994d793ed04c..c25c7f5ec90ed 100644
--- a/app/code/Magento/Eav/Model/Entity/VersionControl/Metadata.php
+++ b/app/code/Magento/Eav/Model/Entity/VersionControl/Metadata.php
@@ -5,10 +5,13 @@
*/
namespace Magento\Eav\Model\Entity\VersionControl;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
+use Magento\Framework\Model\ResourceModel\Db\VersionControl\Metadata as ResourceModelMetaData;
+
/**
* Class Metadata represents a list of entity fields that are applicable for persistence operations
*/
-class Metadata extends \Magento\Framework\Model\ResourceModel\Db\VersionControl\Metadata
+class Metadata extends ResourceModelMetaData implements ResetAfterRequestInterface
{
/**
* Returns list of entity fields that are applicable for persistence operations
@@ -36,4 +39,12 @@ public function getFields(\Magento\Framework\DataObject $entity)
return $this->metadataInfo[$entityClass];
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->metadataInfo = [];
+ }
}
diff --git a/app/code/Magento/Eav/Model/ResourceModel/Attribute/Collection.php b/app/code/Magento/Eav/Model/ResourceModel/Attribute/Collection.php
index 7abb54e780f5f..897640f852cc7 100644
--- a/app/code/Magento/Eav/Model/ResourceModel/Attribute/Collection.php
+++ b/app/code/Magento/Eav/Model/ResourceModel/Attribute/Collection.php
@@ -10,6 +10,7 @@
/**
* EAV additional attribute resource collection (Using Forms)
*
+ * phpcs:disable Magento2.Classes.AbstractApi.AbstractApi
* @api
* @since 100.0.2
*/
@@ -18,7 +19,7 @@ abstract class Collection extends \Magento\Eav\Model\ResourceModel\Entity\Attrib
/**
* code of password hash in customer's EAV tables
*/
- const EAV_CODE_PASSWORD_HASH = 'password_hash';
+ public const EAV_CODE_PASSWORD_HASH = 'password_hash';
/**
* Current website scope instance
@@ -64,6 +65,15 @@ public function __construct(
parent::__construct($entityFactory, $logger, $fetchStrategy, $eventManager, $eavConfig, $connection, $resource);
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void //phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedFunction
+ {
+ /* Note: because Eav attribute loading takes significant performance,
+ we are not resetting it like other collections. */
+ }
+
/**
* Default attribute entity type code
*
@@ -212,6 +222,7 @@ protected function _initSelect()
/**
* Specify attribute entity type filter.
+ *
* Entity type is defined.
*
* @param int $type
diff --git a/app/code/Magento/Eav/Model/ResourceModel/AttributePersistor.php b/app/code/Magento/Eav/Model/ResourceModel/AttributePersistor.php
index 9c6adc0354f8d..1711d87c26487 100644
--- a/app/code/Magento/Eav/Model/ResourceModel/AttributePersistor.php
+++ b/app/code/Magento/Eav/Model/ResourceModel/AttributePersistor.php
@@ -6,18 +6,15 @@
namespace Magento\Eav\Model\ResourceModel;
-use Magento\Catalog\Model\Product;
use Magento\Eav\Api\AttributeRepositoryInterface;
-use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Eav\Model\Entity\Attribute\AbstractAttribute;
use Magento\Framework\EntityManager\EntityMetadataInterface;
-use Magento\Store\Model\StoreManagerInterface;
+use Magento\Framework\EntityManager\MetadataPool;
use Magento\Framework\Locale\FormatInterface;
use Magento\Framework\Model\Entity\ScopeInterface;
-use Magento\Framework\EntityManager\MetadataPool;
/**
- * Class AttributePersistor
+ * Class AttributePersistor persists attributes
*/
class AttributePersistor
{
@@ -67,6 +64,8 @@ public function __construct(
}
/**
+ * Registers delete
+ *
* @param string $entityType
* @param int $link
* @param string $attributeCode
@@ -78,6 +77,8 @@ public function registerDelete($entityType, $link, $attributeCode)
}
/**
+ * Registers update
+ *
* @param string $entityType
* @param int $link
* @param string $attributeCode
@@ -90,6 +91,8 @@ public function registerUpdate($entityType, $link, $attributeCode, $value)
}
/**
+ * Registers Insert
+ *
* @param string $entityType
* @param int $link
* @param string $attributeCode
@@ -102,6 +105,8 @@ public function registerInsert($entityType, $link, $attributeCode, $value)
}
/**
+ * Process deletes
+ *
* @param string $entityType
* @param \Magento\Framework\Model\Entity\ScopeInterface[] $context
* @return void
@@ -132,6 +137,8 @@ public function processDeletes($entityType, $context)
}
/**
+ * Process inserts
+ *
* @param string $entityType
* @param \Magento\Framework\Model\Entity\ScopeInterface[] $context
* @return void
@@ -194,6 +201,8 @@ private function prepareInsertDataForMultipleSave($entityType, $context)
}
/**
+ * Process updates
+ *
* @param string $entityType
* @param \Magento\Framework\Model\Entity\ScopeInterface[] $context
* @return void
@@ -329,10 +338,14 @@ public function flush($entityType, $context)
$this->processDeletes($entityType, $context);
$this->processInserts($entityType, $context);
$this->processUpdates($entityType, $context);
- unset($this->delete, $this->insert, $this->update);
+ $this->delete = [];
+ $this->insert = [];
+ $this->update = [];
}
/**
+ * Prepares value
+ *
* @param string $entityType
* @param string $value
* @param AbstractAttribute $attribute
@@ -355,6 +368,8 @@ protected function prepareValue($entityType, $value, AbstractAttribute $attribut
}
/**
+ * Gets scope value
+ *
* @param ScopeInterface $scope
* @param AbstractAttribute $attribute
* @param bool $useDefault
diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php
index 3e894c5f76a16..b11e88b4e1217 100644
--- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php
+++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php
@@ -17,6 +17,7 @@
use Magento\Framework\DB\Select;
use Magento\Framework\Exception\CouldNotDeleteException;
use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\MessageQueue\PoisonPill\PoisonPillPutInterface;
use Magento\Framework\Model\AbstractModel;
use Magento\Framework\Model\ResourceModel\Db\AbstractDb;
use Magento\Framework\Model\ResourceModel\Db\Context;
@@ -53,6 +54,11 @@ class Attribute extends AbstractDb
*/
private $config;
+ /**
+ * @var PoisonPillPutInterface
+ */
+ private $pillPut;
+
/**
* Class constructor
*
@@ -60,16 +66,20 @@ class Attribute extends AbstractDb
* @param StoreManagerInterface $storeManager
* @param Type $eavEntityType
* @param string $connectionName
+ * @param PoisonPillPutInterface|null $pillPut
* @codeCoverageIgnore
*/
public function __construct(
Context $context,
StoreManagerInterface $storeManager,
Type $eavEntityType,
- $connectionName = null
+ $connectionName = null,
+ PoisonPillPutInterface $pillPut = null
) {
$this->_storeManager = $storeManager;
$this->_eavEntityType = $eavEntityType;
+ $this->pillPut = $pillPut ?: \Magento\Framework\App\ObjectManager::getInstance()
+ ->get(PoisonPillPutInterface::class);
parent::__construct($context, $connectionName);
}
@@ -235,6 +245,7 @@ protected function _afterSave(AbstractModel $object)
$object
);
$this->getConfig()->clear();
+ $this->pillPut->put();
return parent::_afterSave($object);
}
@@ -249,6 +260,7 @@ protected function _afterSave(AbstractModel $object)
protected function _afterDelete(AbstractModel $object)
{
$this->getConfig()->clear();
+ $this->pillPut->put();
return $this;
}
@@ -256,7 +268,6 @@ protected function _afterDelete(AbstractModel $object)
* Returns config instance
*
* @return Config
- * @deprecated 100.0.7
*/
private function getConfig()
{
@@ -388,6 +399,10 @@ protected function _saveOption(AbstractModel $object)
$defaultValue = $this->_processAttributeOptions($object, $option);
}
+ if ($object->getDefaultValue()) {
+ $defaultValue[] = $object->getDefaultValue();
+ }
+
$this->_saveDefaultValue($object, $defaultValue);
return $this;
}
diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/OptionValueProvider.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/OptionValueProvider.php
index 153735f988376..554896a704094 100644
--- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/OptionValueProvider.php
+++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/OptionValueProvider.php
@@ -32,14 +32,14 @@ public function __construct(ResourceConnection $connection)
/**
* Get EAV attribute option value by option id
*
- * @param int $valueId
+ * @param int $optionId
* @return string|null
*/
- public function get(int $valueId): ?string
+ public function get(int $optionId): ?string
{
$select = $this->connection->select()
->from($this->connection->getTableName('eav_attribute_option_value'), 'value')
- ->where('value_id = ?', $valueId);
+ ->where('option_id = ?', $optionId);
$result = $this->connection->fetchOne($select);
diff --git a/app/code/Magento/Eav/Model/ResourceModel/Form/Attribute/Collection.php b/app/code/Magento/Eav/Model/ResourceModel/Form/Attribute/Collection.php
index 9438178e56085..063b6041782d7 100644
--- a/app/code/Magento/Eav/Model/ResourceModel/Form/Attribute/Collection.php
+++ b/app/code/Magento/Eav/Model/ResourceModel/Form/Attribute/Collection.php
@@ -96,6 +96,16 @@ protected function _construct()
}
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ parent::_resetState();
+ $this->_store = null;
+ $this->_entityType = null;
+ }
+
/**
* Get EAV website table
*
@@ -193,6 +203,7 @@ public function setSortOrder($direction = self::SORT_ORDER_ASC)
*/
protected function _beforeLoad()
{
+ $store = $this->getStore();
$select = $this->getSelect();
$connection = $this->getConnection();
$entityType = $this->getEntityType();
@@ -254,7 +265,6 @@ protected function _beforeLoad()
}
}
- $store = $this->getStore();
$joinWebsiteExpression = $connection->quoteInto(
'sa.attribute_id = main_table.attribute_id AND sa.website_id = ?',
(int)$store->getWebsiteId()
diff --git a/app/code/Magento/Eav/Model/Validator/Attribute/Data.php b/app/code/Magento/Eav/Model/Validator/Attribute/Data.php
index 7b29b9dde6790..4175608db5f08 100644
--- a/app/code/Magento/Eav/Model/Validator/Attribute/Data.php
+++ b/app/code/Magento/Eav/Model/Validator/Attribute/Data.php
@@ -8,6 +8,7 @@
use Magento\Eav\Model\Attribute;
use Magento\Eav\Model\AttributeDataFactory;
+use Magento\Eav\Model\Config;
use Magento\Framework\DataObject;
/**
@@ -47,18 +48,39 @@ class Data extends \Magento\Framework\Validator\AbstractValidator
*/
private $ignoredAttributesByTypesList;
+ /**
+ * @var \Magento\Eav\Model\Config
+ */
+ private $eavConfig;
+
/**
* @param AttributeDataFactory $attrDataFactory
+ * @param Config|null $eavConfig
* @param array $ignoredAttributesByTypesList
*/
public function __construct(
AttributeDataFactory $attrDataFactory,
+ Config $eavConfig = null,
array $ignoredAttributesByTypesList = []
) {
+ $this->eavConfig = $eavConfig ?: \Magento\Framework\App\ObjectManager::getInstance()
+ ->get(Config::class);
$this->_attrDataFactory = $attrDataFactory;
$this->ignoredAttributesByTypesList = $ignoredAttributesByTypesList;
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ parent::_resetState();
+ $this->_attributes = [];
+ $this->allowedAttributesList = [];
+ $this->deniedAttributesList = [];
+ $this->_data = [];
+ }
+
/**
* Set list of attributes for validation in isValid method.
*
@@ -166,8 +188,9 @@ protected function _getAttributes($entity)
} elseif ($entity instanceof \Magento\Framework\Model\AbstractModel &&
$entity->getResource() instanceof \Magento\Eav\Model\Entity\AbstractEntity
) { // $entity is EAV-model
+ $type = $entity->getEntityType()->getEntityTypeCode();
/** @var \Magento\Eav\Model\Entity\Type $entityType */
- $entityType = $entity->getEntityType();
+ $entityType = $this->eavConfig->getEntityType($type);
$attributes = $entityType->getAttributeCollection()->getItems();
$ignoredTypeAttributes = $this->ignoredAttributesByTypesList[$entityType->getEntityTypeCode()] ?? [];
diff --git a/app/code/Magento/Eav/README.md b/app/code/Magento/Eav/README.md
index 6710044ac6c89..f669e534a973a 100644
--- a/app/code/Magento/Eav/README.md
+++ b/app/code/Magento/Eav/README.md
@@ -1,2 +1,2 @@
Magento\EAV stands for Entity-Attribute-Value. The purpose of Magento\Eav module is to make entities
-configurable/extendable by admin user.
\ No newline at end of file
+configurable/extendable by admin user.
diff --git a/app/code/Magento/Eav/Test/Fixture/Attribute.php b/app/code/Magento/Eav/Test/Fixture/Attribute.php
new file mode 100644
index 0000000000000..cc7f66594d5c2
--- /dev/null
+++ b/app/code/Magento/Eav/Test/Fixture/Attribute.php
@@ -0,0 +1,110 @@
+ null,
+ 'attribute_id' => null,
+ 'attribute_code' => 'attribute%uniqid%',
+ 'default_frontend_label' => 'Attribute%uniqid%',
+ 'frontend_labels' => [],
+ 'frontend_input' => 'text',
+ 'backend_type' => 'varchar',
+ 'is_required' => false,
+ 'is_user_defined' => true,
+ 'note' => null,
+ 'backend_model' => null,
+ 'source_model' => null,
+ 'default_value' => null,
+ 'is_unique' => '0',
+ 'frontend_class' => null
+ ];
+
+ /**
+ * @var ServiceFactory
+ */
+ private ServiceFactory $serviceFactory;
+
+ /**
+ * @var DataMerger
+ */
+ private DataMerger $dataMerger;
+
+ /**
+ * @var ProcessorInterface
+ */
+ private ProcessorInterface $processor;
+
+ /**
+ * @var AttributeRepositoryInterface
+ */
+ private AttributeRepositoryInterface $attributeRepository;
+
+ /**
+ * @param ServiceFactory $serviceFactory
+ * @param DataMerger $dataMerger
+ * @param ProcessorInterface $processor
+ * @param AttributeRepositoryInterface $attributeRepository
+ */
+ public function __construct(
+ ServiceFactory $serviceFactory,
+ DataMerger $dataMerger,
+ ProcessorInterface $processor,
+ AttributeRepositoryInterface $attributeRepository
+ ) {
+ $this->serviceFactory = $serviceFactory;
+ $this->dataMerger = $dataMerger;
+ $this->processor = $processor;
+ $this->attributeRepository = $attributeRepository;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function apply(array $data = []): ?DataObject
+ {
+ if (empty($data['entity_type_id'])) {
+ throw new InvalidArgumentException(
+ __(
+ '"%field" value is required to create an attribute',
+ [
+ 'field' => 'entity_type_id'
+ ]
+ )
+ );
+ }
+
+ $mergedData = $this->processor->process($this, $this->dataMerger->merge(self::DEFAULT_DATA, $data));
+
+ $this->serviceFactory->create(AttributeRepositoryInterface::class, 'save')->execute(
+ [
+ 'attribute' => $mergedData
+ ]
+ );
+
+ return $this->attributeRepository->get($mergedData['entity_type_id'], $mergedData['attribute_code']);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function revert(DataObject $data): void
+ {
+ $this->attributeRepository->deleteById($data['attribute_id']);
+ }
+}
diff --git a/app/code/Magento/Eav/Test/Fixture/AttributeOption.php b/app/code/Magento/Eav/Test/Fixture/AttributeOption.php
new file mode 100644
index 0000000000000..4d25bdb03ca87
--- /dev/null
+++ b/app/code/Magento/Eav/Test/Fixture/AttributeOption.php
@@ -0,0 +1,150 @@
+ null,
+ 'attribute_code' => null,
+ 'label' => 'Option Label %uniqid%',
+ 'sort_order' => null,
+ 'store_labels' => '',
+ 'is_default' => false
+ ];
+
+ /**
+ * @var ServiceFactory
+ */
+ private ServiceFactory $serviceFactory;
+
+ /**
+ * @var DataMerger
+ */
+ private DataMerger $dataMerger;
+
+ /**
+ * @var ProcessorInterface
+ */
+ private ProcessorInterface $processor;
+
+ /**
+ * @var AttributeRepositoryInterface
+ */
+ private AttributeRepositoryInterface $attributeRepository;
+
+ /**
+ * @param ServiceFactory $serviceFactory
+ * @param DataMerger $dataMerger
+ * @param ProcessorInterface $processor
+ * @param AttributeRepositoryInterface $attributeRepository
+ */
+ public function __construct(
+ ServiceFactory $serviceFactory,
+ DataMerger $dataMerger,
+ ProcessorInterface $processor,
+ AttributeRepositoryInterface $attributeRepository
+ ) {
+ $this->serviceFactory = $serviceFactory;
+ $this->dataMerger = $dataMerger;
+ $this->processor = $processor;
+ $this->attributeRepository = $attributeRepository;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function apply(array $data = []): ?DataObject
+ {
+ if (empty($data['entity_type'])) {
+ throw new InvalidArgumentException(
+ __(
+ '"%field" value is required to create an attribute option',
+ [
+ 'field' => 'entity_type_id'
+ ]
+ )
+ );
+ }
+
+ if (empty($data['attribute_code'])) {
+ throw new InvalidArgumentException(
+ __(
+ '"%field" value is required to create an attribute option',
+ [
+ 'field' => 'attribute_code'
+ ]
+ )
+ );
+ }
+
+ $mergedData = array_filter(
+ $this->processor->process($this, $this->dataMerger->merge(self::DEFAULT_DATA, $data)),
+ function ($value) {
+ return $value !== null;
+ }
+ );
+
+ $entityType = $mergedData['entity_type'];
+ $attributeCode = $mergedData['attribute_code'];
+ unset($mergedData['entity_type'], $mergedData['attribute_code']);
+
+ $this->serviceFactory->create(AttributeOptionManagementInterface::class, 'add')->execute(
+ [
+ 'entityType' => $entityType,
+ 'attributeCode' => $attributeCode,
+ 'option' => $mergedData
+ ]
+ );
+
+ $attribute = $this->attributeRepository->get($entityType, $attributeCode);
+
+ foreach ($attribute->getOptions() as $option) {
+ if ($this->getDefaultLabel($mergedData) === $option->getLabel()) {
+ if (isset($mergedData['is_default']) && $mergedData['is_default']) {
+ $option->setIsDefault(true);
+ }
+ return $option;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Retrieve default label or label for default store
+ *
+ * @param array $mergedData
+ * @return string
+ */
+ private function getDefaultLabel(array $mergedData): string
+ {
+ $defaultLabel = $mergedData['label'];
+ if (!isset($mergedData['store_labels']) || !is_array($mergedData['store_labels'])) {
+ return $defaultLabel;
+ }
+
+ foreach ($mergedData['store_labels'] as $label) {
+ if (isset($label['store_id']) && $label['store_id'] === 0 && isset($label['label'])) {
+ return $label['label'];
+ }
+ }
+
+ return $defaultLabel;
+ }
+}
diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php
index 7b554a19fc281..278bcfdaad440 100644
--- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php
+++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php
@@ -80,6 +80,9 @@ public function testAdd(string $label): void
],
'order' => [
'id_new_option' => $sortOder,
+ ],
+ 'is_default' => [
+ 'id_new_option' => true,
]
];
$newOptionId = 10;
@@ -196,6 +199,9 @@ public function testAddWithCannotSaveException()
],
'order' => [
'id_new_option' => $sortOder,
+ ],
+ 'is_default' => [
+ 'id_new_option' => true,
]
];
@@ -253,6 +259,9 @@ public function testUpdate(string $label): void
],
'order' => [
$optionId => $sortOder,
+ ],
+ 'is_default' => [
+ $optionId => true,
]
];
diff --git a/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php b/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php
index 88daf1a8a6f52..e1aab7f44b48a 100644
--- a/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php
+++ b/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php
@@ -13,6 +13,7 @@
use Magento\Eav\Model\AttributeDataFactory;
use Magento\Eav\Model\Entity\AbstractEntity;
use Magento\Eav\Model\Validator\Attribute\Data;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\DataObject;
use Magento\Framework\Model\AbstractModel;
use Magento\Framework\ObjectManagerInterface;
@@ -35,6 +36,11 @@ class DataTest extends TestCase
*/
private $model;
+ /**
+ * @var \Magento\Eav\Model\Config|MockObject
+ */
+ private $eavConfigMock;
+
/**
* @inheritdoc
*/
@@ -49,7 +55,12 @@ protected function setUp(): void
]
)
->getMock();
-
+ $this->createMock(ObjectManagerInterface::class);
+ ObjectManager::setInstance($this->createMock(ObjectManagerInterface::class));
+ $this->eavConfigMock = $this->getMockBuilder(\Magento\Eav\Model\Config::class)
+ ->onlyMethods(['getEntityType'])
+ ->disableOriginalConstructor()
+ ->getMock();
$this->model = new Data($this->attrDataFactory);
}
@@ -205,13 +216,17 @@ public function testIsValidAttributesFromCollection(): void
'is_visible' => true,
]
);
+ $entityTypeCode = 'entity_type_code';
$collection = $this->getMockBuilder(DataObject::class)
->addMethods(['getItems'])->getMock();
$collection->expects($this->once())->method('getItems')->willReturn([$attribute]);
$entityType = $this->getMockBuilder(DataObject::class)
- ->addMethods(['getAttributeCollection'])
+ ->addMethods(['getAttributeCollection','getEntityTypeCode'])
->getMock();
+ $entityType->expects($this->atMost(2))->method('getEntityTypeCode')->willReturn($entityTypeCode);
$entityType->expects($this->once())->method('getAttributeCollection')->willReturn($collection);
+ $this->eavConfigMock->expects($this->once())->method('getEntityType')
+ ->with($entityTypeCode)->willReturn($entityType);
$entity = $this->_getEntityMock();
$entity->expects($this->once())->method('getResource')->willReturn($resource);
$entity->expects($this->once())->method('getEntityType')->willReturn($entityType);
@@ -235,7 +250,7 @@ public function testIsValidAttributesFromCollection(): void
)->willReturn(
$dataModel
);
- $validator = new Data($attrDataFactory);
+ $validator = new Data($attrDataFactory, $this->eavConfigMock);
$validator->setData(['attribute' => 'new_test_data']);
$this->assertTrue($validator->isValid($entity));
diff --git a/app/code/Magento/EavGraphQl/Model/GetAttributeSelectedOptionComposite.php b/app/code/Magento/EavGraphQl/Model/GetAttributeSelectedOptionComposite.php
new file mode 100644
index 0000000000000..5c918a2354800
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Model/GetAttributeSelectedOptionComposite.php
@@ -0,0 +1,55 @@
+providers = $providers;
+ }
+
+ /**
+ * Returns right GetAttributeSelectedOptionInterface to use for attribute with $attributeCode
+ *
+ * @param string $entityType
+ * @param array $customAttribute
+ * @return array|null
+ * @throws RuntimeException
+ */
+ public function execute(string $entityType, array $customAttribute): ?array
+ {
+ if (!isset($this->providers[$entityType])) {
+ throw new RuntimeException(
+ __(sprintf('"%s" entity type not set in providers', $entityType))
+ );
+ }
+ if (!$this->providers[$entityType] instanceof GetAttributeSelectedOptionInterface) {
+ throw new RuntimeException(
+ __('Configured attribute selected option data providers should implement
+ GetAttributeSelectedOptionInterface')
+ );
+ }
+
+ return $this->providers[$entityType]->execute($entityType, $customAttribute);
+ }
+}
diff --git a/app/code/Magento/EavGraphQl/Model/GetAttributeSelectedOptionInterface.php b/app/code/Magento/EavGraphQl/Model/GetAttributeSelectedOptionInterface.php
new file mode 100644
index 0000000000000..96d5bd6b547b1
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Model/GetAttributeSelectedOptionInterface.php
@@ -0,0 +1,26 @@
+providers = $providers;
+ }
+
+ /**
+ * Returns right GetAttributeValueInterface to use for attribute with $attributeCode
+ *
+ * @param string $entityType
+ * @param array $customAttribute
+ * @return array|null
+ * @throws RuntimeException|LocalizedException
+ */
+ public function execute(string $entityType, array $customAttribute): ?array
+ {
+ if (!isset($this->providers[$entityType])) {
+ throw new RuntimeException(
+ __(sprintf('"%s" entity type not set in providers', $entityType))
+ );
+ }
+ if (!$this->providers[$entityType] instanceof GetAttributeValueInterface) {
+ throw new RuntimeException(
+ __('Configured attribute data providers should implement GetAttributeValueInterface')
+ );
+ }
+
+ return $this->providers[$entityType]->execute($entityType, $customAttribute);
+ }
+}
diff --git a/app/code/Magento/EavGraphQl/Model/GetAttributeValueInterface.php b/app/code/Magento/EavGraphQl/Model/GetAttributeValueInterface.php
new file mode 100644
index 0000000000000..4f9b6f63f3153
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Model/GetAttributeValueInterface.php
@@ -0,0 +1,26 @@
+providers = $providers;
+ }
+
+ /**
+ * Returns right GetAttributesFormInterface to use for form with $formCode
+ *
+ * @param string $formCode
+ * @return array
+ * @throws RuntimeException
+ */
+ public function execute(string $formCode): ?array
+ {
+ foreach ($this->providers as $provider) {
+ if (!$provider instanceof GetAttributesFormInterface) {
+ throw new RuntimeException(
+ __('Configured attribute data providers should implement GetAttributesFormInterface')
+ );
+ }
+
+ try {
+ return $provider->execute($formCode);
+ } catch (LocalizedException $e) {
+ continue;
+ }
+ }
+ return null;
+ }
+}
diff --git a/app/code/Magento/EavGraphQl/Model/GetAttributesFormInterface.php b/app/code/Magento/EavGraphQl/Model/GetAttributesFormInterface.php
new file mode 100644
index 0000000000000..c85054386d153
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Model/GetAttributesFormInterface.php
@@ -0,0 +1,24 @@
+attributeRepository = $attributeRepository;
+ $this->searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory;
+ $this->getAttributeData = $getAttributeData;
+ }
+
+ /**
+ * Get attribute metadata details
+ *
+ * @param array $attributesInputs
+ * @param int $storeId
+ * @return array
+ * @throws RuntimeException
+ */
+ public function execute(array $attributesInputs, int $storeId): array
+ {
+ if (empty($attributesInputs)) {
+ return [];
+ }
+
+ $codes = [];
+ $errors = [];
+
+ foreach ($attributesInputs as $attributeInput) {
+ $codes[$attributeInput['entity_type']][] = $attributeInput['attribute_code'];
+ }
+
+ $items = [];
+
+ foreach ($codes as $entityType => $attributeCodes) {
+ $builder = $this->searchCriteriaBuilderFactory->create();
+ $builder
+ ->addFilter('attribute_code', $attributeCodes, 'in');
+ try {
+ $attributes = $this->attributeRepository->getList($entityType, $builder->create())->getItems();
+ } catch (LocalizedException $exception) {
+ $errors[] = [
+ 'type' => 'ENTITY_NOT_FOUND',
+ 'message' => (string) __('Entity "%entity" could not be found.', ['entity' => $entityType])
+ ];
+ continue;
+ }
+
+ $notFoundCodes = array_diff($attributeCodes, $this->getCodes($attributes));
+ foreach ($notFoundCodes as $notFoundCode) {
+ $errors[] = [
+ 'type' => 'ATTRIBUTE_NOT_FOUND',
+ 'message' => (string) __('Attribute code "%code" could not be found.', ['code' => $notFoundCode])
+ ];
+ }
+ foreach ($attributes as $attribute) {
+ if (method_exists($attribute, 'getIsVisible') && !$attribute->getIsVisible()) {
+ continue;
+ }
+ $items[] = $this->getAttributeData->execute($attribute, $entityType, $storeId);
+ }
+ }
+
+ return [
+ 'items' => $items,
+ 'errors' => $errors
+ ];
+ }
+
+ /**
+ * Retrieve an array of codes from the array of attributes
+ *
+ * @param AttributeInterface[] $attributes
+ * @return AttributeInterface[]
+ */
+ private function getCodes(array $attributes): array
+ {
+ return array_map(
+ function (AttributeInterface $attribute) {
+ return $attribute->getAttributeCode();
+ },
+ $attributes
+ );
+ }
+}
diff --git a/app/code/Magento/EavGraphQl/Model/Output/GetAttributeData.php b/app/code/Magento/EavGraphQl/Model/Output/GetAttributeData.php
new file mode 100644
index 0000000000000..407c77573d6c0
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Model/Output/GetAttributeData.php
@@ -0,0 +1,119 @@
+enumLookup = $enumLookup;
+ }
+
+ /**
+ * Retrieve formatted attribute data
+ *
+ * @param AttributeInterface $attribute
+ * @param string $entityType
+ * @param int $storeId
+ * @return array
+ * @throws RuntimeException
+ */
+ public function execute(
+ AttributeInterface $attribute,
+ string $entityType,
+ int $storeId
+ ): array {
+ return [
+ 'id' => $attribute->getAttributeId(),
+ 'code' => $attribute->getAttributeCode(),
+ 'label' => $attribute->getStoreLabel($storeId),
+ 'sort_order' => $attribute->getPosition(),
+ 'entity_type' => $this->enumLookup->getEnumValueFromField(
+ 'AttributeEntityTypeEnum',
+ $entityType
+ ),
+ 'frontend_input' => $this->getFrontendInput($attribute),
+ 'frontend_class' => $attribute->getFrontendClass(),
+ 'is_required' => $attribute->getIsRequired(),
+ 'default_value' => $attribute->getDefaultValue(),
+ 'is_unique' => $attribute->getIsUnique(),
+ 'options' => $this->getOptions($attribute),
+ 'attribute' => $attribute
+ ];
+ }
+
+ /**
+ * Returns default frontend input for attribute if not set
+ *
+ * @param AttributeInterface $attribute
+ * @return string
+ */
+ private function getFrontendInput(AttributeInterface $attribute): string
+ {
+ if ($attribute->getFrontendInput() === null) {
+ return "UNDEFINED";
+ }
+ return $this->enumLookup->getEnumValueFromField(
+ 'AttributeFrontendInputEnum',
+ $attribute->getFrontendInput()
+ );
+ }
+
+ /**
+ * Retrieve formatted attribute options
+ *
+ * @param AttributeInterface $attribute
+ * @return array
+ */
+ private function getOptions(AttributeInterface $attribute): array
+ {
+ if (!$attribute->getOptions()) {
+ return [];
+ }
+ return array_filter(
+ array_map(
+ function (AttributeOptionInterface $option) use ($attribute) {
+ if (is_array($option->getValue())) {
+ $value = (empty($option->getValue()) ? '' : (string)$option->getValue()[0]['value']);
+ } else {
+ $value = (string)$option->getValue();
+ }
+ $label = (string)$option->getLabel();
+ if (empty(trim($value)) && empty(trim($label))) {
+ return null;
+ }
+ return [
+ 'label' => $label,
+ 'value' => $value,
+ 'is_default' => $attribute->getDefaultValue() ?
+ in_array($value, explode(',', $attribute->getDefaultValue())) : null
+ ];
+ },
+ $attribute->getOptions()
+ )
+ );
+ }
+}
diff --git a/app/code/Magento/EavGraphQl/Model/Output/GetAttributeDataComposite.php b/app/code/Magento/EavGraphQl/Model/Output/GetAttributeDataComposite.php
new file mode 100644
index 0000000000000..30fdc0dba5ab3
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Model/Output/GetAttributeDataComposite.php
@@ -0,0 +1,58 @@
+providers = $providers;
+ }
+
+ /**
+ * Retrieve formatted attribute data
+ *
+ * @param AttributeInterface $attribute
+ * @param string $entityType
+ * @param int $storeId
+ * @return array
+ * @throws RuntimeException
+ */
+ public function execute(
+ AttributeInterface $attribute,
+ string $entityType,
+ int $storeId
+ ): array {
+ $data = [];
+
+ foreach ($this->providers as $provider) {
+ if (!$provider instanceof GetAttributeDataInterface) {
+ throw new RuntimeException(
+ __('Configured attribute data providers should implement GetAttributeDataInterface')
+ );
+ }
+ $data[] = $provider->execute($attribute, $entityType, $storeId);
+ }
+
+ return array_merge([], ...$data);
+ }
+}
diff --git a/app/code/Magento/EavGraphQl/Model/Output/GetAttributeDataInterface.php b/app/code/Magento/EavGraphQl/Model/Output/GetAttributeDataInterface.php
new file mode 100644
index 0000000000000..2a586c824efae
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Model/Output/GetAttributeDataInterface.php
@@ -0,0 +1,28 @@
+providers = $providers;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function execute(string $entity, string $code, string $value): ?array
+ {
+ foreach ($this->providers as $provider) {
+ if (!$provider instanceof GetAttributeValueInterface) {
+ throw new RuntimeException(
+ __('Configured attribute data providers should implement GetAttributeValueInterface')
+ );
+ }
+
+ try {
+ return $provider->execute($entity, $code, $value);
+ } catch (LocalizedException $e) {
+ continue;
+ }
+ }
+ return null;
+ }
+}
diff --git a/app/code/Magento/EavGraphQl/Model/Output/Value/GetAttributeValueInterface.php b/app/code/Magento/EavGraphQl/Model/Output/Value/GetAttributeValueInterface.php
new file mode 100644
index 0000000000000..65a4124fb6ef0
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Model/Output/Value/GetAttributeValueInterface.php
@@ -0,0 +1,24 @@
+attributeRepository = $attributeRepository;
+ $this->frontendInputs = $frontendInputs;
+ $this->getAttributeSelectedOption = $getAttributeSelectedOption;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function execute(string $entity, string $code, string $value): ?array
+ {
+ $attr = $this->attributeRepository->get($entity, $code);
+
+ $result = [
+ 'entity_type' => $entity,
+ 'code' => $code,
+ 'sort_order' => $attr->getSortOrder() ?? ''
+ ];
+
+ if (in_array($attr->getFrontendInput(), $this->frontendInputs)) {
+ $result['selected_options'] = $this->getAttributeSelectedOption->execute($entity, $code, $value);
+ } else {
+ $result['value'] = $value;
+ }
+ return $result;
+ }
+}
diff --git a/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetAttributeSelectedOptionComposite.php b/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetAttributeSelectedOptionComposite.php
new file mode 100644
index 0000000000000..fb7cc4134aa88
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetAttributeSelectedOptionComposite.php
@@ -0,0 +1,52 @@
+providers = $providers;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function execute(string $entity, string $code, string $value): ?array
+ {
+ foreach ($this->providers as $provider) {
+ if (!$provider instanceof GetAttributeSelectedOptionInterface) {
+ throw new RuntimeException(
+ __('Configured attribute selected option data providers should implement
+ GetAttributeSelectedOptionInterface')
+ );
+ }
+
+ try {
+ return $provider->execute($entity, $code, $value);
+ } catch (LocalizedException $e) {
+ continue;
+ }
+ }
+ return null;
+ }
+}
diff --git a/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetAttributeSelectedOptionInterface.php b/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetAttributeSelectedOptionInterface.php
new file mode 100644
index 0000000000000..b4230ea60c958
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Model/Output/Value/Options/GetAttributeSelectedOptionInterface.php
@@ -0,0 +1,24 @@
+attributeRepository = $attributeRepository;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function execute(string $entity, string $code, string $value): ?array
+ {
+ $attribute = $this->attributeRepository->get($entity, $code);
+
+ $result = [];
+ $selectedValues = explode(',', $value);
+ foreach ($attribute->getOptions() as $option) {
+ if (!in_array($option->getValue(), $selectedValues)) {
+ continue;
+ }
+ $result[] = [
+ 'value' => $option->getValue(),
+ 'label' => $option->getLabel()
+ ];
+ }
+ return $result;
+ }
+}
diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/AttributesForm.php b/app/code/Magento/EavGraphQl/Model/Resolver/AttributesForm.php
new file mode 100644
index 0000000000000..257c7c00da588
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Model/Resolver/AttributesForm.php
@@ -0,0 +1,95 @@
+getAttributesFormComposite = $providerFormComposite;
+ $this->getAttributesMetadata = $getAttributesMetadata;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+
+ if (empty($args['formCode'])) {
+ throw new GraphQlInputException(__('Required parameter "%1" of type string.', 'formCode'));
+ }
+
+ $formCode = $args['formCode'];
+
+ $attributes = $this->getAttributesFormComposite->execute($formCode);
+ if ($this->isAnAdminForm($formCode) || $attributes === null) {
+ return [
+ 'items' => [],
+ 'errors' => [
+ [
+ 'type' => 'ENTITY_NOT_FOUND',
+ 'message' => (string) __('Form "%form" could not be found.', ['form' => $formCode])
+ ]
+ ]
+ ];
+ }
+
+ return array_merge(
+ [
+ 'formCode' => $formCode
+ ],
+ $this->getAttributesMetadata->execute(
+ $attributes,
+ (int)$context->getExtensionAttributes()->getStore()->getId()
+ )
+ );
+ }
+
+ /**
+ * Check if passed form formCode is an admin form.
+ *
+ * @param string $formCode
+ * @return bool
+ */
+ private function isAnAdminForm(string $formCode): bool
+ {
+ return str_starts_with($formCode, 'adminhtml_');
+ }
+}
diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/AttributesList.php b/app/code/Magento/EavGraphQl/Model/Resolver/AttributesList.php
new file mode 100644
index 0000000000000..91c00a7ad69cc
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Model/Resolver/AttributesList.php
@@ -0,0 +1,101 @@
+enumLookup = $enumLookup;
+ $this->getAttributeData = $getAttributeData;
+ $this->getFilteredAttributes = $getFilteredAttributes;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ): array {
+ if (!$args['entityType']) {
+ throw new GraphQlInputException(__('Required parameter "%1" of type string.', 'entityType'));
+ }
+
+ $storeId = (int) $context->getExtensionAttributes()->getStore()->getId();
+ $entityType = $this->enumLookup->getEnumValueFromField(
+ 'AttributeEntityTypeEnum',
+ strtolower($args['entityType'])
+ );
+
+ $filterArgs = $args['filters'] ?? [];
+
+ $attributesList = $this->getFilteredAttributes->execute($filterArgs, strtolower($entityType));
+
+ return [
+ 'items' => $this->getAttributesMetadata($attributesList['items'], $entityType, $storeId),
+ 'entity_type' => $entityType,
+ 'errors' => $attributesList['errors']
+ ];
+ }
+
+ /**
+ * Returns formatted list of attributes
+ *
+ * @param AttributeInterface[] $attributesList
+ * @param string $entityType
+ * @param int $storeId
+ *
+ * @return array[]
+ * @throws RuntimeException
+ */
+ private function getAttributesMetadata(array $attributesList, string $entityType, int $storeId): array
+ {
+ return array_map(function (AttributeInterface $attribute) use ($entityType, $storeId): array {
+ return $this->getAttributeData->execute($attribute, strtolower($entityType), $storeId);
+ }, $attributesList);
+ }
+}
diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/AttributesMetadata.php b/app/code/Magento/EavGraphQl/Model/Resolver/AttributesMetadata.php
new file mode 100644
index 0000000000000..f4f9853c1b6ab
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Model/Resolver/AttributesMetadata.php
@@ -0,0 +1,69 @@
+getAttributesMetadata = $getAttributesMetadata;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ $attributeInputs = $args['attributes'];
+
+ if (empty($attributeInputs)) {
+ throw new GraphQlInputException(
+ __(
+ 'Required parameters "attribute_code" and "entity_type" of type String.'
+ )
+ );
+ }
+
+ foreach ($attributeInputs as $attributeInput) {
+ if (!isset($attributeInput['attribute_code'])) {
+ throw new GraphQlInputException(__('The attribute_code is required to retrieve the metadata'));
+ }
+ if (!isset($attributeInput['entity_type'])) {
+ throw new GraphQlInputException(__('The entity_type is required to retrieve the metadata'));
+ }
+ }
+
+ return $this->getAttributesMetadata->execute(
+ $attributeInputs,
+ (int) $context->getExtensionAttributes()->getStore()->getId()
+ );
+ }
+}
diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/Cache/AttributesListIdentity.php b/app/code/Magento/EavGraphQl/Model/Resolver/Cache/AttributesListIdentity.php
new file mode 100644
index 0000000000000..d02542e54d336
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Model/Resolver/Cache/AttributesListIdentity.php
@@ -0,0 +1,48 @@
+getAttributeId()
+ );
+ }
+ }
+ return $identities;
+ }
+}
diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/Cache/CustomAttributeMetadataIdentity.php b/app/code/Magento/EavGraphQl/Model/Resolver/Cache/CustomAttributeMetadataIdentity.php
new file mode 100644
index 0000000000000..25df607cfbde2
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Model/Resolver/Cache/CustomAttributeMetadataIdentity.php
@@ -0,0 +1,40 @@
+ 'NO',
1 => 'FILTERABLE_WITH_RESULTS',
diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/EntityFieldChecker.php b/app/code/Magento/EavGraphQl/Model/Resolver/EntityFieldChecker.php
new file mode 100644
index 0000000000000..0d852b401eac2
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Model/Resolver/EntityFieldChecker.php
@@ -0,0 +1,55 @@
+resource = $resource;
+ $this->eavEntityType = $eavEntityType;
+ }
+
+ /**
+ * Check if the field exists on the entity
+ *
+ * @param string $entityTypeCode
+ * @param string $field
+ * @return bool
+ */
+ public function fieldBelongToEntity(string $entityTypeCode, string $field): bool
+ {
+ $connection = $this->resource->getConnection();
+ $columns = $connection->describeTable(
+ $this->eavEntityType->loadByCode($entityTypeCode)->getAdditionalAttributeTable()
+ );
+
+ return array_key_exists($field, $columns);
+ }
+}
diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/GetFilteredAttributes.php b/app/code/Magento/EavGraphQl/Model/Resolver/GetFilteredAttributes.php
new file mode 100644
index 0000000000000..3b402a6e0dfb5
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Model/Resolver/GetFilteredAttributes.php
@@ -0,0 +1,87 @@
+attributeRepository = $attributeRepository;
+ $this->searchCriteriaBuilder = $searchCriteriaBuilder;
+ $this->entityFieldChecker = $entityFieldChecker;
+ }
+
+ /**
+ * Return the attributes filtered and errors if the filter could not be applied
+ *
+ * @param array $filterArgs
+ * @param string $entityType
+ * @return array
+ * @throws InputException
+ */
+ public function execute(array $filterArgs, string $entityType): array
+ {
+ $errors = [];
+ foreach ($filterArgs as $field => $value) {
+ if ($this->entityFieldChecker->fieldBelongToEntity(strtolower($entityType), $field)) {
+ $this->searchCriteriaBuilder->addFilter($field, $value);
+ } else {
+ $errors[] = [
+ 'type' => 'FILTER_NOT_FOUND',
+ 'message' =>
+ (string)__(
+ 'Cannot filter by "%filter" as that field does not belong to "%entity".',
+ ['filter' => $field, 'entity' => $entityType]
+ )
+ ];
+ }
+ }
+
+ $searchCriteria = $this->searchCriteriaBuilder
+ ->addFilter('is_visible', true)
+ ->addFilter('backend_type', 'static', 'neq')
+ ->create();
+
+ $attributesList = $this->attributeRepository->getList(strtolower($entityType), $searchCriteria)->getItems();
+
+ return [
+ 'items' => $attributesList,
+ 'errors' => $errors
+ ];
+ }
+}
diff --git a/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeMetadata.php b/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeMetadata.php
new file mode 100644
index 0000000000000..c4fd8403bd670
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeMetadata.php
@@ -0,0 +1,42 @@
+entityTypes = $entityTypes;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolveType(array $data): string
+ {
+ if (!isset($data['entity_type'])) {
+ return self::TYPE;
+ }
+ return $this->entityTypes[$data['entity_type']] ?? self::TYPE;
+ }
+}
diff --git a/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeOption.php b/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeOption.php
new file mode 100644
index 0000000000000..0390f3aaf99ab
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeOption.php
@@ -0,0 +1,39 @@
+typeResolvers = $typeResolvers;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolveType(array $data): string
+ {
+ return self::TYPE;
+ }
+}
diff --git a/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeSelectedOption.php b/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeSelectedOption.php
new file mode 100644
index 0000000000000..5a436b911ed09
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Model/TypeResolver/AttributeSelectedOption.php
@@ -0,0 +1,28 @@
+attributeRepository = $attributeRepository;
+ $this->frontendInputs = $frontendInputs;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolveType(array $data): string
+ {
+ /** @var Attribute $attr */
+ $attr = $this->attributeRepository->get(
+ $data['entity_type'],
+ $data['code'],
+ );
+
+ if (in_array($attr->getFrontendInput(), $this->frontendInputs)) {
+ return 'AttributeSelectedOptions';
+ }
+
+ return self::TYPE;
+ }
+}
diff --git a/app/code/Magento/EavGraphQl/Plugin/Eav/AttributePlugin.php b/app/code/Magento/EavGraphQl/Plugin/Eav/AttributePlugin.php
new file mode 100644
index 0000000000000..dd713f4f69acd
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/Plugin/Eav/AttributePlugin.php
@@ -0,0 +1,41 @@
+getEntityType()->getEntityTypeCode(),
+ $subject->getOrigData(AttributeInterface::ATTRIBUTE_CODE)
+ ?? $subject->getData(AttributeInterface::ATTRIBUTE_CODE)
+ )
+ ]
+ );
+ }
+}
diff --git a/app/code/Magento/EavGraphQl/README.md b/app/code/Magento/EavGraphQl/README.md
index 6bf418c798dec..be4879ac18ceb 100644
--- a/app/code/Magento/EavGraphQl/README.md
+++ b/app/code/Magento/EavGraphQl/README.md
@@ -10,6 +10,6 @@ For information about enabling or disabling a module in Magento 2, see [Enable o
You can get more information at articles:
-- [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql).
+- [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/).
- [customAttributeMetadata query](https://developer.adobe.com/commerce/webapi/graphql/schema/store/queries/custom-attribute-metadata/).
- [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html)
diff --git a/app/code/Magento/EavGraphQl/etc/di.xml b/app/code/Magento/EavGraphQl/etc/di.xml
new file mode 100644
index 0000000000000..30c4e72258512
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/etc/di.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/app/code/Magento/EavGraphQl/etc/graphql/di.xml b/app/code/Magento/EavGraphQl/etc/graphql/di.xml
new file mode 100644
index 0000000000000..93726a50a9c0b
--- /dev/null
+++ b/app/code/Magento/EavGraphQl/etc/graphql/di.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+ - Magento\EavGraphQl\Model\Output\GetAttributeData
+
+
+
+
+
+
+ -
+
- boolean
+ - date
+ - datetime
+ - file
+ - gallery
+ - hidden
+ - image
+ - media_image
+ - multiline
+ - multiselect
+ - price
+ - select
+ - text
+ - textarea
+ - weight
+
+
+
+
+
+
+
+ - Magento\EavGraphQl\Model\Output\Value\GetCustomAttributes
+
+
+
+
+
+
+ - multiselect
+ - select
+
+
+
+
+
+
+ - Magento\EavGraphQl\Model\Output\Value\Options\GetCustomSelectedOptionAttributes
+
+
+
+
+
+
+ - multiselect
+ - select
+
+
+
+
diff --git a/app/code/Magento/EavGraphQl/etc/schema.graphqls b/app/code/Magento/EavGraphQl/etc/schema.graphqls
index 25f53c4ad7ea8..3323a63622f44 100644
--- a/app/code/Magento/EavGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/EavGraphQl/etc/schema.graphqls
@@ -2,7 +2,24 @@
# See COPYING.txt for license details.
type Query {
- customAttributeMetadata(attributes: [AttributeInput!]! @doc(description: "An input object that specifies the attribute code and entity type to search.")): CustomAttributeMetadata @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\CustomAttributeMetadata") @doc(description: "Return the attribute type, given an attribute code and entity type.") @cache(cacheable: false)
+ customAttributeMetadata(attributes: [AttributeInput!]! @doc(description: "An input object that specifies the attribute code and entity type to search.")):
+ CustomAttributeMetadata
+ @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\CustomAttributeMetadata")
+ @doc(description: "Return the attribute type, given an attribute code and entity type.")
+ @cache(cacheIdentity: "Magento\\EavGraphQl\\Model\\Resolver\\Cache\\CustomAttributeMetadataIdentity")
+ @deprecated(reason: "Use `customAttributeMetadataV2` query instead.")
+ customAttributeMetadataV2(attributes: [AttributeInput!]): AttributesMetadataOutput! @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\AttributesMetadata") @doc(description: "Retrieve EAV attributes metadata.") @cache(cacheIdentity: "Magento\\EavGraphQl\\Model\\Resolver\\Cache\\CustomAttributeMetadataV2Identity")
+ attributesForm(formCode: String! @doc(description: "Form code.")): AttributesFormOutput!
+ @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\AttributesForm")
+ @doc(description: "Retrieve EAV attributes associated to a frontend form.")
+ @cache(cacheIdentity: "Magento\\Eav\\Model\\Cache\\AttributesFormIdentity")
+ attributesList(
+ entityType: AttributeEntityTypeEnum! @doc(description: "Entity type.")
+ filters: AttributeFilterInput @doc(description: "Identifies which filter inputs to search for and return.")
+ ): AttributesMetadataOutput
+ @resolver(class: "Magento\\EavGraphQl\\Model\\Resolver\\AttributesList")
+ @doc(description: "Returns a list of attributes metadata for a given entity type.")
+ @cache(cacheIdentity: "Magento\\EavGraphQl\\Model\\Resolver\\Cache\\AttributesListIdentity")
}
type CustomAttributeMetadata @doc(description: "Defines an array of custom attributes.") {
@@ -41,3 +58,115 @@ input AttributeInput @doc(description: "Defines the attribute characteristics to
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.")
}
+
+type AttributesMetadataOutput @doc(description: "Metadata of EAV attributes.") {
+ items: [CustomAttributeMetadataInterface!]! @doc(description: "Requested attributes metadata.")
+ errors: [AttributeMetadataError!]! @doc(description: "Errors of retrieving certain attributes metadata.")
+}
+
+type AttributeMetadataError @doc(description: "Attribute metadata retrieval error.") {
+ type: AttributeMetadataErrorType! @doc(description: "Attribute metadata retrieval error type.")
+ message: String! @doc(description: "Attribute metadata retrieval error message.")
+}
+
+enum AttributeMetadataErrorType @doc(description: "Attribute metadata retrieval error types.") {
+ ENTITY_NOT_FOUND @doc(description: "The requested entity was not found.")
+ ATTRIBUTE_NOT_FOUND @doc(description: "The requested attribute was not found.")
+ FILTER_NOT_FOUND @doc(description: "The filter cannot be applied as it does not belong to the entity")
+ UNDEFINED @doc(description: "Not categorized error, see the error message.")
+}
+
+interface CustomAttributeMetadataInterface @typeResolver(class: "Magento\\EavGraphQl\\Model\\TypeResolver\\AttributeMetadata") @doc(description: "An interface containing fields that define the EAV attribute."){
+ code: ID! @doc(description: "The unique identifier for an attribute code. This value should be in lowercase letters without spaces.")
+ label: String @doc(description: "The label assigned to the attribute.")
+ entity_type: AttributeEntityTypeEnum! @doc(description: "The type of entity that defines the attribute.")
+ frontend_input: AttributeFrontendInputEnum @doc(description: "The frontend input type of the attribute.")
+ frontend_class: String @doc(description: "The frontend class of the attribute.")
+ is_required: Boolean! @doc(description: "Whether the attribute value is required.")
+ default_value: String @doc(description: "Default attribute value.")
+ is_unique: Boolean! @doc(description: "Whether the attribute value must be unique.")
+ options: [CustomAttributeOptionInterface!]! @doc(description: "Attribute options.")
+}
+
+interface CustomAttributeOptionInterface @typeResolver(class: "Magento\\EavGraphQl\\Model\\TypeResolver\\AttributeOption") {
+ label: String! @doc(description: "The label assigned to the attribute option.")
+ value: String! @doc(description: "The attribute option value.")
+ is_default: Boolean @doc(description: "Is the option value default.")
+}
+
+type AttributeOptionMetadata implements CustomAttributeOptionInterface @doc(description: "Base EAV implementation of CustomAttributeOptionInterface.") {
+}
+
+type AttributeMetadata implements CustomAttributeMetadataInterface @doc(description: "Base EAV implementation of CustomAttributeMetadataInterface.") {
+}
+
+enum AttributeEntityTypeEnum @doc(description: "List of all entity types. Populated by the modules introducing EAV entities.") {
+}
+
+enum AttributeFrontendInputEnum @doc(description: "EAV attribute frontend input types.") {
+ BOOLEAN
+ DATE
+ DATETIME
+ FILE
+ GALLERY
+ HIDDEN
+ IMAGE
+ MEDIA_IMAGE
+ MULTILINE
+ MULTISELECT
+ PRICE
+ SELECT
+ TEXT
+ TEXTAREA
+ WEIGHT
+ UNDEFINED
+}
+
+type AttributesFormOutput @doc(description: "Metadata of EAV attributes associated to form") {
+ items: [CustomAttributeMetadataInterface!]! @doc(description: "Requested attributes metadata.")
+ errors: [AttributeMetadataError!]! @doc(description: "Errors of retrieving certain attributes metadata.")
+}
+
+interface AttributeValueInterface @typeResolver(class: "Magento\\EavGraphQl\\Model\\TypeResolver\\AttributeValue") {
+ code: ID! @doc(description: "The attribute code.")
+}
+
+type AttributeValue implements AttributeValueInterface {
+ value: String! @doc(description: "The attribute value.")
+}
+
+type AttributeSelectedOptions implements AttributeValueInterface {
+ selected_options: [AttributeSelectedOptionInterface!]!
+}
+
+interface AttributeSelectedOptionInterface @typeResolver(class: "Magento\\EavGraphQl\\Model\\TypeResolver\\AttributeSelectedOption") {
+ label: String! @doc(description: "The attribute selected option label.")
+ value: String! @doc(description: "The attribute selected option value.")
+}
+
+type AttributeSelectedOption implements AttributeSelectedOptionInterface {
+}
+
+input AttributeValueInput @doc(description: "Specifies the value for attribute.") {
+ attribute_code: String! @doc(description: "The code of the attribute.")
+ value: String @doc(description: "The value assigned to the attribute.")
+ selected_options: [AttributeInputSelectedOption!] @doc(description: "An array containing selected options for a select or multiselect attribute.")
+}
+
+input AttributeInputSelectedOption @doc(description: "Specifies selected option for a select or multiselect attribute value.") {
+ value: String! @doc(description: "The attribute option value.")
+}
+
+input AttributeFilterInput @doc(description: "An input object that specifies the filters used for attributes.") {
+ is_comparable: Boolean @doc(description: "Whether a product or category attribute can be compared against another or not.")
+ is_filterable: Boolean @doc(description: "Whether a product or category attribute can be filtered or not.")
+ is_filterable_in_search: Boolean @doc(description: "Whether a product or category attribute can be filtered in search or not.")
+ is_html_allowed_on_front: Boolean @doc(description: "Whether a product or category attribute can use HTML on front or not.")
+ is_searchable: Boolean @doc(description: "Whether a product or category attribute can be searched or not.")
+ is_used_for_price_rules: Boolean @doc(description: "Whether a product or category attribute can be used for price rules or not.")
+ is_used_for_promo_rules: Boolean @doc(description: "Whether a product or category attribute is used for promo rules or not.")
+ is_visible_in_advanced_search: Boolean @doc(description: "Whether a product or category attribute is visible in advanced search or not.")
+ is_visible_on_front: Boolean @doc(description: "Whether a product or category attribute is visible on front or not.")
+ is_wysiwyg_enabled: Boolean @doc(description: "Whether a product or category attribute has WYSIWYG enabled or not.")
+ used_in_product_listing: Boolean @doc(description: "Whether a product or category attribute is used in product listing or not.")
+}
diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php
index a9fb67f209aa7..c1772086d7ba3 100644
--- a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php
+++ b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php
@@ -250,6 +250,7 @@ private function convertAttribute(Attribute $attribute, array $attributeValues,
* - "Visible in Advanced Search" (is_visible_in_advanced_search)
* - "Use in Layered Navigation" (is_filterable)
* - "Use in Search Results Layered Navigation" (is_filterable_in_search)
+ * - "Use in Sorting in Product Listing" (used_for_sort_by)
*
* @param Attribute $attribute
* @return bool
@@ -261,6 +262,7 @@ private function isAttributeLabelsShouldBeMapped(Attribute $attribute): bool
|| $attribute->getIsVisibleInAdvancedSearch()
|| $attribute->getIsFilterable()
|| $attribute->getIsFilterableInSearch()
+ || $attribute->getUsedForSortBy()
);
}
diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php
index 75636991e7ee6..e97a46eed7d39 100644
--- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php
+++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php
@@ -10,42 +10,43 @@
use Magento\Eav\Model\Config;
use Magento\Catalog\Api\Data\ProductAttributeInterface;
use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter\DummyAttribute;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Framework\ObjectManagerInterface;
use Psr\Log\LoggerInterface;
/**
* Provide attribute adapter.
*/
-class AttributeProvider
+class AttributeProvider implements ResetAfterRequestInterface
{
/**
* Object Manager instance
*
* @var ObjectManagerInterface
*/
- private $objectManager;
+ private ObjectManagerInterface $objectManager;
/**
* Instance name to create
*
* @var string
*/
- private $instanceName;
+ private string $instanceName;
/**
* @var Config
*/
- private $eavConfig;
+ private Config $eavConfig;
/**
* @var array
*/
- private $cachedPool = [];
+ private array $cachedPool = [];
/**
* @var LoggerInterface
*/
- private $logger;
+ private LoggerInterface $logger;
/**
* Factory constructor
@@ -101,4 +102,12 @@ public function removeAttributeCacheByCode(string $attributeCode): void
unset($this->cachedPool[$attributeCode]);
}
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->cachedPool = [];
+ }
}
diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php
index 1db0bad36e243..87712641c05d4 100644
--- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php
+++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicField.php
@@ -18,6 +18,7 @@
use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProviderInterface;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Catalog\Model\ResourceModel\Category\Collection;
+use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory;
use Magento\Framework\App\ObjectManager;
use Magento\Store\Model\StoreManagerInterface;
@@ -29,9 +30,9 @@ class DynamicField implements FieldProviderInterface
/**
* Category collection.
*
- * @var Collection
+ * @var CollectionFactory
*/
- private $categoryCollection;
+ private $categoryCollectionFactory;
/**
* Customer group repository.
@@ -41,8 +42,6 @@ class DynamicField implements FieldProviderInterface
private $groupRepository;
/**
- * Search criteria builder.
- *
* @var SearchCriteriaBuilder
*/
private $searchCriteriaBuilder;
@@ -79,8 +78,10 @@ class DynamicField implements FieldProviderInterface
* @param SearchCriteriaBuilder $searchCriteriaBuilder
* @param FieldNameResolver $fieldNameResolver
* @param AttributeProvider $attributeAdapterProvider
- * @param Collection $categoryCollection
+ * @param Collection $categoryCollection @deprecated @see $categoryCollectionFactory
* @param StoreManagerInterface|null $storeManager
+ * @param CollectionFactory|null $categoryCollectionFactory
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __construct(
FieldTypeConverterInterface $fieldTypeConverter,
@@ -90,7 +91,8 @@ public function __construct(
FieldNameResolver $fieldNameResolver,
AttributeProvider $attributeAdapterProvider,
Collection $categoryCollection,
- ?StoreManagerInterface $storeManager = null
+ ?StoreManagerInterface $storeManager = null,
+ ?CollectionFactory $categoryCollectionFactory = null
) {
$this->groupRepository = $groupRepository;
$this->searchCriteriaBuilder = $searchCriteriaBuilder;
@@ -98,7 +100,8 @@ public function __construct(
$this->indexTypeConverter = $indexTypeConverter;
$this->fieldNameResolver = $fieldNameResolver;
$this->attributeAdapterProvider = $attributeAdapterProvider;
- $this->categoryCollection = $categoryCollection;
+ $this->categoryCollectionFactory = $categoryCollectionFactory
+ ?: ObjectManager::getInstance()->get(CollectionFactory::class);
$this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class);
}
@@ -108,7 +111,7 @@ public function __construct(
public function getFields(array $context = []): array
{
$allAttributes = [];
- $categoryIds = $this->categoryCollection->getAllIds();
+ $categoryIds = $this->categoryCollectionFactory->create()->getAllIds();
$positionAttribute = $this->attributeAdapterProvider->getByAttributeCode('position');
$categoryNameAttribute = $this->attributeAdapterProvider->getByAttributeCode('category_name');
foreach ($categoryIds as $categoryId) {
diff --git a/app/code/Magento/Elasticsearch/README.md b/app/code/Magento/Elasticsearch/README.md
index 835cd4ab37f19..8a58ddfde39a7 100644
--- a/app/code/Magento/Elasticsearch/README.md
+++ b/app/code/Magento/Elasticsearch/README.md
@@ -1,7 +1,7 @@
-#Magento_Elasticsearch module
+# Magento_Elasticsearch module
-Magento_Elasticsearch module allows using the Elasticsearch engine for the product searching capabilities. This module
-provides logic used by other modules implementing newer versions of Elasticsearch, this module by itself only adds
+Magento_Elasticsearch module allows using the Elasticsearch engine for the product searching capabilities. This module
+provides logic used by other modules implementing newer versions of Elasticsearch, this module by itself only adds
support for Elasticsearch v5.
The module implements Magento_Search library interfaces.
diff --git a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StoreFrontSearchWithProductAttributeOptionValue.xml b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StoreFrontSearchWithProductAttributeOptionValue.xml
index 3c3bac70f4dc2..8451ff03a1344 100644
--- a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StoreFrontSearchWithProductAttributeOptionValue.xml
+++ b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StoreFrontSearchWithProductAttributeOptionValue.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php
index 9f1b59b1bfc81..354ad01f14a64 100644
--- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php
+++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php
@@ -373,6 +373,57 @@ public static function mapProvider(): array
[10 => '44', 11 => '45'],
['color' => [44, 45], 'color_value' => ['red', 'black']],
],
+ 'select with options with sort by and filterable' => [
+ 10,
+ [
+ 'attribute_code' => 'color',
+ 'backend_type' => 'text',
+ 'frontend_input' => 'select',
+ 'is_searchable' => true,
+ 'used_for_sort_by' => true,
+ 'is_filterable_in_grid' => true,
+ 'options' => [
+ ['value' => '44', 'label' => 'red'],
+ ['value' => '45', 'label' => 'black'],
+ ],
+ ],
+ [10 => '44', 11 => '45'],
+ ['color' => [44, 45], 'color_value' => ['red', 'black']],
+ ],
+ 'unsearchable select with options with sort by and filterable' => [
+ 10,
+ [
+ 'attribute_code' => 'color',
+ 'backend_type' => 'text',
+ 'frontend_input' => 'select',
+ 'is_searchable' => false,
+ 'used_for_sort_by' => false,
+ 'is_filterable_in_grid' => false,
+ 'options' => [
+ ['value' => '44', 'label' => 'red'],
+ ['value' => '45', 'label' => 'black'],
+ ],
+ ],
+ '44',
+ ['color' => 44],
+ ],
+ 'select with options with sort by only' => [
+ 10,
+ [
+ 'attribute_code' => 'color',
+ 'backend_type' => 'text',
+ 'frontend_input' => 'select',
+ 'is_searchable' => false,
+ 'used_for_sort_by' => true,
+ 'is_filterable_in_grid' => false,
+ 'options' => [
+ ['value' => '44', 'label' => 'red'],
+ ['value' => '45', 'label' => 'black'],
+ ],
+ ],
+ [10 => '44', 11 => '45'],
+ ['color' => [44, 45], 'color_value' => ['red', 'black']],
+ ],
'multiselect without options' => [
10,
[
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 a48f65e1b6d75..280284b7a7cb5 100644
--- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php
+++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php
@@ -111,7 +111,7 @@ class ElasticsearchTest extends TestCase
*/
protected function setUp(): void
{
- if (!class_exists(\Elasticsearch\Client::class)) {
+ if (!class_exists(\Elasticsearch\ClientBuilder::class)) { /** @phpstan-ignore-line */
$this->markTestSkipped('AC-6597: Skipped as Elasticsearch 8 is configured');
}
diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php
index 2cf8c9f6a3fa9..6b0e3961a8fc3 100644
--- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php
+++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/DynamicFieldTest.php
@@ -8,6 +8,7 @@
namespace Magento\Elasticsearch\Test\Unit\Model\Adapter\FieldMapper\Product\FieldProvider;
use Magento\Catalog\Model\ResourceModel\Category\Collection;
+use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory;
use Magento\Customer\Api\Data\GroupInterface;
use Magento\Customer\Api\Data\GroupSearchResultsInterface;
use Magento\Customer\Api\GroupRepositoryInterface;
@@ -111,8 +112,13 @@ protected function setUp(): void
->disableOriginalConstructor()
->onlyMethods(['getAllIds'])
->getMock();
+ $categoryCollection = $this->getMockBuilder(CollectionFactory::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['create'])
+ ->getMock();
+ $categoryCollection->method('create')
+ ->willReturn($this->categoryCollection);
$this->storeManager = $this->createMock(StoreManagerInterface::class);
-
$this->provider = new DynamicField(
$this->fieldTypeConverter,
$this->indexTypeConverter,
@@ -121,7 +127,8 @@ protected function setUp(): void
$this->fieldNameResolver,
$this->attributeAdapterProvider,
$this->categoryCollection,
- $this->storeManager
+ $this->storeManager,
+ $categoryCollection
);
}
diff --git a/app/code/Magento/Elasticsearch/composer.json b/app/code/Magento/Elasticsearch/composer.json
index 9e6d4ceaf16e3..714890fd5f452 100644
--- a/app/code/Magento/Elasticsearch/composer.json
+++ b/app/code/Magento/Elasticsearch/composer.json
@@ -12,7 +12,7 @@
"magento/module-store": "*",
"magento/module-catalog-inventory": "*",
"magento/framework": "*",
- "elasticsearch/elasticsearch": "~7.17.0"
+ "elasticsearch/elasticsearch": "~7.17.0 || ~8.5.0"
},
"suggest": {
"magento/module-config": "*"
diff --git a/app/code/Magento/Elasticsearch7/README.md b/app/code/Magento/Elasticsearch7/README.md
index d520f5efc3b91..a0c4063da5d3e 100644
--- a/app/code/Magento/Elasticsearch7/README.md
+++ b/app/code/Magento/Elasticsearch7/README.md
@@ -1,4 +1,4 @@
-#Magento_Elasticsearch7 module
+# Magento_Elasticsearch7 module
Magento_Elasticsearch7 module allows using ElasticSearch engine 7.x version for the product searching capabilities.
diff --git a/app/code/Magento/Elasticsearch7/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php b/app/code/Magento/Elasticsearch7/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php
index 3dca34b09cb78..ab702e6441341 100644
--- a/app/code/Magento/Elasticsearch7/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php
+++ b/app/code/Magento/Elasticsearch7/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php
@@ -226,7 +226,7 @@ public function testGetItemsWithEnabledSearchSuggestion(): void
*/
public function testGetItemsException(): void
{
- if (!class_exists(\Elasticsearch\Client::class)) {
+ if (!class_exists(\Elasticsearch\ClientBuilder::class)) { /** @phpstan-ignore-line */
$this->markTestSkipped('AC-6597: Skipped as Elasticsearch 8 is configured');
}
diff --git a/app/code/Magento/Elasticsearch8/Block/Adminhtml/System/Config/TestConnection.php b/app/code/Magento/Elasticsearch8/Block/Adminhtml/System/Config/TestConnection.php
deleted file mode 100644
index 8168819d79a3b..0000000000000
--- a/app/code/Magento/Elasticsearch8/Block/Adminhtml/System/Config/TestConnection.php
+++ /dev/null
@@ -1,33 +0,0 @@
- 'catalog_search_engine',
- 'hostname' => 'catalog_search_elasticsearch8_server_hostname',
- 'port' => 'catalog_search_elasticsearch8_server_port',
- 'index' => 'catalog_search_elasticsearch8_index_prefix',
- 'enableAuth' => 'catalog_search_elasticsearch8_enable_auth',
- 'username' => 'catalog_search_elasticsearch8_username',
- 'password' => 'catalog_search_elasticsearch8_password',
- 'timeout' => 'catalog_search_elasticsearch8_server_timeout',
- ];
-
- return array_merge(parent::_getFieldMapping(), $fields);
- }
-}
diff --git a/app/code/Magento/Elasticsearch8/LICENSE.txt b/app/code/Magento/Elasticsearch8/LICENSE.txt
deleted file mode 100644
index 49525fd99da9c..0000000000000
--- a/app/code/Magento/Elasticsearch8/LICENSE.txt
+++ /dev/null
@@ -1,48 +0,0 @@
-
-Open Software License ("OSL") v. 3.0
-
-This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work:
-
-Licensed under the Open Software License version 3.0
-
- 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following:
-
- 1. to reproduce the Original Work in copies, either alone or as part of a collective work;
-
- 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work;
-
- 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License;
-
- 4. to perform the Original Work publicly; and
-
- 5. to display the Original Work publicly.
-
- 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works.
-
- 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work.
-
- 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license.
-
- 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c).
-
- 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work.
-
- 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer.
-
- 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation.
-
- 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c).
-
- 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware.
-
- 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License.
-
- 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License.
-
- 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable.
-
- 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
-
- 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You.
-
- 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process.
\ No newline at end of file
diff --git a/app/code/Magento/Elasticsearch8/LICENSE_AFL.txt b/app/code/Magento/Elasticsearch8/LICENSE_AFL.txt
deleted file mode 100644
index f39d641b18a19..0000000000000
--- a/app/code/Magento/Elasticsearch8/LICENSE_AFL.txt
+++ /dev/null
@@ -1,48 +0,0 @@
-
-Academic Free License ("AFL") v. 3.0
-
-This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work:
-
-Licensed under the Academic Free License version 3.0
-
- 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following:
-
- 1. to reproduce the Original Work in copies, either alone or as part of a collective work;
-
- 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work;
-
- 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License;
-
- 4. to perform the Original Work publicly; and
-
- 5. to display the Original Work publicly.
-
- 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works.
-
- 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work.
-
- 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license.
-
- 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c).
-
- 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work.
-
- 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer.
-
- 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation.
-
- 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c).
-
- 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware.
-
- 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License.
-
- 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License.
-
- 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable.
-
- 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
-
- 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You.
-
- 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process.
diff --git a/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/IntegerMapper.php b/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/IntegerMapper.php
deleted file mode 100644
index 96b45ca2f75e7..0000000000000
--- a/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/IntegerMapper.php
+++ /dev/null
@@ -1,31 +0,0 @@
- [
- 'match_mapping_type' => 'long',
- 'mapping' => [
- 'type' => 'integer',
- ],
- ],
- ];
-
- return $templates;
- }
-}
diff --git a/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/MapperInterface.php b/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/MapperInterface.php
deleted file mode 100644
index f4a40ae475b92..0000000000000
--- a/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/MapperInterface.php
+++ /dev/null
@@ -1,22 +0,0 @@
- [
- 'match' => 'position_*',
- 'match_mapping_type' => 'string',
- 'mapping' => [
- 'type' => 'integer',
- 'index' => true,
- ],
- ],
- ];
-
- return $templates;
- }
-}
diff --git a/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/PriceMapper.php b/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/PriceMapper.php
deleted file mode 100644
index c67c92deedffa..0000000000000
--- a/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/PriceMapper.php
+++ /dev/null
@@ -1,33 +0,0 @@
- [
- 'match' => 'price_*',
- 'match_mapping_type' => 'string',
- 'mapping' => [
- 'type' => 'double',
- 'store' => true,
- ],
- ],
- ];
-
- return $templates;
- }
-}
diff --git a/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/StringMapper.php b/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/StringMapper.php
deleted file mode 100644
index 4a08d1760d66a..0000000000000
--- a/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplates/StringMapper.php
+++ /dev/null
@@ -1,34 +0,0 @@
- [
- 'match' => '*',
- 'match_mapping_type' => 'string',
- 'mapping' => [
- 'type' => 'text',
- 'index' => true,
- 'copy_to' => '_search',
- ],
- ],
- ];
-
- return $templates;
- }
-}
diff --git a/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplatesProvider.php b/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplatesProvider.php
deleted file mode 100644
index db016e2a101f9..0000000000000
--- a/app/code/Magento/Elasticsearch8/Model/Adapter/DynamicTemplatesProvider.php
+++ /dev/null
@@ -1,51 +0,0 @@
-mappers = $mappers;
- }
-
- /**
- * Get elasticsearch dynamic templates.
- *
- * @return array
- * @throws InvalidArgumentException
- */
- public function getTemplates(): array
- {
- $templates = [];
- foreach ($this->mappers as $mapper) {
- if (!$mapper instanceof MapperInterface) {
- throw new InvalidArgumentException(
- __('Mapper %1 should implement %2', get_class($mapper), MapperInterface::class)
- );
- }
- $templates = $mapper->processTemplates($templates);
- }
-
- return $templates;
- }
-}
diff --git a/app/code/Magento/Elasticsearch8/Model/Adapter/Elasticsearch.php b/app/code/Magento/Elasticsearch8/Model/Adapter/Elasticsearch.php
deleted file mode 100644
index 6d3ca2add8678..0000000000000
--- a/app/code/Magento/Elasticsearch8/Model/Adapter/Elasticsearch.php
+++ /dev/null
@@ -1,49 +0,0 @@
- $indexName,
- 'body' => [],
- 'refresh' => true,
- ];
-
- foreach ($documents as $id => $document) {
- $bulkArray['body'][] = [
- $action => [
- '_id' => $id,
- '_index' => $indexName
- ]
- ];
-
- if ($action == self::BULK_ACTION_INDEX) {
- $bulkArray['body'][] = $document;
- }
- }
-
- return $bulkArray;
- }
-}
diff --git a/app/code/Magento/Elasticsearch8/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php b/app/code/Magento/Elasticsearch8/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php
deleted file mode 100644
index faa81cc2ab298..0000000000000
--- a/app/code/Magento/Elasticsearch8/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php
+++ /dev/null
@@ -1,48 +0,0 @@
-baseResolver = $baseResolver;
- }
-
- /**
- * Get field name.
- *
- * @param AttributeAdapter $attribute
- * @param array $context
- * @return string|null
- */
- public function getFieldName(AttributeAdapter $attribute, $context = []): ?string
- {
- $fieldName = $this->baseResolver->getFieldName($attribute, $context);
- if ($fieldName === '_all') {
- $fieldName = '_search';
- }
-
- return $fieldName;
- }
-}
diff --git a/app/code/Magento/Elasticsearch8/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch8/Model/Client/Elasticsearch.php
deleted file mode 100644
index d43551d5c81f2..0000000000000
--- a/app/code/Magento/Elasticsearch8/Model/Client/Elasticsearch.php
+++ /dev/null
@@ -1,406 +0,0 @@
-client[getmypid()] = $elasticsearchClient;
- }
- $this->clientOptions = $options;
- $this->fieldsMappingPreprocessors = $fieldsMappingPreprocessors;
- $this->dynamicTemplatesProvider = $dynamicTemplatesProvider ?: ObjectManager::getInstance()
- ->get(DynamicTemplatesProvider::class);
- }
-
- /**
- * Get Elasticsearch 8 Client
- *
- * @return Client|null
- */
- private function getElasticsearchClient(): ?Client /** @phpstan-ignore-line */
- {
- // Intentionally added condition as there are BC changes from ES7 to ES8
- // and by default ES7 is configured.
- if (!class_exists(\Elastic\Elasticsearch\Client::class)) {
- return null;
- }
-
- $pid = getmypid();
- if (!isset($this->client[$pid])) {
- $config = $this->buildESConfig($this->clientOptions);
- $this->client[$pid] = ClientBuilder::fromConfig($config, true); /** @phpstan-ignore-line */
- }
-
- return $this->client[$pid];
- }
-
- /**
- * Ping the Elasticsearch 8 client
- *
- * @return bool
- */
- public function ping(): bool
- {
- $elasticsearchClient = $this->getElasticsearchClient();
- if ($this->pingResult === false && $elasticsearchClient) {
- $this->pingResult = $elasticsearchClient->ping(
- ['client' => ['timeout' => $this->clientOptions['timeout']]]
- )->asBool();
- }
-
- return $this->pingResult;
- }
-
- /**
- * Validate connection params for Elasticsearch 8
- *
- * @return bool
- */
- public function testConnection(): bool
- {
- return $this->ping();
- }
-
- /**
- * Add/update an Elasticsearch index settings.
- *
- * @param string $index
- * @param array $settings
- * @return void
- */
- public function putIndexSettings(string $index, array $settings): void
- {
- $elasticsearchClient = $this->getElasticsearchClient();
- if ($elasticsearchClient) {
- $elasticsearchClient->indices()
- ->putSettings(['index' => $index, 'body' => $settings]);
- }
- }
-
- /**
- * Updates alias.
- *
- * @param string $alias
- * @param string $newIndex
- * @param string $oldIndex
- * @return void
- */
- public function updateAlias(string $alias, string $newIndex, string $oldIndex = '')
- {
- $elasticsearchClient = $this->getElasticsearchClient();
- if ($elasticsearchClient === null) {
- return;
- }
-
- $params = ['body' => ['actions' => []]];
- if ($newIndex) {
- $params['body']['actions'][] = ['add' => ['alias' => $alias, 'index' => $newIndex]];
- }
-
- if ($oldIndex) {
- $params['body']['actions'][] = ['remove' => ['alias' => $alias, 'index' => $oldIndex]];
- }
-
- $elasticsearchClient->indices()->updateAliases($params);
- }
-
- /**
- * Checks whether Elasticsearch 8 index exists
- *
- * @param string $index
- * @return bool
- */
- public function indexExists(string $index): bool
- {
- $indexExists = false;
- $elasticsearchClient = $this->getElasticsearchClient();
- if ($elasticsearchClient) {
- $indexExists = $elasticsearchClient->indices()
- ->exists(['index' => $index])
- ->asBool();
- }
-
- return $indexExists;
- }
-
- /**
- * Build config for Elasticsearch 8
- *
- * @param array $options
- * @return array
- */
- private function buildESConfig(array $options = []): array
- {
- $hostname = preg_replace('/http[s]?:\/\//i', '', $options['hostname']);
- // @codingStandardsIgnoreStart
- $protocol = parse_url($options['hostname'], PHP_URL_SCHEME);
- // @codingStandardsIgnoreEnd
- if (!$protocol) {
- $protocol = 'http';
- }
-
- $authString = '';
- if (!empty($options['enableAuth']) && (int)$options['enableAuth'] === 1) {
- $authString = "{$options['username']}:{$options['password']}@";
- }
-
- $portString = '';
- if (!empty($options['port'])) {
- $portString = ':' . $options['port'];
- }
-
- $host = $protocol . '://' . $authString . $hostname . $portString;
-
- $options['hosts'] = [$host];
-
- return $options;
- }
-
- /**
- * Exists alias.
- *
- * @param string $alias
- * @param string $index
- * @return bool
- */
- public function existsAlias(string $alias, string $index = ''): bool
- {
- $existAlias = false;
- $elasticsearchClient = $this->getElasticsearchClient();
- if ($elasticsearchClient) {
- $params = ['name' => $alias];
- if ($index) {
- $params['index'] = $index;
- }
-
- $existAlias = $elasticsearchClient->indices()->existsAlias($params)->asBool();
- }
-
- return $existAlias;
- }
-
- /**
- * Performs bulk query over Elasticsearch 8 index
- *
- * @param array $query
- * @return void
- */
- public function bulkQuery(array $query)
- {
- $elasticsearchClient = $this->getElasticsearchClient();
- if ($elasticsearchClient) {
- $elasticsearchClient->bulk($query);
- }
- }
-
- /**
- * Creates an Elasticsearch 8 index.
- *
- * @param string $index
- * @param array $settings
- * @return void
- */
- public function createIndex(string $index, array $settings): void
- {
- $elasticsearchClient = $this->getElasticsearchClient();
- if ($elasticsearchClient) {
- $elasticsearchClient->indices()
- ->create([
- 'index' => $index,
- 'body' => $settings,
- ]);
- }
- }
-
- /**
- * Get alias.
- *
- * @param string $alias
- * @return array
- */
- public function getAlias(string $alias): array
- {
- $elasticsearchClient = $this->getElasticsearchClient();
- if ($elasticsearchClient === null) {
- return [];
- }
-
- return $elasticsearchClient->indices()
- ->getAlias(['name' => $alias])
- ->asArray();
- }
-
- /**
- * Add mapping to Elasticsearch 8 index
- *
- * @param array $fields
- * @param string $index
- * @param string $entityType
- * @return void
- * @SuppressWarnings("unused")
- */
- public function addFieldsMapping(array $fields, string $index, string $entityType)
- {
- $elasticsearchClient = $this->getElasticsearchClient();
- if ($elasticsearchClient === null) {
- return;
- }
-
- $params = [
- 'index' => $index,
- 'body' => [
- 'properties' => [],
- 'dynamic_templates' => $this->dynamicTemplatesProvider->getTemplates(),
- ],
- ];
-
- foreach ($this->applyFieldsMappingPreprocessors($fields) as $field => $fieldInfo) {
- $params['body']['properties'][$field] = $fieldInfo;
- }
-
- $elasticsearchClient->indices()->putMapping($params);
- }
-
- /**
- * Delete an Elasticsearch 8 index.
- *
- * @param string $index
- * @return void
- */
- public function deleteIndex(string $index)
- {
- $elasticsearchClient = $this->getElasticsearchClient();
- if ($elasticsearchClient) {
- $elasticsearchClient->indices()
- ->delete(['index' => $index]);
- }
- }
-
- /**
- * Check if index is empty.
- *
- * @param string $index
- * @return bool
- */
- public function isEmptyIndex(string $index): bool
- {
- $elasticsearchClient = $this->getElasticsearchClient();
- if ($elasticsearchClient === null) {
- return false;
- }
-
- $stats = $this->getElasticsearchClient()->indices()->stats(['index' => $index, 'metric' => 'docs']);
- if ($stats['indices'][$index]['primaries']['docs']['count'] === 0) {
- return true;
- }
-
- return false;
- }
-
- /**
- * Execute search by $query
- *
- * @param array $query
- */
- public function query(array $query): array
- {
- $elasticsearchClient = $this->getElasticsearchClient();
-
- return $elasticsearchClient === null ? [] : $elasticsearchClient->search($query)->asArray();
- }
-
- /**
- * Get mapping from Elasticsearch index.
- *
- * @param array $params
- * @return array
- */
- public function getMapping(array $params): array
- {
- $elasticsearchClient = $this->getElasticsearchClient();
-
- return $elasticsearchClient === null ? [] : $elasticsearchClient->indices()->getMapping($params)->asArray();
- }
-
- /**
- * Apply fields mapping preprocessors
- *
- * @param array $properties
- * @return array
- */
- private function applyFieldsMappingPreprocessors(array $properties): array
- {
- foreach ($this->fieldsMappingPreprocessors as $preprocessor) {
- $properties = $preprocessor->process($properties);
- }
- return $properties;
- }
-}
diff --git a/app/code/Magento/Elasticsearch8/README.md b/app/code/Magento/Elasticsearch8/README.md
deleted file mode 100644
index 25b26b4ec4373..0000000000000
--- a/app/code/Magento/Elasticsearch8/README.md
+++ /dev/null
@@ -1,32 +0,0 @@
-#Magento_Elasticsearch8 module
-
-Magento_Elasticsearch8 module allows using ElasticSearch engine 8.x version for the product searching capabilities.
-
-The module implements Magento_Search library interfaces.
-
-## Installation details
-
-The Magento_Elasticsearch8 module is one of the base Magento 2 modules. Disabling or uninstalling this module is not recommended.
-
-For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html).
-
-## Structure
-
-`SearchAdapter/` - the directory that contains solutions for adapting ElasticSearch query searching.
-
-For information about a typical file structure of a module in Magento 2, see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/).
-
-## Additional information
-
-By default`indices.id_field_data` is disallowed in Elasticsearch8 hence it needs to enabled it from `elasticsearch.yml`
-by adding the following configuration
-`indices:
-id_field_data:
-enabled: true`
-
-More information about ElasticSearch are at articles:
-
-- [Configuring Catalog Search](https://experienceleague.adobe.com/docs/commerce-admin/catalog/catalog/search/search-configuration.html).
-- [Installation Guide/Elasticsearch](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/prerequisites/search-engine/overview.html).
-- [Configure and maintain Elasticsearch](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/search/overview-search.html).
-- Magento Commerce Cloud - [set up Elasticsearch service](https://experienceleague.adobe.com/docs/commerce-cloud-service/user-guide/configure/service/elasticsearch.html).
diff --git a/app/code/Magento/Elasticsearch8/SearchAdapter/Adapter.php b/app/code/Magento/Elasticsearch8/SearchAdapter/Adapter.php
deleted file mode 100644
index 2d159c764bf69..0000000000000
--- a/app/code/Magento/Elasticsearch8/SearchAdapter/Adapter.php
+++ /dev/null
@@ -1,125 +0,0 @@
- [
- "hits" => []
- ],
- "aggregations" => [
- "price_bucket" => [],
- "category_bucket" => ["buckets" => []],
- ]
- ];
-
- /**
- * @var LoggerInterface
- */
- private LoggerInterface $logger;
-
- /**
- * @param ConnectionManager $connectionManager
- * @param Mapper $mapper
- * @param ResponseFactory $responseFactory
- * @param AggregationBuilder $aggregationBuilder
- * @param QueryContainerFactory $queryContainerFactory
- * @param LoggerInterface $logger
- */
- public function __construct(
- ConnectionManager $connectionManager,
- Mapper $mapper,
- ResponseFactory $responseFactory,
- AggregationBuilder $aggregationBuilder,
- QueryContainerFactory $queryContainerFactory,
- LoggerInterface $logger
- ) {
- $this->connectionManager = $connectionManager;
- $this->mapper = $mapper;
- $this->responseFactory = $responseFactory;
- $this->aggregationBuilder = $aggregationBuilder;
- $this->queryContainerFactory = $queryContainerFactory;
- $this->logger = $logger;
- }
-
- /**
- * Search query
- *
- * @param RequestInterface $request
- * @return QueryResponse
- */
- public function query(RequestInterface $request) : QueryResponse
- {
- $client = $this->connectionManager->getConnection();
- $query = $this->mapper->buildQuery($request);
- $aggregationBuilder = $this->aggregationBuilder;
- $aggregationBuilder->setQuery($this->queryContainerFactory->create(['query' => $query]));
-
- try {
- $rawResponse = $client->query($query);
- } catch (\Exception $e) {
- $this->logger->critical($e);
- // return empty search result in case an exception is thrown from Elasticsearch
- $rawResponse = self::$emptyRawResponse;
- }
-
- $rawDocuments = $rawResponse['hits']['hits'] ?? [];
- return $this->responseFactory->create(
- [
- 'documents' => $rawDocuments,
- 'aggregations' => $aggregationBuilder->build($request, $rawResponse),
- 'total' => $rawResponse['hits']['total']['value'] ?? 0
- ]
- );
- }
-}
diff --git a/app/code/Magento/Elasticsearch8/SearchAdapter/Mapper.php b/app/code/Magento/Elasticsearch8/SearchAdapter/Mapper.php
deleted file mode 100644
index 8c320ed152dba..0000000000000
--- a/app/code/Magento/Elasticsearch8/SearchAdapter/Mapper.php
+++ /dev/null
@@ -1,45 +0,0 @@
-mapper = $mapper;
- }
-
- /**
- * Build adapter dependent query
- *
- * @param RequestInterface $request
- * @return array
- */
- public function buildQuery(RequestInterface $request) : array
- {
- $searchQuery = $this->mapper->buildQuery($request);
- $searchQuery['track_total_hits'] = true;
- return $searchQuery;
- }
-}
diff --git a/app/code/Magento/Elasticsearch8/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearch8ByProductSkuTest.xml b/app/code/Magento/Elasticsearch8/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearch8ByProductSkuTest.xml
deleted file mode 100644
index cf733bd22f8e4..0000000000000
--- a/app/code/Magento/Elasticsearch8/Test/Mftf/Test/StorefrontQuickSearchUsingElasticSearch8ByProductSkuTest.xml
+++ /dev/null
@@ -1,63 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Elasticsearch8/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php b/app/code/Magento/Elasticsearch8/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php
deleted file mode 100644
index 1598c83c735d5..0000000000000
--- a/app/code/Magento/Elasticsearch8/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolverTest.php
+++ /dev/null
@@ -1,128 +0,0 @@
-fieldTypeConverter = $this->getMockBuilder(FieldTypeConverterInterface::class)
- ->disableOriginalConstructor()
- ->setMethods(['convert'])
- ->getMockForAbstractClass();
- $this->fieldTypeResolver = $this->getMockBuilder(FieldTypeResolver::class)
- ->disableOriginalConstructor()
- ->setMethods(['getFieldType'])
- ->getMockForAbstractClass();
-
- $baseResolver = $objectManager->getObject(
- BaseDefaultResolver::class,
- [
- 'fieldTypeResolver' => $this->fieldTypeResolver,
- 'fieldTypeConverter' => $this->fieldTypeConverter
- ]
- );
-
- $this->resolver = $objectManager->getObject(DefaultResolver::class, ['baseResolver' => $baseResolver]);
- }
-
- /**
- * @dataProvider getFieldNameProvider
- * @param $fieldType
- * @param $attributeCode
- * @param $frontendInput
- * @param $isSortable
- * @param $context
- * @param $expected
- * @return void
- */
- public function testGetFieldName(
- $fieldType,
- $attributeCode,
- $frontendInput,
- $isSortable,
- $context,
- $expected
- ) {
- $attributeMock = $this->getMockBuilder(AttributeAdapter::class)
- ->disableOriginalConstructor()
- ->setMethods(['getAttributeCode', 'getFrontendInput', 'isSortable'])
- ->getMock();
- $this->fieldTypeConverter->expects($this->any())
- ->method('convert')
- ->willReturn('string');
- $attributeMock->expects($this->any())
- ->method('getFrontendInput')
- ->willReturn($frontendInput);
- $attributeMock->expects($this->any())
- ->method('getAttributeCode')
- ->willReturn($attributeCode);
- $attributeMock->expects($this->any())
- ->method('isSortable')
- ->willReturn($isSortable);
- $this->fieldTypeResolver->expects($this->any())
- ->method('getFieldType')
- ->willReturn($fieldType);
-
- $this->assertEquals(
- $expected,
- $this->resolver->getFieldName($attributeMock, $context)
- );
- }
-
- /**
- * @return array
- */
- public function getFieldNameProvider(): array
- {
- return [
- ['', 'code', '', false, [], 'code'],
- ['', 'code', '', false, ['type' => 'default'], 'code'],
- ['string', '*', '', false, ['type' => 'default'], '_search'],
- ['', 'code', '', false, ['type' => 'default'], 'code'],
- ['', 'code', 'select', false, ['type' => 'default'], 'code'],
- ['', 'code', '', true, ['type' => 'sort'], 'sort_code'],
- ['', 'code', 'boolean', false, ['type' => 'default'], 'code'],
- ];
- }
-}
diff --git a/app/code/Magento/Elasticsearch8/Test/Unit/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch8/Test/Unit/Model/Client/ElasticsearchTest.php
deleted file mode 100644
index f27235d30229b..0000000000000
--- a/app/code/Magento/Elasticsearch8/Test/Unit/Model/Client/ElasticsearchTest.php
+++ /dev/null
@@ -1,680 +0,0 @@
-elasticsearchClientMock = $this->getMockBuilder(Client::class) /** @phpstan-ignore-line */
- ->setMethods(
- [
- 'indices',
- 'ping',
- 'bulk',
- 'search',
- 'scroll',
- 'info',
- ]
- )
- ->disableOriginalConstructor()
- ->getMock();
- $this->indicesMock = $this->getMockBuilder(Indices::class) /** @phpstan-ignore-line */
- ->setMethods(
- [
- 'exists',
- 'getSettings',
- 'create',
- 'delete',
- 'putMapping',
- 'deleteMapping',
- 'getMapping',
- 'stats',
- 'updateAliases',
- 'existsAlias',
- 'getAlias',
- ]
- )
- ->disableOriginalConstructor()
- ->getMock();
- $this->elasticsearchResponse = $this->getMockBuilder(ElasticsearchResponse::class) /** @phpstan-ignore-line */
- ->setMethods([
- 'asBool',
- 'asArray',
- ])
- ->getMock();
- $this->elasticsearchClientMock->expects($this->any())
- ->method('indices')
- ->willReturn($this->indicesMock);
- $this->elasticsearchClientMock->expects($this->any())
- ->method('ping')
- ->willReturn($this->elasticsearchResponse);
-
- $this->objectManager = new ObjectManagerHelper($this);
- $dynamicTemplatesProvider = new DynamicTemplatesProvider(
- [
- new PriceMapper(),
- new PositionMapper(),
- new StringMapper(),
- new IntegerMapper(),
- ]
- );
- $this->model = $this->objectManager->getObject(
- Elasticsearch::class,
- [
- 'options' => $this->getOptions(),
- 'elasticsearchClient' => $this->elasticsearchClientMock,
- 'fieldsMappingPreprocessors' => [new AddDefaultSearchField()],
- 'dynamicTemplatesProvider' => $dynamicTemplatesProvider,
- ]
- );
- }
-
- /**
- * Test configurations with exception
- *
- * @return void
- */
- public function testConstructorOptionsException()
- {
- $this->expectException('Magento\Framework\Exception\LocalizedException');
- $result = $this->objectManager->getObject(
- Elasticsearch::class,
- [
- 'options' => [],
- ]
- );
- $this->assertNotNull($result);
- }
-
- /**
- * Test client creation from the list of options
- */
- public function testConstructorWithOptions()
- {
- $result = $this->objectManager->getObject(
- Elasticsearch::class,
- [
- 'options' => $this->getOptions(),
- ]
- );
- $this->assertNotNull($result);
- }
-
- /**
- * Ensure that configuration returns correct url.
- *
- * @param array $options
- * @param string $expectedResult
- * @throws LocalizedException
- * @throws \ReflectionException
- * @dataProvider getOptionsDataProvider
- */
- public function testBuildConfig(array $options, string $expectedResult): void
- {
- $buildConfig = new Elasticsearch($options);
- $config = $this->getPrivateMethod();
- $result = $config->invoke($buildConfig, $options);
- $this->assertEquals($expectedResult, $result['hosts'][0]);
- }
-
- /**
- * Return private method for elastic search class.
- *
- * @return \ReflectionMethod
- */
- private function getPrivateMethod(): \ReflectionMethod
- {
- $reflector = new \ReflectionClass(Elasticsearch::class);
- $method = $reflector->getMethod('buildESConfig');
- $method->setAccessible(true);
-
- return $method;
- }
-
- /**
- * Get options data provider.
- */
- public function getOptionsDataProvider(): array
- {
- return [
- [
- 'without_protocol' => [
- 'hostname' => 'localhost',
- 'port' => '9200',
- 'timeout' => 15,
- 'index' => 'magento2',
- 'enableAuth' => 0,
- ],
- 'expected_result' => 'http://localhost:9200',
- ],
- [
- 'with_protocol' => [
- 'hostname' => 'https://localhost',
- 'port' => '9200',
- 'timeout' => 15,
- 'index' => 'magento2',
- 'enableAuth' => 0,
- ],
- 'expected_result' => 'https://localhost:9200',
- ],
- ];
- }
-
- /**
- * Test ping functionality
- */
- public function testPing()
- {
- $this->elasticsearchResponse->expects($this->once())
- ->method('asBool')
- ->willReturn(true);
- $this->assertTrue($this->model->ping());
- }
-
- /**
- * Get elasticsearch client options
- *
- * @return array
- */
- protected function getOptions(): array
- {
- return [
- 'hostname' => 'localhost',
- 'port' => '9200',
- 'timeout' => 15,
- 'index' => 'magento2',
- 'enableAuth' => 1,
- 'username' => 'user',
- 'password' => 'passwd',
- ];
- }
-
- /**
- * Test validation of connection parameters
- */
- public function testTestConnection()
- {
- $this->elasticsearchResponse->expects($this->once())
- ->method('asBool')
- ->willReturn(true);
- $this->assertTrue($this->model->testConnection());
- }
-
- /**
- * Test validation of connection parameters returns false
- */
- public function testTestConnectionFalse()
- {
- $this->elasticsearchResponse->expects($this->once())
- ->method('asBool')
- ->willReturn(false);
- $this->assertFalse($this->model->testConnection());
- }
-
- /**
- * Test validation of connection parameters
- */
- public function testTestConnectionPing()
- {
- $this->elasticsearchResponse->expects($this->once())
- ->method('asBool')
- ->willReturn(true);
- $this->model = $this->objectManager->getObject(
- Elasticsearch::class,
- [
- 'options' => $this->getEmptyIndexOption(),
- 'elasticsearchClient' => $this->elasticsearchClientMock,
- ]
- );
-
- $this->model->ping();
- $this->assertTrue($this->model->testConnection());
- }
-
- /**
- * @return array
- */
- private function getEmptyIndexOption(): array
- {
- return [
- 'hostname' => 'localhost',
- 'port' => '9200',
- 'index' => '',
- 'timeout' => 15,
- 'enableAuth' => 1,
- 'username' => 'user',
- 'password' => 'passwd',
- ];
- }
-
- /**
- * Test bulkQuery() method
- */
- public function testBulkQuery()
- {
- $this->elasticsearchClientMock->expects($this->once())
- ->method('bulk')
- ->with([]);
- $this->model->bulkQuery([]);
- }
-
- /**
- * Test createIndex() method, case when such index exists
- */
- public function testCreateIndexExists()
- {
- $this->indicesMock->expects($this->once())
- ->method('create')
- ->with(
- [
- 'index' => 'indexName',
- 'body' => [],
- ]
- );
- $this->model->createIndex('indexName', []);
- }
-
- /**
- * Test deleteIndex() method.
- */
- public function testDeleteIndex()
- {
- $this->indicesMock->expects($this->once())
- ->method('delete')
- ->with(['index' => 'indexName']);
- $this->model->deleteIndex('indexName');
- }
-
- /**
- * Test isEmptyIndex() method.
- */
- public function testIsEmptyIndex()
- {
- $indexName = 'magento2_test_index';
- $stats['indices'][$indexName]['primaries']['docs']['count'] = 0;
-
- $this->indicesMock->expects($this->once())
- ->method('stats')
- ->with(['index' => $indexName, 'metric' => 'docs'])
- ->willReturn($stats);
- $this->assertTrue($this->model->isEmptyIndex($indexName));
- }
-
- /**
- * Test isEmptyIndex() method returns false.
- */
- public function testIsEmptyIndexFalse()
- {
- $indexName = 'magento2_test_index';
- $stats['indices'][$indexName]['primaries']['docs']['count'] = 1;
-
- $this->indicesMock->expects($this->once())
- ->method('stats')
- ->with(['index' => $indexName, 'metric' => 'docs'])
- ->willReturn($stats);
- $this->assertFalse($this->model->isEmptyIndex($indexName));
- }
-
- /**
- * Test updateAlias() method with new index.
- */
- public function testUpdateAlias()
- {
- $alias = 'alias1';
- $index = 'index1';
-
- $params['body']['actions'][] = ['add' => ['alias' => $alias, 'index' => $index]];
-
- $this->indicesMock->expects($this->once())
- ->method('updateAliases')
- ->with($params);
- $this->model->updateAlias($alias, $index);
- }
-
- /**
- * Test updateAlias() method with new and old index.
- */
- public function testUpdateAliasRemoveOldIndex()
- {
- $alias = 'alias1';
- $newIndex = 'index1';
- $oldIndex = 'indexOld';
-
- $params['body']['actions'][] = ['add' => ['alias' => $alias, 'index' => $newIndex]];
- $params['body']['actions'][] = ['remove' => ['alias' => $alias, 'index' => $oldIndex]];
-
- $this->indicesMock->expects($this->once())
- ->method('updateAliases')
- ->with($params);
- $this->model->updateAlias($alias, $newIndex, $oldIndex);
- }
-
- /**
- * Test indexExists() method, case when no such index exists
- */
- public function testIndexExists()
- {
- $this->elasticsearchResponse->expects($this->once())
- ->method('asBool')
- ->willReturn(true);
- $this->indicesMock->expects($this->once())
- ->method('exists')
- ->with(['index' => 'indexName'])
- ->willReturn($this->elasticsearchResponse);
- $this->model->indexExists('indexName');
- }
-
- /**
- * Tests existsAlias() method checking for alias.
- */
- public function testExistsAlias()
- {
- $this->elasticsearchResponse->expects($this->once())
- ->method('asBool')
- ->willReturn(true);
- $alias = 'alias1';
- $params = ['name' => $alias];
- $this->indicesMock->expects($this->once())
- ->method('existsAlias')
- ->with($params)
- ->willReturn($this->elasticsearchResponse);
- $this->assertTrue($this->model->existsAlias($alias));
- }
-
- /**
- * Tests existsAlias() method checking for alias and index.
- */
- public function testExistsAliasWithIndex()
- {
- $this->elasticsearchResponse->expects($this->once())
- ->method('asBool')
- ->willReturn(true);
- $alias = 'alias1';
- $index = 'index1';
- $params = ['name' => $alias, 'index' => $index];
- $this->indicesMock->expects($this->once())
- ->method('existsAlias')
- ->with($params)
- ->willReturn($this->elasticsearchResponse);
- $this->assertTrue($this->model->existsAlias($alias, $index));
- }
-
- /**
- * Test getAlias() method.
- */
- public function testGetAlias()
- {
- $this->elasticsearchResponse->expects($this->once())
- ->method('asArray')
- ->willReturn([]);
- $alias = 'alias1';
- $params = ['name' => $alias];
- $this->indicesMock->expects($this->once())
- ->method('getAlias')
- ->with($params)
- ->willReturn($this->elasticsearchResponse);
- $this->assertEquals([], $this->model->getAlias($alias));
- }
-
- /**
- * Test createIndexIfNotExists() method, case when operation fails
- */
- public function testCreateIndexFailure()
- {
- $this->expectException('Exception');
- $this->indicesMock->expects($this->once())
- ->method('create')
- ->with(
- [
- 'index' => 'indexName',
- 'body' => [],
- ]
- )
- ->willThrowException(new \Exception('Something went wrong'));
- $this->model->createIndex('indexName', []);
- }
-
- /**
- * Test testAddFieldsMapping() method
- */
- public function testAddFieldsMapping()
- {
- $this->indicesMock->expects($this->once())
- ->method('putMapping')
- ->with(
- [
- 'index' => 'indexName',
- 'body' => [
- 'properties' => [
- '_search' => [
- 'type' => 'text',
- ],
- 'name' => [
- 'type' => 'text',
- ],
- ],
- 'dynamic_templates' => [
- [
- 'price_mapping' => [
- 'match' => 'price_*',
- 'match_mapping_type' => 'string',
- 'mapping' => [
- 'type' => 'double',
- 'store' => true,
- ],
- ],
- ],
- [
- 'position_mapping' => [
- 'match' => 'position_*',
- 'match_mapping_type' => 'string',
- 'mapping' => [
- 'type' => 'integer',
- 'index' => true,
- ],
- ],
- ],
- [
- 'string_mapping' => [
- 'match' => '*',
- 'match_mapping_type' => 'string',
- 'mapping' => [
- 'type' => 'text',
- 'index' => true,
- 'copy_to' => '_search',
- ],
- ],
- ],
- [
- 'integer_mapping' => [
- 'match_mapping_type' => 'long',
- 'mapping' => [
- 'type' => 'integer',
- ],
- ],
- ],
- ],
- ],
- ]
- );
- $this->model->addFieldsMapping(
- [
- 'name' => [
- 'type' => 'text',
- ],
- ],
- 'indexName',
- 'product'
- );
- }
-
- /**
- * Test testAddFieldsMapping() method
- */
- public function testAddFieldsMappingFailure()
- {
- $this->expectException('Exception');
- $this->indicesMock->expects($this->once())
- ->method('putMapping')
- ->with(
- [
- 'index' => 'indexName',
- 'body' => [
- 'properties' => [
- '_search' => [
- 'type' => 'text',
- ],
- 'name' => [
- 'type' => 'text',
- ],
- ],
- 'dynamic_templates' => [
- [
- 'price_mapping' => [
- 'match' => 'price_*',
- 'match_mapping_type' => 'string',
- 'mapping' => [
- 'type' => 'double',
- 'store' => true,
- ],
- ],
- ],
- [
- 'position_mapping' => [
- 'match' => 'position_*',
- 'match_mapping_type' => 'string',
- 'mapping' => [
- 'type' => 'integer',
- 'index' => true,
- ],
- ],
- ],
- [
- 'string_mapping' => [
- 'match' => '*',
- 'match_mapping_type' => 'string',
- 'mapping' => [
- 'type' => 'text',
- 'index' => true,
- 'copy_to' => '_search',
- ],
- ],
- ],
- [
- 'integer_mapping' => [
- 'match_mapping_type' => 'long',
- 'mapping' => [
- 'type' => 'integer',
- ],
- ],
- ],
- ],
- ],
- ]
- )
- ->willThrowException(new \Exception('Something went wrong'));
- $this->model->addFieldsMapping(
- [
- 'name' => [
- 'type' => 'text',
- ],
- ],
- 'indexName',
- 'product'
- );
- }
-
- /**
- * Test get Elasticsearch mapping process.
- *
- * @return void
- */
- public function testGetMapping(): void
- {
- $this->elasticsearchResponse->expects($this->once())
- ->method('asArray')
- ->willReturn([]);
- $params = ['index' => 'indexName'];
- $this->indicesMock->expects($this->once())
- ->method('getMapping')
- ->with($params)
- ->willReturn($this->elasticsearchResponse);
-
- $this->model->getMapping($params);
- }
-
- /**
- * Test query() method
- *
- * @return void
- */
- public function testQuery()
- {
- $this->elasticsearchResponse->expects($this->once())
- ->method('asArray')
- ->willReturn([]);
- $query = ['test phrase query'];
- $this->elasticsearchClientMock->expects($this->once())
- ->method('search')
- ->with($query)
- ->willReturn($this->elasticsearchResponse);
- $this->assertEquals([], $this->model->query($query));
- }
-}
diff --git a/app/code/Magento/Elasticsearch8/etc/adminhtml/system.xml b/app/code/Magento/Elasticsearch8/etc/adminhtml/system.xml
deleted file mode 100644
index 590f717d306b4..0000000000000
--- a/app/code/Magento/Elasticsearch8/etc/adminhtml/system.xml
+++ /dev/null
@@ -1,93 +0,0 @@
-
-
-
-
-
-
-
-
- Elasticsearch Server Hostname
-
- elasticsearch8
-
-
-
-
- Elasticsearch Server Port
-
- elasticsearch8
-
-
-
-
- Elasticsearch Index Prefix
-
- elasticsearch8
-
-
-
-
- Enable Elasticsearch HTTP Auth
- Magento\Config\Model\Config\Source\Yesno
-
- elasticsearch8
-
-
-
-
- Elasticsearch HTTP Username
-
- elasticsearch8
- 1
-
-
-
-
- Elasticsearch HTTP Password
-
- elasticsearch8
- 1
-
-
-
-
- Elasticsearch Server Timeout
-
- elasticsearch8
-
-
-
-
-
- Test Connection
- Magento\Elasticsearch8\Block\Adminhtml\System\Config\TestConnection
-
- elasticsearch8
-
-
-
- Minimum Terms to Match
-
- elasticsearch8
-
- Learn more about valid syntax.]]>
- Magento\Elasticsearch\Model\Config\Backend\MinimumShouldMatch
-
-
-
-
-
diff --git a/app/code/Magento/Elasticsearch8/etc/config.xml b/app/code/Magento/Elasticsearch8/etc/config.xml
deleted file mode 100644
index 016b249abd4db..0000000000000
--- a/app/code/Magento/Elasticsearch8/etc/config.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
- localhost
- 9200
- magento2
- 0
- 15
-
-
-
-
-
diff --git a/app/code/Magento/Elasticsearch8/etc/di.xml b/app/code/Magento/Elasticsearch8/etc/di.xml
deleted file mode 100644
index c73462a8da22f..0000000000000
--- a/app/code/Magento/Elasticsearch8/etc/di.xml
+++ /dev/null
@@ -1,281 +0,0 @@
-
-
-
-
-
-
-
- - elasticsearch8
-
-
-
-
-
-
-
- - Elastic\Elasticsearch\Exception\ClientResponseException
-
-
-
-
-
-
-
- - Elastic\Elasticsearch\Exception\ClientResponseException
-
-
-
-
-
-
-
- - Elasticsearch 8
-
-
-
-
-
-
-
- - Magento\Elasticsearch\Elasticsearch5\Model\Adapter\BatchDataMapper\CategoryFieldsProvider
-
-
-
-
-
-
- - Magento\Elasticsearch8\Model\Adapter\FieldMapper\ProductFieldMapper
-
-
-
-
-
-
-
- - \Magento\Elasticsearch8\Model\Client\ElasticsearchFactory
-
-
- - \Magento\Elasticsearch\Model\Config
-
-
-
-
-
-
-
- - Magento\Elasticsearch\Model\Indexer\IndexerHandler
-
-
-
-
-
-
-
- - Magento\Elasticsearch\Model\Indexer\IndexStructure
-
-
-
-
-
-
-
- - Magento\Elasticsearch\Model\ResourceModel\Engine
-
-
-
-
-
-
-
- - \Magento\Elasticsearch8\SearchAdapter\Adapter
-
-
-
-
-
-
-
- - elasticsearch8
-
- elasticsearch8
-
-
-
-
-
- Magento\Elasticsearch8\Model\Client\Elasticsearch
-
-
-
-
-
-
- - Magento\Elasticsearch8\Model\Client\ElasticsearchFactory
-
-
-
-
-
-
-
- - Magento\Elasticsearch\Elasticsearch5\SearchAdapter\Aggregation\Interval
-
-
-
-
-
-
-
- - Magento\Elasticsearch\SearchAdapter\Dynamic\DataProvider
-
-
-
-
-
-
- elasticsearch5FieldProvider
-
-
-
-
-
- - Magento\Elasticsearch8\Model\DataProvider\Suggestions
-
-
-
-
-
-
- - \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\NotEavAttribute
- - \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\SpecialAttribute
- - \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\Price
- - \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CategoryName
- - \Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\Position
- - Magento\Elasticsearch8\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\DefaultResolver
-
-
-
-
-
- Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\DefaultResolver
-
-
-
-
- elasticsearch5FieldProvider
- \Magento\Elasticsearch8\Model\Adapter\FieldMapper\Product\FieldProvider\FieldName\Resolver\CompositeResolver
-
-
-
-
-
-
- - 10000
-
-
-
-
-
-
-
- - elasticsearchCategoryCollectionFactory
-
-
-
-
-
-
-
- - elasticsearchAdvancedCollectionFactory
- - elasticsearchAdvancedCollectionFactory
-
-
-
-
-
-
-
- - Magento\Elasticsearch\Model\Advanced\ProductCollectionPrepareStrategy
-
-
-
-
-
-
-
- - elasticsearchFulltextSearchCollectionFactory
-
-
-
-
-
-
-
- - 1
- - 1
- - 1
-
-
- - 1
- - 1
- - 1
- - 1
- - 1
- - 1
- - 1
-
-
-
-
-
-
- - Magento\Elasticsearch\Model\Adapter\FieldMapper\CopySearchableFieldsToSearchField
- - Magento\Elasticsearch\Model\Adapter\FieldMapper\AddDefaultSearchField
-
-
-
-
-
-
-
- - elasticsearch8_server_hostname
- - elasticsearch8_server_port
- - elasticsearch8_server_timeout
- - elasticsearch8_index_prefix
- - elasticsearch8_enable_auth
- - elasticsearch8_username
- - elasticsearch8_password
-
-
-
-
-
-
- - Magento\Elasticsearch8\Setup\InstallConfig
-
-
-
-
-
-
- - Magento\Elasticsearch\Setup\Validator
-
-
-
-
-
-
- - Magento\Elasticsearch8\Model\Adapter\DynamicTemplates\PriceMapper
- - Magento\Elasticsearch8\Model\Adapter\DynamicTemplates\PositionMapper
- - Magento\Elasticsearch8\Model\Adapter\DynamicTemplates\StringMapper
- - Magento\Elasticsearch8\Model\Adapter\DynamicTemplates\IntegerMapper
-
-
-
-
diff --git a/app/code/Magento/Elasticsearch8/etc/search_engine.xml b/app/code/Magento/Elasticsearch8/etc/search_engine.xml
deleted file mode 100644
index 28e4074bfc886..0000000000000
--- a/app/code/Magento/Elasticsearch8/etc/search_engine.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/code/Magento/Elasticsearch8/registration.php b/app/code/Magento/Elasticsearch8/registration.php
deleted file mode 100644
index bfe52f2f4ceee..0000000000000
--- a/app/code/Magento/Elasticsearch8/registration.php
+++ /dev/null
@@ -1,12 +0,0 @@
- ''];
+ /**
+ * @var string
+ */
+ private const CACHE_KEY_PREFIX = "EMAIL_FILTER_";
+
/**
* @var bool
*/
@@ -284,7 +289,7 @@ public function setUseAbsoluteLinks($flag)
* @return $this
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
* @deprecated SID query parameter is not used in URLs anymore.
- * @see setUseSessionInUrl
+ * @see SessionId's in URL
*/
public function setUseSessionInUrl($flag)
{
@@ -408,6 +413,11 @@ public function blockDirective($construction)
{
$skipParams = ['class', 'id', 'output'];
$blockParameters = $this->getParameters($construction[2]);
+
+ if (isset($blockParameters['cache_key'])) {
+ $blockParameters['cache_key'] = self::CACHE_KEY_PREFIX . $blockParameters['cache_key'];
+ }
+
$block = null;
if (isset($blockParameters['class'])) {
@@ -694,7 +704,7 @@ public function varDirective($construction)
* @param string $default assumed modifier if none present
* @return array
* @deprecated 101.0.4 Use the new FilterApplier or Directive Processor interfaces
- * @see explodeModifiers
+ * @see Directive Processor Interfaces
*/
protected function explodeModifiers($value, $default = null)
{
@@ -714,7 +724,7 @@ protected function explodeModifiers($value, $default = null)
* @param string $modifiers
* @return string
* @deprecated 101.0.4 Use the new FilterApplier or Directive Processor interfaces
- * @see applyModifiers
+ * @see Directive Processor Interfaces
*/
protected function applyModifiers($value, $modifiers)
{
@@ -744,7 +754,7 @@ protected function applyModifiers($value, $modifiers)
* @param string $type
* @return string
* @deprecated 101.0.4 Use the new FilterApplier or Directive Processor interfaces
- * @see modifierEscape
+ * @see Directive Processor Interfacees
*/
public function modifierEscape($value, $type = 'html')
{
@@ -1124,16 +1134,16 @@ public function filter($value)
try {
$value = parent::filter($value);
} catch (Exception $e) {
- // Since a single instance of this class can be used to filter content multiple times, reset callbacks to
- // prevent callbacks running for unrelated content (e.g., email subject and email body)
- $this->resetAfterFilterCallbacks();
-
if ($this->_appState->getMode() == State::MODE_DEVELOPER) {
$value = sprintf(__('Error filtering template: %s')->render(), $e->getMessage());
} else {
$value = (string) __("We're sorry, an error has occurred while generating this content.");
}
$this->_logger->critical($e);
+ } finally {
+ // Since a single instance of this class can be used to filter content multiple times, reset callbacks to
+ // prevent callbacks running for unrelated content (e.g., email subject and email body)
+ $this->resetAfterFilterCallbacks();
}
return $value;
}
diff --git a/app/code/Magento/Email/Test/Mftf/Test/AdminMarketingEmailTemplatesNavigateMenuTest.xml b/app/code/Magento/Email/Test/Mftf/Test/AdminMarketingEmailTemplatesNavigateMenuTest.xml
index 40f7b48b21122..2487c288af115 100644
--- a/app/code/Magento/Email/Test/Mftf/Test/AdminMarketingEmailTemplatesNavigateMenuTest.xml
+++ b/app/code/Magento/Email/Test/Mftf/Test/AdminMarketingEmailTemplatesNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Email/Test/Mftf/Test/TransactionalEmailsLogoUploadTest.xml b/app/code/Magento/Email/Test/Mftf/Test/TransactionalEmailsLogoUploadTest.xml
index 89b07e4be44e9..a2d22a14acd2e 100644
--- a/app/code/Magento/Email/Test/Mftf/Test/TransactionalEmailsLogoUploadTest.xml
+++ b/app/code/Magento/Email/Test/Mftf/Test/TransactionalEmailsLogoUploadTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Email/etc/config.xml b/app/code/Magento/Email/etc/config.xml
index 6f486c15472c2..88f7b81ea2ea8 100644
--- a/app/code/Magento/Email/etc/config.xml
+++ b/app/code/Magento/Email/etc/config.xml
@@ -27,6 +27,7 @@
0
localhost
25
+
0
sendmail
none
diff --git a/app/code/Magento/EncryptionKey/README.md b/app/code/Magento/EncryptionKey/README.md
index 07838cceeb3f2..1d4f642ac6033 100644
--- a/app/code/Magento/EncryptionKey/README.md
+++ b/app/code/Magento/EncryptionKey/README.md
@@ -1,4 +1,4 @@
-#Magento_EncryptionKey module
+# Magento_EncryptionKey module
The Magento_EncryptionKey module provides an advanced encryption model to protect passwords and other sensitive data.
diff --git a/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyAutoGenerateKeyTest.xml b/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyAutoGenerateKeyTest.xml
index 02e94d0410103..46b0af6a41ba8 100644
--- a/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyAutoGenerateKeyTest.xml
+++ b/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyAutoGenerateKeyTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyManualGenerateKeyTest.xml b/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyManualGenerateKeyTest.xml
index 10787d056a187..3aa71154de419 100644
--- a/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyManualGenerateKeyTest.xml
+++ b/app/code/Magento/EncryptionKey/Test/Mftf/Test/AdminEncryptionKeyManualGenerateKeyTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Fedex/Model/Config/Backend/FedexUrl.php b/app/code/Magento/Fedex/Model/Config/Backend/FedexUrl.php
new file mode 100644
index 0000000000000..33cd6f64de9cf
--- /dev/null
+++ b/app/code/Magento/Fedex/Model/Config/Backend/FedexUrl.php
@@ -0,0 +1,76 @@
+url = $url;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return AbstractModel
+ * @throws ValidatorException
+ */
+ public function beforeSave(): AbstractModel
+ {
+ $isValid = $this->url->isValid($this->getValue(), ['http', 'https']);
+
+ if ($isValid) {
+ // phpcs:ignore Magento2.Functions.DiscouragedFunction
+ $host = parse_url((string)$this->getValue(), \PHP_URL_HOST);
+
+ if (!empty($host) && !preg_match('/(?:.+\.|^)fedex\.com$/i', $host)) {
+ throw new ValidatorException(__('Fedex API endpoint URL\'s must use fedex.com'));
+ }
+ }
+
+ return parent::beforeSave();
+ }
+}
diff --git a/app/code/Magento/Fedex/README.md b/app/code/Magento/Fedex/README.md
index b872b53bf879e..419d9771987fb 100644
--- a/app/code/Magento/Fedex/README.md
+++ b/app/code/Magento/Fedex/README.md
@@ -17,12 +17,14 @@ A lot of functionality in the module is on JavaScript, use [mixins](https://deve
### Layouts
This module introduces the following layouts in the `view/frontend/layout` directory:
+
- `checkout_cart_index`
- `checkout_index_index`
## Additional information
You can get more information about delivery method in magento at the articles:
+
- [FedEx Configuration Settings](https://docs.magento.com/user-guide/shipping/fedex.html)
- [Delivery Methods Configuration](https://docs.magento.com/user-guide/configuration/sales/delivery-methods.html)
- [Add custom shipping carrier](https://developer.adobe.com/commerce/php/tutorials/frontend/custom-checkout/add-shipping-carrier/)
diff --git a/app/code/Magento/Fedex/Test/Unit/Model/Config/Backend/FedexUrlTest.php b/app/code/Magento/Fedex/Test/Unit/Model/Config/Backend/FedexUrlTest.php
new file mode 100644
index 0000000000000..56626222312bf
--- /dev/null
+++ b/app/code/Magento/Fedex/Test/Unit/Model/Config/Backend/FedexUrlTest.php
@@ -0,0 +1,131 @@
+contextMock = $this->createMock(Context::class);
+ $registry = $this->createMock(Registry::class);
+ $config = $this->createMock(ScopeConfigInterface::class);
+ $cacheTypeList = $this->createMock(TypeListInterface::class);
+ $this->url = $this->createMock(Url::class);
+ $resource = $this->createMock(AbstractResource::class);
+ $resourceCollection = $this->createMock(AbstractDb::class);
+ $eventManagerMock = $this->getMockForAbstractClass(ManagerInterface::class);
+ $eventManagerMock->expects($this->any())->method('dispatch');
+ $this->contextMock->expects($this->any())->method('getEventDispatcher')->willReturn($eventManagerMock);
+
+ $this->urlConfig = $objectManager->getObject(
+ FedexUrl::class,
+ [
+ 'url' => $this->url,
+ 'context' => $this->contextMock,
+ 'registry' => $registry,
+ 'config' => $config,
+ 'cacheTypeList' => $cacheTypeList,
+ 'resource' => $resource,
+ 'resourceCollection' => $resourceCollection,
+ ]
+ );
+ }
+
+ /**
+ * @dataProvider validDataProvider
+ * @param string|null $data The valid data
+ * @throws ValidatorException
+ */
+ public function testBeforeSave(string $data = null): void
+ {
+ $this->url->expects($this->any())->method('isValid')->willReturn(true);
+ $this->urlConfig->setValue($data);
+ $this->urlConfig->beforeSave();
+ $this->assertTrue($this->url->isValid($data));
+ }
+
+ /**
+ * @dataProvider invalidDataProvider
+ * @param string $data The invalid data
+ */
+ public function testBeforeSaveErrors(string $data): void
+ {
+ $this->url->expects($this->any())->method('isValid')->willReturn(true);
+ $this->expectException('Magento\Framework\Exception\ValidatorException');
+ $this->expectExceptionMessage('Fedex API endpoint URL\'s must use fedex.com');
+ $this->urlConfig->setValue($data);
+ $this->urlConfig->beforeSave();
+ }
+
+ /**
+ * Validator Data Provider
+ *
+ * @return array
+ */
+ public function validDataProvider(): array
+ {
+ return [
+ [],
+ [null],
+ [''],
+ ['http://fedex.com'],
+ ['https://foo.fedex.com'],
+ ['http://foo.fedex.com/foo/bar?baz=bash&fizz=buzz'],
+ ];
+ }
+
+ /**
+ * @return \string[][]
+ */
+ public function invalidDataProvider(): array
+ {
+ return [
+ ['http://fedexfoo.com'],
+ ['https://foofedex.com'],
+ ['https://fedex.com.fake.com'],
+ ['https://fedex.info'],
+ ['http://fedex.com.foo.com/foo/bar?baz=bash&fizz=buzz'],
+ ['http://foofedex.com/foo/bar?baz=bash&fizz=buzz'],
+ ];
+ }
+}
diff --git a/app/code/Magento/Fedex/Test/Unit/Model/Source/GenericTest.php b/app/code/Magento/Fedex/Test/Unit/Model/Source/GenericTest.php
index 124d80b7cf4e1..1d85cd923dd2e 100644
--- a/app/code/Magento/Fedex/Test/Unit/Model/Source/GenericTest.php
+++ b/app/code/Magento/Fedex/Test/Unit/Model/Source/GenericTest.php
@@ -55,9 +55,8 @@ protected function setUp(): void
* @return void
* @dataProvider toOptionArrayDataProvider
*/
- public function testToOptionArray($code, $methods, $result): void
+ public function testToOptionArray($methods, $result): void
{
- $this->model->code = $code;
$this->shippingFedexMock->expects($this->once())
->method('getCode')
->willReturn($methods);
@@ -74,7 +73,6 @@ public function toOptionArrayDataProvider(): array
{
return [
[
- 'method',
[
'FEDEX_GROUND' => __('Ground'),
'FIRST_OVERNIGHT' => __('First Overnight')
@@ -85,7 +83,6 @@ public function toOptionArrayDataProvider(): array
]
],
[
- '',
false,
[]
]
diff --git a/app/code/Magento/Fedex/etc/adminhtml/system.xml b/app/code/Magento/Fedex/etc/adminhtml/system.xml
index f164a8e21e0ae..a200b5bda7199 100644
--- a/app/code/Magento/Fedex/etc/adminhtml/system.xml
+++ b/app/code/Magento/Fedex/etc/adminhtml/system.xml
@@ -40,12 +40,14 @@
Web-Services URL (Production)
+ Magento\Fedex\Model\Config\Backend\FedexUrl
0
Web-Services URL (Sandbox)
+ Magento\Fedex\Model\Config\Backend\FedexUrl
1
diff --git a/app/code/Magento/Fedex/i18n/en_US.csv b/app/code/Magento/Fedex/i18n/en_US.csv
index d1509d42730bc..2911ebe793f23 100644
--- a/app/code/Magento/Fedex/i18n/en_US.csv
+++ b/app/code/Magento/Fedex/i18n/en_US.csv
@@ -78,3 +78,4 @@ Debug,Debug
"Show Method if Not Applicable","Show Method if Not Applicable"
"Sort Order","Sort Order"
"Can't convert a shipping cost from ""%1-%2"" for FedEx carrier.","Can't convert a shipping cost from ""%1-%2"" for FedEx carrier."
+"Fedex API endpoint URL\'s must use fedex.com","Fedex API endpoint URL\'s must use fedex.com"
diff --git a/app/code/Magento/GiftMessage/Model/OrderItemRepository.php b/app/code/Magento/GiftMessage/Model/OrderItemRepository.php
index 445ba54ac4d9c..5f9828a9c35e9 100644
--- a/app/code/Magento/GiftMessage/Model/OrderItemRepository.php
+++ b/app/code/Magento/GiftMessage/Model/OrderItemRepository.php
@@ -10,14 +10,15 @@
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Exception\State\InvalidTransitionException;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Order item gift message repository object.
*/
-class OrderItemRepository implements \Magento\GiftMessage\Api\OrderItemRepositoryInterface
+class OrderItemRepository implements \Magento\GiftMessage\Api\OrderItemRepositoryInterface, ResetAfterRequestInterface
{
/**
- * Order factory.
+ * Factory for Order instances.
*
* @var \Magento\Sales\Model\OrderFactory
*/
@@ -38,7 +39,7 @@ class OrderItemRepository implements \Magento\GiftMessage\Api\OrderItemRepositor
protected $storeManager;
/**
- * Gift message save model.
+ * Model for Gift message save.
*
* @var \Magento\GiftMessage\Model\Save
*/
@@ -52,8 +53,6 @@ class OrderItemRepository implements \Magento\GiftMessage\Api\OrderItemRepositor
protected $helper;
/**
- * Message factory.
- *
* @var \Magento\GiftMessage\Model\MessageFactory
*/
protected $messageFactory;
@@ -175,4 +174,12 @@ protected function getItemById($orderId, $orderItemId)
}
return false;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->orders = [];
+ }
}
diff --git a/app/code/Magento/GiftMessage/README.md b/app/code/Magento/GiftMessage/README.md
index b63c37cc64c7d..ba3bb3962b062 100644
--- a/app/code/Magento/GiftMessage/README.md
+++ b/app/code/Magento/GiftMessage/README.md
@@ -36,6 +36,7 @@ A lot of functionality in the module is on JavaScript, use [mixins](https://deve
### Events
The module dispatches the following events:
+
- `gift_options_prepare_items` event in the `\Magento\GiftMessage\Block\Message\Inline::getItems` method. Parameters:
- `items` is a entityItems (`array` type)
@@ -47,6 +48,7 @@ For information about an event in Magento 2, see [Events and observers](https://
### Layout
This module introduces the following layouts in the `view/frontend/layout` and `view/adminhtml/layout` directories:
+
- `view/adminhtml/layout`:
- `sales_order_create_index`
- `sales_order_create_load_block_data`
@@ -70,11 +72,11 @@ For more information about a layout in Magento 2, see the [Layout documentation]
- `\Magento\GiftMessage\Api\CartRepositoryInterface`
- get the gift message by cart ID for specified shopping cart
- set the gift message for an entire shopping cart
-
+
- `\Magento\GiftMessage\Api\GuestCartRepositoryInterface`
- get the gift message by cart ID for specified shopping cart
- set the gift message for an entire shopping cart
-
+
#### Cart Item
- `\Magento\GiftMessage\Api\GuestItemRepositoryInterface`
@@ -84,7 +86,7 @@ For more information about a layout in Magento 2, see the [Layout documentation]
- `\Magento\GiftMessage\Api\ItemRepositoryInterface`
- get the gift message for a specified item in a specified shopping cart
- set the gift message for a specified item in a specified shopping cart
-
+
#### Order
- `\Magento\GiftMessage\Api\OrderItemRepositoryInterface`
@@ -96,7 +98,7 @@ For more information about a layout in Magento 2, see the [Layout documentation]
- `\Magento\GiftMessage\Api\OrderItemRepositoryInterface`
- get the gift message for a specified item in a specified order
- set the gift message for a specified item in a specified order
-
+
For information about a public API in Magento 2, see [Public interfaces & APIs](https://developer.adobe.com/commerce/php/development/components/api-concepts/).
## Additional information
diff --git a/app/code/Magento/GiftMessage/Test/Mftf/ActionGroup/GuestGiftCheckoutFillingShippingSectionActionGroup.xml b/app/code/Magento/GiftMessage/Test/Mftf/ActionGroup/GuestGiftCheckoutFillingShippingSectionActionGroup.xml
index 9da9c8cac1483..712f66893017a 100644
--- a/app/code/Magento/GiftMessage/Test/Mftf/ActionGroup/GuestGiftCheckoutFillingShippingSectionActionGroup.xml
+++ b/app/code/Magento/GiftMessage/Test/Mftf/ActionGroup/GuestGiftCheckoutFillingShippingSectionActionGroup.xml
@@ -19,6 +19,6 @@
-
+
diff --git a/app/code/Magento/GiftMessageGraphQl/README.md b/app/code/Magento/GiftMessageGraphQl/README.md
index 5eb270c12fdb1..485b403bbc34c 100644
--- a/app/code/Magento/GiftMessageGraphQl/README.md
+++ b/app/code/Magento/GiftMessageGraphQl/README.md
@@ -16,4 +16,4 @@ Extension developers can interact with the Magento_GiftMessageGraphQl module. Fo
## Additional information
-You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql).
+You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/).
diff --git a/app/code/Magento/GoogleAdwords/README.md b/app/code/Magento/GoogleAdwords/README.md
index 2e2b275787f32..d79a7837149db 100644
--- a/app/code/Magento/GoogleAdwords/README.md
+++ b/app/code/Magento/GoogleAdwords/README.md
@@ -17,6 +17,7 @@ Extension developers can interact with the Magento_GoogleAdwords module. For mor
### Layouts
This module introduces the following layouts in the `view/frontend/layout` directory:
+
- `checkout_onepage_success`
For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/).
diff --git a/app/code/Magento/GoogleAdwords/Test/Mftf/Test/AdminValidateConversionIdConfigTest.xml b/app/code/Magento/GoogleAdwords/Test/Mftf/Test/AdminValidateConversionIdConfigTest.xml
index 050f8711027ec..68f49ff1ebee1 100644
--- a/app/code/Magento/GoogleAdwords/Test/Mftf/Test/AdminValidateConversionIdConfigTest.xml
+++ b/app/code/Magento/GoogleAdwords/Test/Mftf/Test/AdminValidateConversionIdConfigTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/GoogleAnalytics/Block/Ga.php b/app/code/Magento/GoogleAnalytics/Block/Ga.php
index 0370174c0b7f4..3be62a588efa2 100644
--- a/app/code/Magento/GoogleAnalytics/Block/Ga.php
+++ b/app/code/Magento/GoogleAnalytics/Block/Ga.php
@@ -82,6 +82,7 @@ public function getPageName()
* @link https://developers.google.com/analytics/devguides/collection/analyticsjs/method-reference#set
* @link https://developers.google.com/analytics/devguides/collection/analyticsjs/method-reference#gaObjectMethods
* @deprecated 100.2.0 please use getPageTrackingData method
+ * @see getPageTrackingData method
*/
public function getPageTrackingCode($accountId)
{
@@ -103,6 +104,7 @@ public function getPageTrackingCode($accountId)
*
* @return string|void
* @deprecated 100.2.0 please use getOrdersTrackingData method
+ * @see getOrdersTrackingData method
*/
public function getOrdersTrackingCode()
{
@@ -120,17 +122,19 @@ public function getOrdersTrackingCode()
foreach ($collection as $order) {
$result[] = "ga('set', 'currencyCode', '" . $order->getOrderCurrencyCode() . "');";
foreach ($order->getAllVisibleItems() as $item) {
+ $quantity = $item->getQtyOrdered() * 1;
+ $format = fmod($quantity, 1) !== 0.00 ? '%.2f' : '%d';
$result[] = sprintf(
"ga('ec:addProduct', {
'id': '%s',
'name': '%s',
- 'price': '%s',
- 'quantity': %s
+ 'price': %.2f,
+ 'quantity': $format
});",
$this->escapeJsQuote($item->getSku()),
$this->escapeJsQuote($item->getName()),
- $item->getPrice(),
- $item->getQtyOrdered()
+ (float)$item->getPrice(),
+ $quantity
);
}
@@ -138,15 +142,15 @@ public function getOrdersTrackingCode()
"ga('ec:setAction', 'purchase', {
'id': '%s',
'affiliation': '%s',
- 'revenue': '%s',
- 'tax': '%s',
- 'shipping': '%s'
+ 'revenue': %.2f,
+ 'tax': %.2f,
+ 'shipping': %.2f
});",
$order->getIncrementId(),
$this->escapeJsQuote($this->_storeManager->getStore()->getFrontendName()),
- $order->getGrandTotal(),
- $order->getTaxAmount(),
- $order->getShippingAmount()
+ (float)$order->getGrandTotal(),
+ (float)$order->getTaxAmount(),
+ (float)$order->getShippingAmount(),
);
$result[] = "ga('send', 'pageview');";
@@ -232,19 +236,20 @@ public function getOrdersTrackingData()
foreach ($collection as $order) {
foreach ($order->getAllVisibleItems() as $item) {
+ $quantity = $item->getQtyOrdered() * 1;
$result['products'][] = [
'id' => $this->escapeJsQuote($item->getSku()),
'name' => $this->escapeJsQuote($item->getName()),
- 'price' => $item->getPrice(),
- 'quantity' => $item->getQtyOrdered(),
+ 'price' => (float)$item->getPrice(),
+ 'quantity' => $quantity,
];
}
$result['orders'][] = [
'id' => $order->getIncrementId(),
'affiliation' => $this->escapeJsQuote($this->_storeManager->getStore()->getFrontendName()),
- 'revenue' => $order->getGrandTotal(),
- 'tax' => $order->getTaxAmount(),
- 'shipping' => $order->getShippingAmount(),
+ 'revenue' => (float)$order->getGrandTotal(),
+ 'tax' => (float)$order->getTaxAmount(),
+ 'shipping' => (float)$order->getShippingAmount(),
];
$result['currency'] = $order->getOrderCurrencyCode();
}
diff --git a/app/code/Magento/GoogleAnalytics/README.md b/app/code/Magento/GoogleAnalytics/README.md
index d4abd290bd665..226871406e241 100644
--- a/app/code/Magento/GoogleAnalytics/README.md
+++ b/app/code/Magento/GoogleAnalytics/README.md
@@ -21,6 +21,7 @@ A lot of functionality in the module is on JavaScript, use [mixins](https://deve
### Layouts
This module introduces the following layouts in the `view/frontend/layout` directory:
+
- `default`
For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/).
diff --git a/app/code/Magento/GoogleAnalytics/Test/Unit/Block/GaTest.php b/app/code/Magento/GoogleAnalytics/Test/Unit/Block/GaTest.php
index a367a938d45b9..8088b03707b2e 100644
--- a/app/code/Magento/GoogleAnalytics/Test/Unit/Block/GaTest.php
+++ b/app/code/Magento/GoogleAnalytics/Test/Unit/Block/GaTest.php
@@ -115,15 +115,21 @@ public function testOrderTrackingCode()
ga('ec:addProduct', {
'id': 'sku0',
'name': 'testName0',
- 'price': '0.00',
+ 'price': 0.00,
'quantity': 1
});
+ ga('ec:addProduct', {
+ 'id': 'sku1',
+ 'name': 'testName1',
+ 'price': 1.00,
+ 'quantity': 1.11
+ });
ga('ec:setAction', 'purchase', {
'id': '100',
'affiliation': 'test',
- 'revenue': '10',
- 'tax': '2',
- 'shipping': '1'
+ 'revenue': 10.00,
+ 'tax': 2.00,
+ 'shipping': 2.00
});
ga('send', 'pageview');";
@@ -163,9 +169,9 @@ public function testOrderTrackingData()
[
'id' => 100,
'affiliation' => 'test',
- 'revenue' => 10,
- 'tax' => 2,
- 'shipping' => 1
+ 'revenue' => 10.00,
+ 'tax' => 2.00,
+ 'shipping' => 2.0
]
],
'products' => [
@@ -174,6 +180,12 @@ public function testOrderTrackingData()
'name' => 'testName0',
'price' => 0.00,
'quantity' => 1
+ ],
+ [
+ 'id' => 'sku1',
+ 'name' => 'testName1',
+ 'price' => 1.00,
+ 'quantity' => 1.11
]
],
'currency' => 'USD'
@@ -204,7 +216,7 @@ public function testGetPageTrackingData()
* @param int $orderItemCount
* @return Order|MockObject
*/
- protected function createOrderMock($orderItemCount = 1)
+ protected function createOrderMock($orderItemCount = 2)
{
$orderItems = [];
for ($i = 0; $i < $orderItemCount; $i++) {
@@ -213,8 +225,8 @@ protected function createOrderMock($orderItemCount = 1)
->getMockForAbstractClass();
$orderItemMock->expects($this->once())->method('getSku')->willReturn('sku' . $i);
$orderItemMock->expects($this->once())->method('getName')->willReturn('testName' . $i);
- $orderItemMock->expects($this->once())->method('getPrice')->willReturn($i . '.00');
- $orderItemMock->expects($this->once())->method('getQtyOrdered')->willReturn($i + 1);
+ $orderItemMock->expects($this->once())->method('getPrice')->willReturn((float)($i . '.0000'));
+ $orderItemMock->expects($this->once())->method('getQtyOrdered')->willReturn($i == 1 ? 1.11 : $i + 1);
$orderItems[] = $orderItemMock;
}
@@ -223,9 +235,9 @@ protected function createOrderMock($orderItemCount = 1)
->getMock();
$orderMock->expects($this->once())->method('getIncrementId')->willReturn(100);
$orderMock->expects($this->once())->method('getAllVisibleItems')->willReturn($orderItems);
- $orderMock->expects($this->once())->method('getGrandTotal')->willReturn(10);
- $orderMock->expects($this->once())->method('getTaxAmount')->willReturn(2);
- $orderMock->expects($this->once())->method('getShippingAmount')->willReturn($orderItemCount);
+ $orderMock->expects($this->once())->method('getGrandTotal')->willReturn(10.00);
+ $orderMock->expects($this->once())->method('getTaxAmount')->willReturn(2.00);
+ $orderMock->expects($this->once())->method('getShippingAmount')->willReturn(round((float)$orderItemCount, 2));
$orderMock->expects($this->once())->method('getOrderCurrencyCode')->willReturn('USD');
return $orderMock;
}
@@ -241,7 +253,7 @@ protected function createCollectionMock()
$collectionMock->expects($this->any())
->method('getIterator')
- ->willReturn(new \ArrayIterator([$this->createOrderMock(1)]));
+ ->willReturn(new \ArrayIterator([$this->createOrderMock(2)]));
return $collectionMock;
}
diff --git a/app/code/Magento/GoogleGtag/README.md b/app/code/Magento/GoogleGtag/README.md
index 4d1a49ada70c7..d5985c308bbc2 100644
--- a/app/code/Magento/GoogleGtag/README.md
+++ b/app/code/Magento/GoogleGtag/README.md
@@ -21,6 +21,7 @@ A lot of functionality in the module is on JavaScript, use [mixins](https://deve
### Layouts
This module introduces the following layouts in the `view/frontend/layout` directory:
+
- `default`
- `checkout_onepage_success`
diff --git a/app/code/Magento/GoogleOptimizer/README.md b/app/code/Magento/GoogleOptimizer/README.md
index 5e493d69cf13b..2d2a32562f828 100644
--- a/app/code/Magento/GoogleOptimizer/README.md
+++ b/app/code/Magento/GoogleOptimizer/README.md
@@ -22,6 +22,7 @@ Extension developers can interact with the Magento_GoogleOptimizer module. For m
### Layouts
This module introduces the following layouts in the `view/frontend/layout` and `view/adminhtml/layout` directories:
+
- `view/adminhtml/layout`:
- `catalog_product_new`
- `cms_page_edit`
@@ -35,13 +36,14 @@ For more information about a layout in Magento 2, see the [Layout documentation]
### UI components
This module extends following ui components located in the `view/adminhtml/ui_component` directory:
+
- `category_form`
- `cms_page_form`
- `new_category_form`
For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/).
-## Additional information
+## Additional information
Google Experiment (on Google side) allows to make two variants of the same page and compare their popularity.
From Magento side, code generated by Google should be saved and displayed on a particular page.
diff --git a/app/code/Magento/GraphQl/Controller/GraphQl.php b/app/code/Magento/GraphQl/Controller/GraphQl.php
index f03079c89bc68..f20956407c258 100644
--- a/app/code/Magento/GraphQl/Controller/GraphQl.php
+++ b/app/code/Magento/GraphQl/Controller/GraphQl.php
@@ -20,6 +20,7 @@
use Magento\Framework\Controller\Result\JsonFactory;
use Magento\Framework\GraphQl\Exception\ExceptionFormatter;
use Magento\Framework\GraphQl\Query\Fields as QueryFields;
+use Magento\Framework\GraphQl\Query\QueryParser;
use Magento\Framework\GraphQl\Query\QueryProcessor;
use Magento\Framework\GraphQl\Query\Resolver\ContextInterface;
use Magento\Framework\GraphQl\Schema\SchemaGeneratorInterface;
@@ -41,6 +42,7 @@ class GraphQl implements FrontControllerInterface
/**
* @var \Magento\Framework\Webapi\Response
* @deprecated 100.3.2
+ * @see no replacement
*/
private $response;
@@ -66,7 +68,8 @@ class GraphQl implements FrontControllerInterface
/**
* @var ContextInterface
- * @deprecated 100.3.3 $contextFactory is used for creating Context object
+ * @deprecated 100.3.3
+ * @see $contextFactory is used for creating Context object
*/
private $resolverContext;
@@ -110,6 +113,11 @@ class GraphQl implements FrontControllerInterface
*/
private $areaList;
+ /**
+ * @var QueryParser
+ */
+ private $queryParser;
+
/**
* @param Response $response
* @param SchemaGeneratorInterface $schemaGenerator
@@ -125,6 +133,7 @@ class GraphQl implements FrontControllerInterface
* @param LogData|null $logDataHelper
* @param LoggerPool|null $loggerPool
* @param AreaList|null $areaList
+ * @param QueryParser|null $queryParser
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -141,7 +150,8 @@ public function __construct(
ContextFactoryInterface $contextFactory = null,
LogData $logDataHelper = null,
LoggerPool $loggerPool = null,
- AreaList $areaList = null
+ AreaList $areaList = null,
+ QueryParser $queryParser = null
) {
$this->response = $response;
$this->schemaGenerator = $schemaGenerator;
@@ -157,6 +167,7 @@ public function __construct(
$this->logDataHelper = $logDataHelper ?: ObjectManager::getInstance()->get(LogData::class);
$this->loggerPool = $loggerPool ?: ObjectManager::getInstance()->get(LoggerPool::class);
$this->areaList = $areaList ?: ObjectManager::getInstance()->get(AreaList::class);
+ $this->queryParser = $queryParser ?: ObjectManager::getInstance()->get(QueryParser::class);
}
/**
@@ -179,18 +190,18 @@ public function dispatch(RequestInterface $request): ResponseInterface
try {
/** @var Http $request */
$this->requestProcessor->validateRequest($request);
-
$query = $data['query'] ?? '';
- $variables = $data['variables'] ?? null;
+ $parsedQuery = $this->queryParser->parse($query);
+ $data['parsedQuery'] = $parsedQuery;
// We must extract queried field names to avoid instantiation of unnecessary fields in webonyx schema
// Temporal coupling is required for performance optimization
- $this->queryFields->setQuery($query, $variables);
+ $this->queryFields->setQuery($parsedQuery, $data['variables'] ?? null);
$schema = $this->schemaGenerator->generate();
$result = $this->queryProcessor->process(
$schema,
- $query,
+ $parsedQuery,
$this->contextFactory->create(),
$data['variables'] ?? []
);
diff --git a/app/code/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidator.php b/app/code/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidator.php
index d42676f5dd1b4..56351c7711cec 100644
--- a/app/code/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidator.php
+++ b/app/code/Magento/GraphQl/Controller/HttpRequestValidator/HttpVerbValidator.php
@@ -9,12 +9,12 @@
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
-use GraphQL\Language\Parser;
-use GraphQL\Language\Source;
use GraphQL\Language\Visitor;
use Magento\Framework\App\HttpRequestInterface;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\Request\Http;
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
+use Magento\Framework\GraphQl\Query\QueryParser;
use Magento\Framework\Phrase;
use Magento\GraphQl\Controller\HttpRequestValidatorInterface;
@@ -23,6 +23,19 @@
*/
class HttpVerbValidator implements HttpRequestValidatorInterface
{
+ /**
+ * @var QueryParser
+ */
+ private $queryParser;
+
+ /**
+ * @param QueryParser|null $queryParser
+ */
+ public function __construct(QueryParser $queryParser = null)
+ {
+ $this->queryParser = $queryParser ?: ObjectManager::getInstance()->get(QueryParser::class);
+ }
+
/**
* Check if request is using correct verb for query or mutation
*
@@ -37,9 +50,9 @@ public function validate(HttpRequestInterface $request): void
$query = $request->getParam('query', '');
if (!empty($query)) {
$operationType = '';
- $queryAst = Parser::parse(new Source($query ?: '', 'GraphQL'));
+ $parsedQuery = $this->queryParser->parse($query);
Visitor::visit(
- $queryAst,
+ $parsedQuery,
[
'leave' => [
NodeKind::OPERATION_DEFINITION => function (Node $node) use (&$operationType) {
diff --git a/app/code/Magento/GraphQl/Helper/Query/Logger/LogData.php b/app/code/Magento/GraphQl/Helper/Query/Logger/LogData.php
index 91e2518bfc634..fd45ef93cf13b 100644
--- a/app/code/Magento/GraphQl/Helper/Query/Logger/LogData.php
+++ b/app/code/Magento/GraphQl/Helper/Query/Logger/LogData.php
@@ -8,13 +8,14 @@
namespace Magento\GraphQl\Helper\Query\Logger;
use GraphQL\Error\SyntaxError;
+use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
-use GraphQL\Language\Parser;
-use GraphQL\Language\Source;
use GraphQL\Language\Visitor;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\App\Response\Http as HttpResponse;
+use Magento\Framework\GraphQl\Query\QueryParser;
use Magento\Framework\GraphQl\Schema;
use Magento\GraphQl\Model\Query\Logger\LoggerInterface;
@@ -23,6 +24,19 @@
*/
class LogData
{
+ /**
+ * @var QueryParser
+ */
+ private $queryParser;
+
+ /**
+ * @param QueryParser|null $queryParser
+ */
+ public function __construct(QueryParser $queryParser = null)
+ {
+ $this->queryParser = $queryParser ?: ObjectManager::getInstance()->get(QueryParser::class);
+ }
+
/**
* Extracts relevant information about the request
*
@@ -43,7 +57,7 @@ public function getLogData(
$logData = array_merge($logData, $this->gatherRequestInformation($request));
try {
- $complexity = $this->getFieldCount($data['query'] ?? '');
+ $complexity = $this->getFieldCount($data['parsedQuery'] ?? $data['query'] ?? '');
$logData[LoggerInterface::COMPLEXITY] = $complexity;
if ($schema) {
$logData = array_merge($logData, $this->gatherQueryInformation($schema));
@@ -114,18 +128,20 @@ private function gatherResponseInformation(HttpResponse $response) : array
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*
- * @param string $query
+ * @param DocumentNode|string $query
* @return int
* @throws SyntaxError
- * @throws /Exception
+ * @throws \Exception
*/
- private function getFieldCount(string $query): int
+ private function getFieldCount(DocumentNode|string $query): int
{
if (!empty($query)) {
$totalFieldCount = 0;
- $queryAst = Parser::parse(new Source($query ?: '', 'GraphQL'));
+ if (is_string($query)) {
+ $query = $this->queryParser->parse($query);
+ }
Visitor::visit(
- $queryAst,
+ $query,
[
'leave' => [
NodeKind::FIELD => function (Node $node) use (&$totalFieldCount) {
diff --git a/app/code/Magento/GraphQl/Model/Backpressure/BackpressureFieldValidator.php b/app/code/Magento/GraphQl/Model/Backpressure/BackpressureFieldValidator.php
index 5edbf4e207c91..c9f1c943a71e3 100644
--- a/app/code/Magento/GraphQl/Model/Backpressure/BackpressureFieldValidator.php
+++ b/app/code/Magento/GraphQl/Model/Backpressure/BackpressureFieldValidator.php
@@ -43,6 +43,7 @@ public function __construct(
/**
* Validate resolver args
*
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
* @param Field $field
* @param array $args
* @return void
diff --git a/app/code/Magento/GraphQl/Model/Query/ContextFactory.php b/app/code/Magento/GraphQl/Model/Query/ContextFactory.php
index d8fa03657e401..5eb03d4ed13d2 100644
--- a/app/code/Magento/GraphQl/Model/Query/ContextFactory.php
+++ b/app/code/Magento/GraphQl/Model/Query/ContextFactory.php
@@ -9,13 +9,15 @@
use Magento\Authorization\Model\UserContextInterface;
use Magento\Framework\Api\ExtensionAttributesFactory;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Framework\ObjectManagerInterface;
/**
* @inheritdoc
*/
-class ContextFactory implements ContextFactoryInterface
+class ContextFactory implements ContextFactoryInterface, ResetAfterRequestInterface
{
/**
* @var ExtensionAttributesFactory
@@ -58,15 +60,16 @@ public function __construct(
public function create(?UserContextInterface $userContext = null): ContextInterface
{
$contextParameters = $this->objectManager->create(ContextParametersInterface::class);
-
foreach ($this->contextParametersProcessors as $contextParametersProcessor) {
if (!$contextParametersProcessor instanceof ContextParametersProcessorInterface) {
throw new LocalizedException(
__('ContextParametersProcessors must implement %1', ContextParametersProcessorInterface::class)
);
}
- if ($userContext && $contextParametersProcessor instanceof UserContextParametersProcessorInterface) {
- $contextParametersProcessor->setUserContext($userContext);
+ if ($contextParametersProcessor instanceof UserContextParametersProcessorInterface) {
+ $contextParametersProcessor->setUserContext(
+ $userContext ?? $this->objectManager->create(UserContextInterface::class)
+ );
}
$contextParameters = $contextParametersProcessor->execute($contextParameters);
}
@@ -100,4 +103,12 @@ public function get(): ContextInterface
}
return $this->context;
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->context = null;
+ }
}
diff --git a/app/code/Magento/GraphQl/Model/Query/Logger/NewRelic.php b/app/code/Magento/GraphQl/Model/Query/Logger/NewRelic.php
index 55f25c176ed43..248797896389a 100644
--- a/app/code/Magento/GraphQl/Model/Query/Logger/NewRelic.php
+++ b/app/code/Magento/GraphQl/Model/Query/Logger/NewRelic.php
@@ -41,6 +41,9 @@ public function __construct(
*/
public function execute(array $queryDetails)
{
+ $transactionName = $queryDetails[LoggerInterface::OPERATION_NAMES] ?? '';
+ $this->newRelicWrapper->setTransactionName('GraphQL-' . $transactionName);
+
if (!$this->config->isNewRelicEnabled()) {
return;
}
@@ -48,9 +51,5 @@ public function execute(array $queryDetails)
foreach ($queryDetails as $key => $value) {
$this->newRelicWrapper->addCustomParameter($key, $value);
}
-
- $transactionName = $queryDetails[LoggerInterface::OPERATION_NAMES] ?: '';
-
- $this->newRelicWrapper->setTransactionName('GraphQL-' . $transactionName);
}
}
diff --git a/app/code/Magento/GraphQl/Model/Query/Resolver/Context.php b/app/code/Magento/GraphQl/Model/Query/Resolver/Context.php
index 9403ccaf07099..bae3ceabf2783 100644
--- a/app/code/Magento/GraphQl/Model/Query/Resolver/Context.php
+++ b/app/code/Magento/GraphQl/Model/Query/Resolver/Context.php
@@ -8,19 +8,21 @@
namespace Magento\GraphQl\Model\Query\Resolver;
use Magento\Framework\GraphQl\Query\Resolver\ContextInterface;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
/**
* Do not use this class. It was kept for backward compatibility.
*
- * @deprecated 100.3.3 \Magento\GraphQl\Model\Query\Context is used instead of this
+ * @deprecated 100.3.3
+ * @see \Magento\GraphQl\Model\Query\Context
*/
class Context extends \Magento\Framework\Model\AbstractExtensibleModel implements ContextInterface
{
/**#@+
* Constants defined for type of context
*/
- const USER_TYPE_ID = 'user_type';
- const USER_ID = 'user_id';
+ public const USER_TYPE_ID = 'user_type';
+ public const USER_ID = 'user_id';
/**#@-*/
/**
@@ -86,4 +88,12 @@ public function setUserType(int $typeId) : ContextInterface
{
return $this->setData(self::USER_TYPE_ID, $typeId);
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->_data = [];
+ }
}
diff --git a/app/code/Magento/GraphQl/README.md b/app/code/Magento/GraphQl/README.md
index de575fae59b5f..1372e5760e936 100644
--- a/app/code/Magento/GraphQl/README.md
+++ b/app/code/Magento/GraphQl/README.md
@@ -1,7 +1,7 @@
# Magento_GraphQl module
This module provides the framework for the application to expose GraphQL compliant web services. It exposes an area for
-GraphQL services and resolves request data based on the generated schema. It also maps this response to a JSON object
+GraphQL services and resolves request data based on the generated schema. It also maps this response to a JSON object
for the client to read.
## Installation
@@ -9,10 +9,12 @@ for the client to read.
The Magento_GraphQl module is one of the base Magento 2 modules. You cannot disable or uninstall this module.
This module is dependent on the following modules:
+
- `Magento_Authorization`
- `Magento_Eav`
The following modules depend on this module:
+
- `Magento_BundleGraphQl`
- `Magento_CatalogGraphQl`
- `Magento_CmsGraphQl`
@@ -35,4 +37,4 @@ Extension developers can interact with the Magento_GraphQl module. For more info
## Additional information
-You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql).
+You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/).
diff --git a/app/code/Magento/GraphQl/composer.json b/app/code/Magento/GraphQl/composer.json
index b81c3a924d4e5..af1fe042c6df5 100644
--- a/app/code/Magento/GraphQl/composer.json
+++ b/app/code/Magento/GraphQl/composer.json
@@ -9,7 +9,7 @@
"magento/module-webapi": "*",
"magento/module-new-relic-reporting": "*",
"magento/module-authorization": "*",
- "webonyx/graphql-php": "^14.11"
+ "webonyx/graphql-php": "^15.0"
},
"suggest": {
"magento/module-graph-ql-cache": "*"
diff --git a/app/code/Magento/GraphQl/etc/schema.graphqls b/app/code/Magento/GraphQl/etc/schema.graphqls
index 1ba190cd8bb22..67fb0d4e1b268 100644
--- a/app/code/Magento/GraphQl/etc/schema.graphqls
+++ b/app/code/Magento/GraphQl/etc/schema.graphqls
@@ -76,7 +76,7 @@ input FilterRangeTypeInput @doc(description: "Defines a filter that matches a ra
}
input FilterMatchTypeInput @doc(description: "Defines a filter that performs a fuzzy search.") {
- match: String @doc(description: "Use this attribute to exactly match the specified string. For example, to filter on a specific SKU, specify a value such as `24-MB01`.")
+ match: String @doc(description: "Use this attribute to fuzzy match the specified string. For example, to filter on a specific SKU, specify a value such as `24-MB01`.")
}
input FilterStringTypeInput @doc(description: "Defines a filter for an input string.") {
diff --git a/app/code/Magento/GraphQlCache/Controller/Plugin/GraphQl.php b/app/code/Magento/GraphQlCache/Controller/Plugin/GraphQl.php
index a594dcd6148f5..c4ce6cb4e7ec7 100644
--- a/app/code/Magento/GraphQlCache/Controller/Plugin/GraphQl.php
+++ b/app/code/Magento/GraphQlCache/Controller/Plugin/GraphQl.php
@@ -8,6 +8,7 @@
namespace Magento\GraphQlCache\Controller\Plugin;
use Magento\Framework\App\FrontControllerInterface;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\App\Response\Http as ResponseHttp;
use Magento\Framework\Controller\ResultInterface;
@@ -16,9 +17,11 @@
use Magento\GraphQlCache\Model\CacheableQuery;
use Magento\GraphQlCache\Model\CacheId\CacheIdCalculator;
use Magento\PageCache\Model\Config;
+use Psr\Log\LoggerInterface;
/**
* Plugin for handling controller after controller tags and pre-controller validation.
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class GraphQl
{
@@ -32,11 +35,6 @@ class GraphQl
*/
private $config;
- /**
- * @var ResponseHttp
- */
- private $response;
-
/**
* @var HttpRequestProcessor
*/
@@ -52,28 +50,37 @@ class GraphQl
*/
private $cacheIdCalculator;
+ /**
+ * @var LoggerInterface $logger
+ */
+ private $logger;
+
/**
* @param CacheableQuery $cacheableQuery
+ * @param CacheIdCalculator $cacheIdCalculator
* @param Config $config
- * @param ResponseHttp $response
+ * @param LoggerInterface $logger
* @param HttpRequestProcessor $requestProcessor
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ * @param ResponseHttp $response @deprecated do not use
* @param Registry $registry
- * @param CacheIdCalculator $cacheIdCalculator
*/
public function __construct(
CacheableQuery $cacheableQuery,
+ CacheIdCalculator $cacheIdCalculator,
Config $config,
- ResponseHttp $response,
+ LoggerInterface $logger,
HttpRequestProcessor $requestProcessor,
- Registry $registry,
- CacheIdCalculator $cacheIdCalculator
+ ResponseHttp $response,
+ Registry $registry = null
) {
$this->cacheableQuery = $cacheableQuery;
+ $this->cacheIdCalculator = $cacheIdCalculator;
$this->config = $config;
- $this->response = $response;
+ $this->logger = $logger;
$this->requestProcessor = $requestProcessor;
- $this->registry = $registry;
- $this->cacheIdCalculator = $cacheIdCalculator;
+ $this->registry = $registry ?: ObjectManager::getInstance()
+ ->get(Registry::class);
}
/**
@@ -87,7 +94,12 @@ public function __construct(
public function beforeDispatch(
FrontControllerInterface $subject,
RequestInterface $request
- ) {
+ ): void {
+ try {
+ $this->requestProcessor->validateRequest($request);
+ } catch (\Exception $error) {
+ $this->logger->critical($error->getMessage());
+ }
/** @var \Magento\Framework\App\Request\Http $request */
$this->requestProcessor->processHeaders($request);
}
@@ -109,26 +121,22 @@ public function afterRenderResult(ResultInterface $subject, ResultInterface $res
/** @see \Magento\Framework\App\Http::launch */
/** @see \Magento\PageCache\Model\Controller\Result\BuiltinPlugin::afterRenderResult */
$this->registry->register('use_page_cache_plugin', true, true);
-
$cacheId = $this->cacheIdCalculator->getCacheId();
if ($cacheId) {
- $this->response->setHeader(CacheIdCalculator::CACHE_ID_HEADER, $cacheId, true);
+ $response->setHeader(CacheIdCalculator::CACHE_ID_HEADER, $cacheId, true);
}
-
if ($this->cacheableQuery->shouldPopulateCacheHeadersWithTags()) {
- $this->response->setPublicHeaders($this->config->getTtl());
- $this->response->setHeader('X-Magento-Tags', implode(',', $this->cacheableQuery->getCacheTags()), true);
+ $response->setPublicHeaders($this->config->getTtl());
+ $response->setHeader('X-Magento-Tags', implode(',', $this->cacheableQuery->getCacheTags()), true);
} else {
$sendNoCacheHeaders = true;
}
} else {
$sendNoCacheHeaders = true;
}
-
if ($sendNoCacheHeaders) {
- $this->response->setNoCacheHeaders();
+ $response->setNoCacheHeaders();
}
-
return $result;
}
}
diff --git a/app/code/Magento/GraphQlCache/Model/CacheableQuery.php b/app/code/Magento/GraphQlCache/Model/CacheableQuery.php
index 451e1039eec57..da29ccb83bb75 100644
--- a/app/code/Magento/GraphQlCache/Model/CacheableQuery.php
+++ b/app/code/Magento/GraphQlCache/Model/CacheableQuery.php
@@ -7,10 +7,12 @@
namespace Magento\GraphQlCache\Model;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
+
/**
- * CacheableQuery should be used as a singleton for collecting cache related info and tags of all entities.
+ * CacheableQuery should be used as a singleton for collecting HTTP cache-related info and tags of all entities.
*/
-class CacheableQuery
+class CacheableQuery implements ResetAfterRequestInterface
{
/**
* @var string[]
@@ -40,11 +42,11 @@ public function getCacheTags(): array
*/
public function addCacheTags(array $cacheTags): void
{
- $this->cacheTags = array_merge($this->cacheTags, $cacheTags);
+ $this->cacheTags = array_unique(array_merge($this->cacheTags, $cacheTags));
}
/**
- * Return if its valid to cache the response
+ * Return if it's valid to cache the response
*
* @return bool
*/
@@ -54,7 +56,7 @@ public function isCacheable(): bool
}
/**
- * Set cache validity
+ * Set HTTP full page cache validity
*
* @param bool $cacheable
*/
@@ -71,7 +73,17 @@ public function setCacheValidity(bool $cacheable): void
public function shouldPopulateCacheHeadersWithTags() : bool
{
$cacheTags = $this->getCacheTags();
- $isQueryCaheable = $this->isCacheable();
- return !empty($cacheTags) && $isQueryCaheable;
+ $isQueryCacheable = $this->isCacheable();
+
+ return !empty($cacheTags) && $isQueryCacheable;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->cacheTags = [];
+ $this->cacheable = true;
}
}
diff --git a/app/code/Magento/GraphQlCache/Model/CacheableQueryHandler.php b/app/code/Magento/GraphQlCache/Model/CacheableQueryHandler.php
index 53f5155f8a3ac..d43fd26dc5f54 100644
--- a/app/code/Magento/GraphQlCache/Model/CacheableQueryHandler.php
+++ b/app/code/Magento/GraphQlCache/Model/CacheableQueryHandler.php
@@ -7,13 +7,12 @@
namespace Magento\GraphQlCache\Model;
-use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\App\Request\Http;
use Magento\GraphQlCache\Model\Resolver\IdentityPool;
/**
- * Handler of collecting tagging on cache.
+ * Handler for collecting tags on HTTP full page cache.
*
* This class would be used to collect tags after each operation where we need to collect tags
* usually after data is fetched or resolved.
@@ -51,7 +50,7 @@ public function __construct(
}
/**
- * Set cache validity to the cacheableQuery after resolving any resolver or evaluating a promise in a query
+ * Set HTTP full page cache validity on $cacheableQuery after resolving any resolver in a query
*
* @param array $resolvedValue
* @param array $cacheAnnotation Eg: ['cacheable' => true, 'cacheTag' => 'someTag', cacheIdentity=>'\Mage\Class']
@@ -69,11 +68,12 @@ public function handleCacheFromResolverResponse(array $resolvedValue, array $cac
} else {
$cacheable = false;
}
+
$this->setCacheValidity($cacheable);
}
/**
- * Set cache validity for the graphql request
+ * Set HTTP full page cache validity for the graphql request
*
* @param bool $isValid
* @return void
diff --git a/app/code/Magento/GraphQlCache/Model/Plugin/Query/Resolver.php b/app/code/Magento/GraphQlCache/Model/Plugin/Query/Resolver.php
index 10fe5739c461e..e5a703de3399e 100644
--- a/app/code/Magento/GraphQlCache/Model/Plugin/Query/Resolver.php
+++ b/app/code/Magento/GraphQlCache/Model/Plugin/Query/Resolver.php
@@ -13,7 +13,7 @@
use Magento\GraphQlCache\Model\CacheableQueryHandler;
/**
- * Plugin to handle cache validation that can be done after each resolver
+ * Plugin to handle HTTP cache validation that can be done after each resolver
*/
class Resolver
{
diff --git a/app/code/Magento/GraphQlCache/README.md b/app/code/Magento/GraphQlCache/README.md
index 32555f1423666..85e03391eb9ee 100644
--- a/app/code/Magento/GraphQlCache/README.md
+++ b/app/code/Magento/GraphQlCache/README.md
@@ -1,7 +1,7 @@
# Magento_GraphQlCache module
This module provides the ability to cache GraphQL queries.
-This module allows Magento built-in cache or Varnish as the application for serving the Full Page Cache to the front end.
+This module allows Magento built-in cache or Varnish as the application for serving the Full Page Cache to the front end.
## Installation
@@ -20,5 +20,5 @@ Extension developers can interact with the Magento_GraphQlCache module. For more
## Additional information
-- [Learn more about GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql).
+- [Learn more about GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/).
- [Learn more about GraphQl Caching In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/usage/caching/).
diff --git a/app/code/Magento/GraphQlCache/Test/Unit/Controller/Plugin/GraphQlTest.php b/app/code/Magento/GraphQlCache/Test/Unit/Controller/Plugin/GraphQlTest.php
new file mode 100644
index 0000000000000..dd45b3c715f9e
--- /dev/null
+++ b/app/code/Magento/GraphQlCache/Test/Unit/Controller/Plugin/GraphQlTest.php
@@ -0,0 +1,115 @@
+cacheableQueryMock = $this->createMock(CacheableQuery::class);
+ $this->cacheIdCalculatorMock = $this->createMock(CacheIdCalculator::class);
+ $this->configMock = $this->createMock(Config::class);
+ $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
+ ->onlyMethods(['critical'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->requestProcessorMock = $this->getMockBuilder(HttpRequestProcessor::class)
+ ->onlyMethods(['validateRequest','processHeaders'])
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+ $this->responseMock = $this->createMock(ResponseHttp::class);
+ $this->subjectMock = $this->createMock(FrontControllerInterface::class);
+ $this->requestMock = $this->createMock(Http::class);
+ $this->graphql = new GraphQl(
+ $this->cacheableQueryMock,
+ $this->cacheIdCalculatorMock,
+ $this->configMock,
+ $this->loggerMock,
+ $this->requestProcessorMock,
+ $this->responseMock
+ );
+ }
+
+ /**
+ * test beforeDispatch function for validation purpose
+ */
+ public function testBeforeDispatch(): void
+ {
+ $this->requestProcessorMock
+ ->expects($this->any())
+ ->method('validateRequest');
+ $this->requestProcessorMock
+ ->expects($this->any())
+ ->method('processHeaders');
+ $this->loggerMock
+ ->expects($this->any())
+ ->method('critical');
+ $this->assertNull($this->graphql->beforeDispatch($this->subjectMock, $this->requestMock));
+ }
+}
diff --git a/app/code/Magento/GraphQlCache/etc/graphql/di.xml b/app/code/Magento/GraphQlCache/etc/graphql/di.xml
index 1270ba24c94bb..1a85f02b5be9c 100644
--- a/app/code/Magento/GraphQlCache/etc/graphql/di.xml
+++ b/app/code/Magento/GraphQlCache/etc/graphql/di.xml
@@ -12,7 +12,7 @@
-
+
graphQlResolverCache = $graphQlResolverCache;
+ $this->serializer = $serializer;
+ $this->cacheState = $cacheState;
+ $this->resolverIdentityClassProvider = $resolverIdentityClassProvider;
+ $this->valueProcessor = $valueProcessor;
+ $this->keyCalculatorProvider = $keyCalculatorProvider;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Checks for cacheability of resolver's data, and, if cacheable, loads and persists cache entry for future use
+ *
+ * @param ResolverInterface $subject
+ * @param \Closure $proceed
+ * @param Field $field
+ * @param ContextInterface $context
+ * @param ResolveInfo $info
+ * @param array|null $value
+ * @param array|null $args
+ * @return mixed|Value
+ */
+ public function aroundResolve(
+ ResolverInterface $subject,
+ \Closure $proceed,
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ // even though a frontend access proxy is used to prevent saving/loading in $graphQlResolverCache when it is
+ // disabled, it's best to return as early as possible to avoid unnecessary processing
+ if (!$this->cacheState->isEnabled(GraphQlResolverCache::TYPE_IDENTIFIER)
+ || $info->operation->operation !== 'query'
+ ) {
+ return $proceed($field, $context, $info, $value, $args);
+ }
+
+ $identityProvider = $this->resolverIdentityClassProvider->getIdentityFromResolver($subject);
+
+ if (!$identityProvider) { // not cacheable; proceed
+ return $this->executeResolver($proceed, $field, $context, $info, $value, $args);
+ }
+
+ // Cache key provider may base cache key on the parent resolver value
+ // $value is processed on key calculation if needed
+ try {
+ $cacheKey = $this->prepareCacheIdentifier($subject, $args, $value);
+ } catch (CalculationException $e) {
+ $this->logger->warning(
+ sprintf(
+ "Unable to obtain cache key for %s resolver results, proceeding to invoke resolver."
+ . "Original exception message: %s ",
+ get_class($subject),
+ $e->getMessage()
+ )
+ );
+ return $this->executeResolver($proceed, $field, $context, $info, $value, $args);
+ }
+
+ $cachedResult = $this->graphQlResolverCache->load($cacheKey);
+
+ if ($cachedResult !== false) {
+ $returnValue = $this->serializer->unserialize($cachedResult);
+ $this->valueProcessor->processCachedValueAfterLoad($info, $subject, $cacheKey, $returnValue);
+ return $returnValue;
+ }
+
+ $returnValue = $this->executeResolver($proceed, $field, $context, $info, $value, $args);
+
+ // $value (parent value) is preprocessed (hydrated) on the previous step
+ $identities = $identityProvider->getIdentities($returnValue, $value);
+
+ if (count($identities)) {
+ $cachedValue = $returnValue;
+ $this->valueProcessor->preProcessValueBeforeCacheSave($subject, $cachedValue);
+ $this->graphQlResolverCache->save(
+ $this->serializer->serialize($cachedValue),
+ $cacheKey,
+ $identities,
+ false // use default lifetime directive
+ );
+ unset($cachedValue);
+ }
+
+ return $returnValue;
+ }
+
+ /**
+ * Call proceed method with context.
+ *
+ * @param \Closure $closure
+ * @param Field $field
+ * @param ContextInterface $context
+ * @param ResolveInfo $info
+ * @param array|null $value
+ * @param array|null $args
+ * @return mixed
+ */
+ private function executeResolver(
+ \Closure $closure,
+ Field $field,
+ ContextInterface $context,
+ ResolveInfo $info,
+ array &$value = null,
+ array $args = null
+ ) {
+ if (is_array($value)) {
+ $this->valueProcessor->preProcessParentValue($value);
+ }
+ return $closure($field, $context, $info, $value, $args);
+ }
+
+ /**
+ * Generate cache key incorporating factors from parameters.
+ *
+ * @param ResolverInterface $resolver
+ * @param array|null $args
+ * @param array|null $value
+ *
+ * @return string
+ * @throws CalculationException
+ */
+ private function prepareCacheIdentifier(
+ ResolverInterface $resolver,
+ ?array $args,
+ ?array $value
+ ): string {
+ $queryPayloadHash = sha1(get_class($resolver) . $this->serializer->serialize($args ?? []));
+
+ return GraphQlResolverCache::CACHE_TAG
+ . '_'
+ . $this->keyCalculatorProvider->getKeyCalculatorForResolver($resolver)->calculateCacheKey($value)
+ . '_'
+ . $queryPayloadHash;
+ }
+}
diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/Cache/IdentityInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/Cache/IdentityInterface.php
new file mode 100644
index 0000000000000..659967a2a7ff5
--- /dev/null
+++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/Cache/IdentityInterface.php
@@ -0,0 +1,26 @@
+contextFactory = $contextFactory;
+ $this->factorProviders = $factorProviders;
+ $this->objectManager = $objectManager;
+ $this->valueProcessor = $valueProcessor;
+ }
+
+ /**
+ * Calculates the value of resolver cache identifier.
+ *
+ * @param array|null $parentData
+ *
+ * @return string|null
+ *
+ * @throws CalculationException
+ */
+ public function calculateCacheKey(?array $parentData = null): ?string
+ {
+ if (!$this->factorProviders) {
+ return null;
+ }
+ try {
+ $this->initializeFactorProviderInstances();
+ $factors = $this->getFactors($parentData);
+ $keysString = strtoupper(implode('|', array_values($factors)));
+ return hash('sha256', $keysString);
+ } catch (\Throwable $e) {
+ throw new CalculationException($e->getMessage(), $e->getCode(), $e);
+ }
+ }
+
+ /**
+ * Get key factors from parent data for current context.
+ *
+ * @param array|null $parentData
+ * @return array
+ */
+ private function getFactors(?array $parentData): array
+ {
+ $factors = [];
+ $context = $this->contextFactory->get();
+ foreach ($this->factorProviderInstances as $factorProvider) {
+ if ($factorProvider instanceof ParentValueFactorProviderInterface && is_array($parentData)) {
+ // preprocess data if the data was fetched from cache and has reference key
+ // and the factorProvider expects processed data (original data from resolver)
+ if (isset($parentData[ValueProcessorInterface::VALUE_PROCESSING_REFERENCE_KEY])
+ && $factorProvider->isRequiredOrigData()
+ ) {
+ $this->valueProcessor->preProcessParentValue($parentData);
+ }
+ // fetch factor value considering parent data
+ $factors[$factorProvider->getFactorName()] = $factorProvider->getFactorValue(
+ $context,
+ $parentData
+ );
+ } else {
+ // get factor value considering only context
+ $factors[$factorProvider->getFactorName()] = $factorProvider->getFactorValue(
+ $context
+ );
+ }
+ }
+ ksort($factors);
+ return $factors;
+ }
+
+ /**
+ * Initialize instances of factor providers.
+ *
+ * @return void
+ */
+ private function initializeFactorProviderInstances(): void
+ {
+ if (empty($this->factorProviderInstances) && !empty($this->factorProviders)) {
+ foreach ($this->factorProviders as $factorProviderClass) {
+ $this->factorProviderInstances[] = $this->objectManager->get($factorProviderClass);
+ }
+ }
+ }
+}
diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator/Provider.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator/Provider.php
new file mode 100644
index 0000000000000..994b9570148e7
--- /dev/null
+++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator/Provider.php
@@ -0,0 +1,129 @@
+objectManager = $objectManager;
+ $this->customFactorProviders = $customFactorProviders;
+ }
+
+ /**
+ * Initialize custom cache key calculator for the given resolver.
+ *
+ * @param ResolverInterface $resolver
+ *
+ * @return void
+ */
+ private function initForResolver(ResolverInterface $resolver): void
+ {
+ $resolverClass = trim(get_class($resolver), '\\');
+ if (isset($this->keyCalculatorInstances[$resolverClass])) {
+ return;
+ }
+ $customKeyFactorProviders = $this->getCustomFactorProvidersForResolver($resolver);
+ if (empty($customKeyFactorProviders)) {
+ $this->keyCalculatorInstances[$resolverClass] = $this->objectManager->get(Calculator::class);
+ } else {
+ $runtimePoolKey = $this->generateCustomProvidersKey($customKeyFactorProviders);
+ if (!isset($this->keyCalculatorInstances[$runtimePoolKey])) {
+ $this->keyCalculatorInstances[$runtimePoolKey] = $this->objectManager->create(
+ Calculator::class,
+ ['factorProviders' => $customKeyFactorProviders]
+ );
+ }
+ $this->keyCalculatorInstances[$resolverClass] = $this->keyCalculatorInstances[$runtimePoolKey];
+ }
+ }
+
+ /**
+ * Generate runtime pool key from the set of custom providers.
+ *
+ * @param array $customProviders
+ * @return string
+ */
+ private function generateCustomProvidersKey(array $customProviders): string
+ {
+ $keyArray = array_keys($customProviders);
+ sort($keyArray);
+ return implode('_', $keyArray);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getKeyCalculatorForResolver(ResolverInterface $resolver): Calculator
+ {
+ $resolverClass = trim(get_class($resolver), '\\');
+ if (!isset($this->keyCalculatorInstances[$resolverClass])) {
+ $this->initForResolver($resolver);
+ }
+ return $this->keyCalculatorInstances[$resolverClass];
+ }
+
+ /**
+ * Get class inheritance chain for the given resolver object.
+ *
+ * @param ResolverInterface $resolver
+ * @return array
+ */
+ private function getResolverClassChain(ResolverInterface $resolver): array
+ {
+ $resolverClasses = [trim(get_class($resolver), '\\')];
+ foreach (class_parents($resolver) as $classParent) {
+ $resolverClasses[] = trim($classParent, '\\');
+ }
+ return $resolverClasses;
+ }
+
+ /**
+ * Get custom cache key factor providers for the given resolver object.
+ *
+ * @param ResolverInterface $resolver
+ * @return array
+ */
+ private function getCustomFactorProvidersForResolver(ResolverInterface $resolver): array
+ {
+ foreach ($this->getResolverClassChain($resolver) as $resolverClass) {
+ if (!empty($this->customFactorProviders[$resolverClass])) {
+ return $this->customFactorProviders[$resolverClass];
+ }
+ }
+ return [];
+ }
+}
diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator/ProviderInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator/ProviderInterface.php
new file mode 100644
index 0000000000000..d7efbf45d32cf
--- /dev/null
+++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/CacheKey/Calculator/ProviderInterface.php
@@ -0,0 +1,25 @@
+dehydrators = $dehydrators;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function dehydrate(array &$resolvedValue): void
+ {
+ if (empty($resolvedValue)) {
+ return;
+ }
+ foreach ($this->dehydrators as $dehydrator) {
+ $dehydrator->dehydrate($resolvedValue);
+ }
+ }
+}
diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/DehydratorInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/DehydratorInterface.php
new file mode 100644
index 0000000000000..2616bdb0e2137
--- /dev/null
+++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/DehydratorInterface.php
@@ -0,0 +1,22 @@
+hydrators = $hydrators;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function hydrate(array &$resolverData): void
+ {
+ if (empty($resolverData)) {
+ return;
+ }
+ foreach ($this->hydrators as $hydrator) {
+ $hydrator->hydrate($resolverData);
+ }
+ }
+}
diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorDehydratorProvider.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorDehydratorProvider.php
new file mode 100644
index 0000000000000..a8e4cee7f8705
--- /dev/null
+++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorDehydratorProvider.php
@@ -0,0 +1,215 @@
+objectManager = $objectManager;
+ $this->dehydratorConfig = $dehydratorConfig;
+ $this->hydratorConfig = $hydratorConfig;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getDehydratorForResolver(ResolverInterface $resolver): ?DehydratorInterface
+ {
+ $resolverClass = $this->getResolverClass($resolver);
+ if (array_key_exists($resolverClass, $this->dehydratorInstances)) {
+ return $this->dehydratorInstances[$resolverClass];
+ }
+ $resolverDehydrators = $this->getInstancesForResolver(
+ $resolver,
+ $this->dehydratorConfig,
+ DehydratorInterface::class
+ );
+ if (empty($resolverDehydrators)) {
+ $this->dehydratorInstances[$resolverClass] = null;
+ } else {
+ $this->dehydratorInstances[$resolverClass] = $this->objectManager->create(
+ DehydratorComposite::class,
+ [
+ 'dehydrators' => $resolverDehydrators
+ ]
+ );
+ }
+ return $this->dehydratorInstances[$resolverClass];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getHydratorForResolver(ResolverInterface $resolver): ?HydratorInterface
+ {
+ $resolverClass = $this->getResolverClass($resolver);
+ if (array_key_exists($resolverClass, $this->hydratorInstances)) {
+ return $this->hydratorInstances[$resolverClass];
+ }
+ $resolverHydrators = $this->getInstancesForResolver(
+ $resolver,
+ $this->hydratorConfig,
+ HydratorInterface::class
+ );
+ if (empty($resolverHydrators)) {
+ $this->hydratorInstances[$resolverClass] = null;
+ } else {
+ $this->hydratorInstances[$resolverClass] = $this->objectManager->create(
+ HydratorComposite::class,
+ [
+ 'hydrators' => $resolverHydrators
+ ]
+ );
+ }
+ return $this->hydratorInstances[$resolverClass];
+ }
+
+ /**
+ * Get resolver instance class name.
+ *
+ * @param ResolverInterface $resolver
+ * @return string
+ */
+ private function getResolverClass(ResolverInterface $resolver): string
+ {
+ return trim(get_class($resolver), '\\');
+ }
+
+ /**
+ * Get hydrator or dehydrator instances for the given resolver from given configuration.
+ *
+ * @param ResolverInterface $resolver
+ * @param array $classesConfig
+ * @param string $interfaceName
+ * @return array
+ * @throws ConfigurationMismatchException
+ */
+ private function getInstancesForResolver(
+ ResolverInterface $resolver,
+ array $classesConfig,
+ string $interfaceName
+ ): array {
+ $resolverClassesConfig = [];
+ foreach ($this->getResolverClassChain($resolver) as $resolverClass) {
+ if (isset($classesConfig[$resolverClass])) {
+ $resolverClassesConfig[$resolverClass] = $classesConfig[$resolverClass];
+ }
+ }
+ if (empty($resolverClassesConfig)) {
+ return [];
+ }
+ $dataProcessingClassList = [];
+ foreach ($resolverClassesConfig as $resolverClass => $classChain) {
+ $this->validateClassChain($classChain, $interfaceName, $resolverClass);
+ foreach ($classChain as $classData) {
+ $dataProcessingClassList[] = $classData;
+ }
+ }
+ usort($dataProcessingClassList, function ($data1, $data2) {
+ return ((int)$data1['sortOrder'] > (int)$data2['sortOrder']) ? 1 : -1;
+ });
+ $dataProcessingInstances = [];
+ foreach ($dataProcessingClassList as $classData) {
+ $dataProcessingInstances[] = $this->objectManager->get($classData['class']);
+ }
+ return $dataProcessingInstances;
+ }
+
+ /**
+ * Validate hydrator or dehydrator classes and throw exception if class does not implement relevant interface.
+ *
+ * @param array $classChain
+ * @param string $interfaceName
+ * @param string $resolverClass
+ * @return void
+ * @throws ConfigurationMismatchException
+ */
+ private function validateClassChain(array $classChain, string $interfaceName, string $resolverClass)
+ {
+ foreach ($classChain as $classData) {
+ if (!is_a($classData['class'], $interfaceName, true)) {
+ if ($interfaceName == HydratorInterface::class) {
+ throw new ConfigurationMismatchException(
+ __(
+ 'Hydrator %1 configured for resolver %2 must implement %3.',
+ $classData['class'],
+ $resolverClass,
+ $interfaceName
+ )
+ );
+ } else {
+ throw new ConfigurationMismatchException(
+ __(
+ 'Dehydrator %1 configured for resolver %2 must implement %3.',
+ $classData['class'],
+ $resolverClass,
+ $interfaceName
+ )
+ );
+ }
+
+ }
+ }
+ }
+
+ /**
+ * Get class inheritance chain for the given resolver object.
+ *
+ * @param ResolverInterface $resolver
+ * @return array
+ */
+ private function getResolverClassChain(ResolverInterface $resolver): array
+ {
+ $resolverClasses = [trim(get_class($resolver), '\\')];
+ foreach (class_parents($resolver) as $classParent) {
+ $resolverClasses[] = trim($classParent, '\\');
+ }
+ return $resolverClasses;
+ }
+}
diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorInterface.php
new file mode 100644
index 0000000000000..95660f6ed5771
--- /dev/null
+++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/HydratorInterface.php
@@ -0,0 +1,22 @@
+ Identity Provider
+ *
+ * @var string[]
+ */
+ private array $cacheableResolverClassNameIdentityMap;
+
+ /**
+ * @param ObjectManagerInterface $objectManager
+ * @param array $cacheableResolverClassNameIdentityMap
+ */
+ public function __construct(
+ ObjectManagerInterface $objectManager,
+ array $cacheableResolverClassNameIdentityMap
+ ) {
+ $this->objectManager = $objectManager;
+ $this->cacheableResolverClassNameIdentityMap = $cacheableResolverClassNameIdentityMap;
+ }
+
+ /**
+ * Get Identity provider based on $resolver instance.
+ *
+ * @param ResolverInterface $resolver
+ * @return Cache\IdentityInterface|null
+ */
+ public function getIdentityFromResolver(ResolverInterface $resolver): ?Cache\IdentityInterface
+ {
+ $matchingIdentityProviderClassName = null;
+
+ foreach ($this->cacheableResolverClassNameIdentityMap as $resolverClassName => $identityProviderClassName) {
+ if ($resolver instanceof $resolverClassName) {
+ $matchingIdentityProviderClassName = $identityProviderClassName;
+ break;
+ }
+ }
+
+ if (!$matchingIdentityProviderClassName) {
+ return null;
+ }
+
+ return $this->objectManager->get($matchingIdentityProviderClassName);
+ }
+}
diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/TagResolver.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/TagResolver.php
new file mode 100644
index 0000000000000..19e650a315c76
--- /dev/null
+++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/TagResolver.php
@@ -0,0 +1,56 @@
+invalidatableObjectTypes = $invalidatableObjectTypes;
+
+ parent::__construct($factory);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getTags($object)
+ {
+ $isInvalidatable = false;
+
+ foreach ($this->invalidatableObjectTypes as $invalidatableObjectType) {
+ $isInvalidatable = $object instanceof $invalidatableObjectType;
+
+ if ($isInvalidatable) {
+ break;
+ }
+ }
+
+ if (!$isInvalidatable) {
+ return [];
+ }
+
+ return parent::getTags($object);
+ }
+}
diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/Type.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/Type.php
new file mode 100644
index 0000000000000..950ec9baeb417
--- /dev/null
+++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/Type.php
@@ -0,0 +1,32 @@
+get(self::TYPE_IDENTIFIER), self::CACHE_TAG);
+ }
+}
diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor.php
new file mode 100644
index 0000000000000..f95dee244c958
--- /dev/null
+++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor.php
@@ -0,0 +1,168 @@
+hydratorProvider = $hydratorProvider;
+ $this->dehydratorProvider = $dehydratorProvider;
+ $this->typeConfig = $typeConfig;
+ $this->objectManager = $objectManager;
+ $this->defaultFlagGetter = $defaultFlagGetter;
+ $this->defaultFlagSetter = $defaultFlagSetter;
+ }
+
+ /**
+ * Get flag setter for the resolver return type.
+ *
+ * @param ResolveInfo $info
+ * @return FlagSetterInterface
+ */
+ private function getFlagSetterForType(ResolveInfo $info): FlagSetterInterface
+ {
+ if (isset($this->typeConfig['setters'][get_class($info->returnType)])) {
+ return $this->objectManager->get(
+ $this->typeConfig['setters'][get_class($info->returnType)]
+ );
+ }
+ return $this->defaultFlagSetter;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function processCachedValueAfterLoad(
+ ResolveInfo $info,
+ ResolverInterface $resolver,
+ string $cacheKey,
+ &$value
+ ): void {
+ if ($value === null) {
+ return;
+ }
+ $hydrator = $this->hydratorProvider->getHydratorForResolver($resolver);
+ if ($hydrator) {
+ $this->hydrators[$cacheKey] = $hydrator;
+ $this->getFlagSetterForType($info)->setFlagOnValue($value, $cacheKey);
+ }
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function preProcessParentValue(array &$value): void
+ {
+ $this->hydrateData($value);
+ }
+
+ /**
+ * Perform data hydration.
+ *
+ * @param array|null $value
+ * @return void
+ */
+ private function hydrateData(&$value)
+ {
+ if ($value === null) {
+ return;
+ }
+ // the parent value is always a single object that contains currently resolved value
+ $reference = $this->defaultFlagGetter->getFlagFromValue($value) ?? null;
+ if (isset($reference['cacheKey']) && isset($reference['index'])) {
+ $cacheKey = $reference['cacheKey'];
+ $index = $reference['index'];
+ if ($cacheKey) {
+ if (isset($this->processedValues[$cacheKey][$index])) {
+ $value = $this->processedValues[$cacheKey][$index];
+ } elseif (isset($this->hydrators[$cacheKey])
+ && $this->hydrators[$cacheKey] instanceof HydratorInterface
+ ) {
+ $this->hydrators[$cacheKey]->hydrate($value);
+ $this->defaultFlagSetter->unsetFlagFromValue($value);
+ $this->processedValues[$cacheKey][$index] = $value;
+ }
+ }
+ }
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function preProcessValueBeforeCacheSave(ResolverInterface $resolver, &$value): void
+ {
+ $dehydrator = $this->dehydratorProvider->getDehydratorForResolver($resolver);
+ if ($dehydrator) {
+ $dehydrator->dehydrate($value);
+ }
+ }
+}
diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagGetter/FlagGetterInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagGetter/FlagGetterInterface.php
new file mode 100644
index 0000000000000..82d45f3ccb32d
--- /dev/null
+++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagGetter/FlagGetterInterface.php
@@ -0,0 +1,22 @@
+ $flagValue,
+ 'index' => $key
+ ];
+ }
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function unsetFlagFromValue(&$value): void
+ {
+ foreach (array_keys($value) as $key) {
+ unset($value[$key][ValueProcessorInterface::VALUE_PROCESSING_REFERENCE_KEY]);
+ }
+ }
+}
diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagSetter/SingleObject.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagSetter/SingleObject.php
new file mode 100644
index 0000000000000..bd860cd5cde18
--- /dev/null
+++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessor/FlagSetter/SingleObject.php
@@ -0,0 +1,35 @@
+ $flagValue,
+ 'index' => 0
+ ];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function unsetFlagFromValue(&$value): void
+ {
+ unset($value[ValueProcessorInterface::VALUE_PROCESSING_REFERENCE_KEY]);
+ }
+}
diff --git a/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessorInterface.php b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessorInterface.php
new file mode 100644
index 0000000000000..f2ce961f312d0
--- /dev/null
+++ b/app/code/Magento/GraphQlResolverCache/Model/Resolver/Result/ValueProcessorInterface.php
@@ -0,0 +1,55 @@
+graphQlResolverCache = $graphQlResolverCache;
+ $this->cacheState = $cacheState;
+ $this->tagResolver = $tagResolver;
+ }
+
+ /**
+ * Clean identities of event object from GraphQL Resolver cache
+ *
+ * @param Observer $observer
+ *
+ * @return void
+ */
+ public function execute(Observer $observer)
+ {
+ $object = $observer->getEvent()->getObject();
+
+ if (!is_object($object)) {
+ return;
+ }
+
+ if (!$this->cacheState->isEnabled(GraphQlResolverCache::TYPE_IDENTIFIER)) {
+ return;
+ }
+
+ $tags = $this->tagResolver->getTags($object);
+
+ if (!empty($tags)) {
+ $this->graphQlResolverCache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, $tags);
+ }
+ }
+}
diff --git a/app/code/Magento/GraphQlResolverCache/README.md b/app/code/Magento/GraphQlResolverCache/README.md
new file mode 100644
index 0000000000000..e101723035b86
--- /dev/null
+++ b/app/code/Magento/GraphQlResolverCache/README.md
@@ -0,0 +1,21 @@
+# Magento_GraphQlResolverCache module
+
+This module provides the ability to granular cache GraphQL resolver results on resolver level.
+
+## Installation
+
+Before installing this module, note that the Magento_GraphQlResolverCache module is dependent on the following modules:
+
+- `Magento_GraphQl`
+
+For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html).
+
+## Extensibility
+
+Extension developers can interact with the Magento_GraphQlResolverCache module. For more information about the Magento extension mechanism, see [Magento plugins](https://developer.adobe.com/commerce/php/development/components/plugins/).
+
+[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_GraphQlCache module.
+
+## Additional information
+
+- [Learn more about GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/).
diff --git a/app/code/Magento/GraphQlResolverCache/composer.json b/app/code/Magento/GraphQlResolverCache/composer.json
new file mode 100644
index 0000000000000..d73b69c86e707
--- /dev/null
+++ b/app/code/Magento/GraphQlResolverCache/composer.json
@@ -0,0 +1,22 @@
+{
+ "name": "magento/module-graph-ql-resolver-cache",
+ "description": "N/A",
+ "type": "magento2-module",
+ "require": {
+ "php": "~8.1.0||~8.2.0",
+ "magento/framework": "*",
+ "magento/module-graph-ql": "*"
+ },
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Magento\\GraphQlResolverCache\\": ""
+ }
+ }
+}
diff --git a/app/code/Magento/GraphQlResolverCache/etc/cache.xml b/app/code/Magento/GraphQlResolverCache/etc/cache.xml
new file mode 100644
index 0000000000000..92667d350167a
--- /dev/null
+++ b/app/code/Magento/GraphQlResolverCache/etc/cache.xml
@@ -0,0 +1,13 @@
+
+
+
+
+ GraphQL Query Resolver Results
+ Results from resolvers in GraphQL queries
+
+
diff --git a/app/code/Magento/GraphQlResolverCache/etc/events.xml b/app/code/Magento/GraphQlResolverCache/etc/events.xml
new file mode 100644
index 0000000000000..5633cd8b713de
--- /dev/null
+++ b/app/code/Magento/GraphQlResolverCache/etc/events.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/app/code/Magento/GraphQlResolverCache/etc/graphql/di.xml b/app/code/Magento/GraphQlResolverCache/etc/graphql/di.xml
new file mode 100644
index 0000000000000..060abbe09cf6f
--- /dev/null
+++ b/app/code/Magento/GraphQlResolverCache/etc/graphql/di.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
- Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessor\FlagSetter\ListOfObjects
+
+
+
+
+
diff --git a/app/code/Magento/Elasticsearch8/etc/module.xml b/app/code/Magento/GraphQlResolverCache/etc/module.xml
similarity index 69%
rename from app/code/Magento/Elasticsearch8/etc/module.xml
rename to app/code/Magento/GraphQlResolverCache/etc/module.xml
index 32ea0b381b767..6639cd1c7f909 100644
--- a/app/code/Magento/Elasticsearch8/etc/module.xml
+++ b/app/code/Magento/GraphQlResolverCache/etc/module.xml
@@ -6,10 +6,9 @@
*/
-->
-
+
-
-
+
diff --git a/app/code/Magento/GraphQlResolverCache/i18n/en_US.csv b/app/code/Magento/GraphQlResolverCache/i18n/en_US.csv
new file mode 100644
index 0000000000000..db3844a297f26
--- /dev/null
+++ b/app/code/Magento/GraphQlResolverCache/i18n/en_US.csv
@@ -0,0 +1,4 @@
+"GraphQL Query Resolver Results","GraphQL Query Resolver Results"
+"Results from resolvers in GraphQL queries","Results from resolvers in GraphQL queries"
+"Hydrator %1 configured for resolver %2 must implement %3.","Hydrator %1 configured for resolver %2 must implement %3."
+"Deydrator %1 configured for resolver %2 must implement %3.","Dehydrator %1 configured for resolver %2 must implement %3."
diff --git a/app/code/Magento/GraphQlResolverCache/registration.php b/app/code/Magento/GraphQlResolverCache/registration.php
new file mode 100644
index 0000000000000..e091fa98baaff
--- /dev/null
+++ b/app/code/Magento/GraphQlResolverCache/registration.php
@@ -0,0 +1,10 @@
+
+
diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductNonDefaultAttributeSetTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductNonDefaultAttributeSetTest.xml
index d5dcd7f48b956..456982014c609 100644
--- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductNonDefaultAttributeSetTest.xml
+++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductNonDefaultAttributeSetTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductTest.xml
index 55144884c7195..610554d0d0f8c 100644
--- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductTest.xml
+++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminCreateGroupedProductTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml
index 68f9da93ec992..2980a6cefa54c 100644
--- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml
+++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.xml
index ef1665d965200..16c4c29c18a69 100644
--- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.xml
+++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedSetEditRelatedProductsTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedSetEditRelatedProductsTest.xml
index 8d808cd07a875..c1ef2864bb8be 100644
--- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedSetEditRelatedProductsTest.xml
+++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedSetEditRelatedProductsTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultImageGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultImageGroupedProductTest.xml
index 0dc622a82aaae..9d33ca36bd48d 100644
--- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultImageGroupedProductTest.xml
+++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultImageGroupedProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductBySkuTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductBySkuTest.xml
index b51b14f254099..cbef10f3da280 100644
--- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductBySkuTest.xml
+++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductBySkuTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/GroupedProduct/Test/Unit/Model/ProductTest.php b/app/code/Magento/GroupedProduct/Test/Unit/Model/ProductTest.php
index f31b3a3db9d8a..94331fc65278c 100644
--- a/app/code/Magento/GroupedProduct/Test/Unit/Model/ProductTest.php
+++ b/app/code/Magento/GroupedProduct/Test/Unit/Model/ProductTest.php
@@ -349,7 +349,7 @@ protected function setUp(): void
*/
public function testGetProductLinks(): void
{
- $this->markTestIncomplete('Skipped due to https://jira.corp.x.com/browse/MAGETWO-36926');
+ $this->markTestSkipped('Skipped due to https://jira.corp.x.com/browse/MAGETWO-36926');
$linkTypes = ['related' => 1, 'upsell' => 4, 'crosssell' => 5, 'associated' => 3];
$this->linkTypeProviderMock->expects($this->once())->method('getLinkTypes')->willReturn($linkTypes);
diff --git a/app/code/Magento/GroupedProductGraphQl/Model/Resolver/Product/Price/Provider.php b/app/code/Magento/GroupedProductGraphQl/Model/Resolver/Product/Price/Provider.php
index b2336a0741292..3a45f7a234799 100644
--- a/app/code/Magento/GroupedProductGraphQl/Model/Resolver/Product/Price/Provider.php
+++ b/app/code/Magento/GroupedProductGraphQl/Model/Resolver/Product/Price/Provider.php
@@ -9,6 +9,7 @@
use Magento\Catalog\Pricing\Price\FinalPrice;
use Magento\Catalog\Pricing\Price\RegularPrice;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Framework\Pricing\PriceInfoInterface;
use Magento\Framework\Pricing\Amount\AmountInterface;
use Magento\Framework\Pricing\SaleableInterface;
@@ -17,7 +18,7 @@
/**
* Provides product prices for configurable products
*/
-class Provider implements ProviderInterface
+class Provider implements ProviderInterface, ResetAfterRequestInterface
{
/**
* Cache product prices so only fetch once
@@ -93,4 +94,12 @@ private function getMinimalProductAmount(SaleableInterface $product, string $pri
return $this->minimalProductAmounts[$product->getId()][$priceType];
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->minimalProductAmounts = [];
+ }
}
diff --git a/app/code/Magento/GroupedProductGraphQl/README.md b/app/code/Magento/GroupedProductGraphQl/README.md
index 07ac1f2cecf98..f29f3098ae033 100644
--- a/app/code/Magento/GroupedProductGraphQl/README.md
+++ b/app/code/Magento/GroupedProductGraphQl/README.md
@@ -21,4 +21,4 @@ Extension developers can interact with the Magento_GroupedProductGraphQll module
## Additional information
-You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql).
+You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/).
diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/History/Download.php b/app/code/Magento/ImportExport/Controller/Adminhtml/History/Download.php
index 9dcb2fdafb74f..6fd229f26a1a6 100644
--- a/app/code/Magento/ImportExport/Controller/Adminhtml/History/Download.php
+++ b/app/code/Magento/ImportExport/Controller/Adminhtml/History/Download.php
@@ -7,6 +7,7 @@
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\Filesystem\DirectoryList;
+use Magento\ImportExport\Model\Import;
/**
* Download history controller
@@ -47,6 +48,7 @@ public function __construct(
*/
public function execute()
{
+ // phpcs:ignore Magento2.Functions.DiscouragedFunction
$fileName = basename($this->getRequest()->getParam('filename'));
/** @var \Magento\ImportExport\Helper\Report $reportHelper */
@@ -59,17 +61,12 @@ public function execute()
return $resultRedirect;
}
- $this->fileFactory->create(
+ return $this->fileFactory->create(
$fileName,
- null,
+ ['type' => 'filename', 'value' => Import::IMPORT_HISTORY_DIR . $fileName],
DirectoryList::VAR_IMPORT_EXPORT,
'application/octet-stream',
$reportHelper->getReportSize($fileName)
);
-
- /** @var \Magento\Framework\Controller\Result\Raw $resultRaw */
- $resultRaw = $this->resultRawFactory->create();
- $resultRaw->setContents($reportHelper->getReportOutput($fileName));
- return $resultRaw;
}
}
diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Download.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Download.php
index ebf88e6c68e23..b74f48685feed 100644
--- a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Download.php
+++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Download.php
@@ -106,18 +106,13 @@ public function execute()
$fileSize = $this->sampleFileProvider->getSize($entityName);
$fileName = $entityName . '.csv';
- $this->fileFactory->create(
+ return $this->fileFactory->create(
$fileName,
- null,
+ $fileContents,
DirectoryList::VAR_IMPORT_EXPORT,
'application/octet-stream',
$fileSize
);
-
- $resultRaw = $this->resultRawFactory->create();
- $resultRaw->setContents($fileContents);
-
- return $resultRaw;
}
/**
diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php b/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php
index 4092879e23622..81347ce41a904 100644
--- a/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php
+++ b/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php
@@ -5,39 +5,43 @@
*/
namespace Magento\ImportExport\Controller\Adminhtml;
-use Magento\Backend\App\Action;
-use Magento\ImportExport\Model\Import\Entity\AbstractEntity;
+use Magento\Backend\App\Action\Context;
+use Magento\Framework\View\Element\AbstractBlock;
+use Magento\ImportExport\Helper\Report;
+use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError;
use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface;
use Magento\ImportExport\Model\History as ModelHistory;
use Magento\Framework\Escaper;
use Magento\Framework\App\ObjectManager;
+use Magento\ImportExport\Model\Import\RenderErrorMessages;
+use Magento\ImportExport\Model\Report\ReportProcessorInterface;
/**
* Import controller
*/
abstract class ImportResult extends Import
{
- const IMPORT_HISTORY_FILE_DOWNLOAD_ROUTE = '*/history/download';
+ public const IMPORT_HISTORY_FILE_DOWNLOAD_ROUTE = '*/history/download';
/**
* Limit view errors
*/
- const LIMIT_ERRORS_MESSAGE = 100;
+ public const LIMIT_ERRORS_MESSAGE = 100;
/**
- * @var \Magento\ImportExport\Model\Report\ReportProcessorInterface
+ * @var ReportProcessorInterface
*/
- protected $reportProcessor;
+ protected ReportProcessorInterface $reportProcessor;
/**
- * @var \Magento\ImportExport\Model\History
+ * @var ModelHistory
*/
- protected $historyModel;
+ protected ModelHistory $historyModel;
/**
- * @var \Magento\ImportExport\Helper\Report
+ * @var Report
*/
- protected $reportHelper;
+ protected Report $reportHelper;
/**
* @var Escaper|null
@@ -45,18 +49,25 @@ abstract class ImportResult extends Import
protected $escaper;
/**
- * @param \Magento\Backend\App\Action\Context $context
- * @param \Magento\ImportExport\Model\Report\ReportProcessorInterface $reportProcessor
- * @param \Magento\ImportExport\Model\History $historyModel
- * @param \Magento\ImportExport\Helper\Report $reportHelper
+ * @var RenderErrorMessages
+ */
+ private RenderErrorMessages $renderErrorMessages;
+
+ /**
+ * @param Context $context
+ * @param ReportProcessorInterface $reportProcessor
+ * @param ModelHistory $historyModel
+ * @param Report $reportHelper
* @param Escaper|null $escaper
+ * @param RenderErrorMessages|null $renderErrorMessages
*/
public function __construct(
- \Magento\Backend\App\Action\Context $context,
- \Magento\ImportExport\Model\Report\ReportProcessorInterface $reportProcessor,
- \Magento\ImportExport\Model\History $historyModel,
- \Magento\ImportExport\Helper\Report $reportHelper,
- Escaper $escaper = null
+ Context $context,
+ ReportProcessorInterface $reportProcessor,
+ ModelHistory $historyModel,
+ Report $reportHelper,
+ Escaper $escaper = null,
+ ?RenderErrorMessages $renderErrorMessages = null
) {
parent::__construct($context);
$this->reportProcessor = $reportProcessor;
@@ -64,46 +75,25 @@ public function __construct(
$this->reportHelper = $reportHelper;
$this->escaper = $escaper
?? ObjectManager::getInstance()->get(Escaper::class);
+ $this->renderErrorMessages = $renderErrorMessages ??
+ ObjectManager::getInstance()->get(RenderErrorMessages::class);
}
/**
* Add Error Messages for Import
*
- * @param \Magento\Framework\View\Element\AbstractBlock $resultBlock
+ * @param AbstractBlock $resultBlock
* @param ProcessingErrorAggregatorInterface $errorAggregator
* @return $this
*/
protected function addErrorMessages(
- \Magento\Framework\View\Element\AbstractBlock $resultBlock,
+ AbstractBlock $resultBlock,
ProcessingErrorAggregatorInterface $errorAggregator
) {
if ($errorAggregator->getErrorsCount()) {
- $message = '';
- $counter = 0;
- $escapedMessages = [];
- foreach ($this->getErrorMessages($errorAggregator) as $error) {
- $escapedMessages[] = (++$counter) . '. ' . $this->escaper->escapeHtml($error);
- if ($counter >= self::LIMIT_ERRORS_MESSAGE) {
- break;
- }
- }
- if ($errorAggregator->hasFatalExceptions()) {
- foreach ($this->getSystemExceptions($errorAggregator) as $error) {
- $escapedMessages[] = $this->escaper->escapeHtml($error->getErrorMessage())
- . ' '
- . __('Show more') . ' ' . __('Additional data') . ': '
- . $this->escaper->escapeHtml($error->getErrorDescription()) . '
';
- }
- }
try {
- $message .= implode(' ', $escapedMessages);
$resultBlock->addNotice(
- '' . __('Following Error(s) has been occurred during importing process:') . ' '
- . ''
+ $this->renderErrorMessages->renderMessages($errorAggregator)
);
} catch (\Exception $e) {
foreach ($this->getErrorMessages($errorAggregator) as $errorMessage) {
@@ -118,28 +108,23 @@ protected function addErrorMessages(
/**
* Get all Error Messages from Import Results
*
- * @param \Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface $errorAggregator
+ * @param ProcessingErrorAggregatorInterface $errorAggregator
* @return array
*/
protected function getErrorMessages(ProcessingErrorAggregatorInterface $errorAggregator)
{
- $messages = [];
- $rowMessages = $errorAggregator->getRowsGroupedByErrorCode([], [AbstractEntity::ERROR_CODE_SYSTEM_EXCEPTION]);
- foreach ($rowMessages as $errorCode => $rows) {
- $messages[] = $errorCode . ' ' . __('in row(s):') . ' ' . implode(', ', $rows);
- }
- return $messages;
+ return $this->renderErrorMessages->getErrorMessages($errorAggregator);
}
/**
* Get System Generated Exception
*
* @param ProcessingErrorAggregatorInterface $errorAggregator
- * @return \Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError[]
+ * @return ProcessingError[]
*/
protected function getSystemExceptions(ProcessingErrorAggregatorInterface $errorAggregator)
{
- return $errorAggregator->getErrorsByCode([AbstractEntity::ERROR_CODE_SYSTEM_EXCEPTION]);
+ return $this->renderErrorMessages->getSystemExceptions($errorAggregator);
}
/**
@@ -150,15 +135,7 @@ protected function getSystemExceptions(ProcessingErrorAggregatorInterface $error
*/
protected function createErrorReport(ProcessingErrorAggregatorInterface $errorAggregator)
{
- $this->historyModel->loadLastInsertItem();
- $sourceFile = $this->reportHelper->getReportAbsolutePath($this->historyModel->getImportedFile());
- $writeOnlyErrorItems = true;
- if ($this->historyModel->getData('execution_time') == ModelHistory::IMPORT_VALIDATION) {
- $writeOnlyErrorItems = false;
- }
- $fileName = $this->reportProcessor->createReport($sourceFile, $errorAggregator, $writeOnlyErrorItems);
- $this->historyModel->addErrorReportFile($fileName);
- return $fileName;
+ return $this->renderErrorMessages->createErrorReport($errorAggregator);
}
/**
@@ -169,6 +146,6 @@ protected function createErrorReport(ProcessingErrorAggregatorInterface $errorAg
*/
protected function createDownloadUrlImportHistoryFile($fileName)
{
- return $this->getUrl(self::IMPORT_HISTORY_FILE_DOWNLOAD_ROUTE, ['filename' => $fileName]);
+ return $this->renderErrorMessages->createDownloadUrlImportHistoryFile($fileName);
}
}
diff --git a/app/code/Magento/ImportExport/Model/Export.php b/app/code/Magento/ImportExport/Model/Export.php
index 6d466ba6dcdb8..1252a0665009b 100644
--- a/app/code/Magento/ImportExport/Model/Export.php
+++ b/app/code/Magento/ImportExport/Model/Export.php
@@ -6,6 +6,12 @@
namespace Magento\ImportExport\Model;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Filesystem;
+use Magento\ImportExport\Model\Export\ConfigInterface;
+use Magento\ImportExport\Model\Export\Entity\Factory;
+use Psr\Log\LoggerInterface;
+
/**
* Export model
*
@@ -78,12 +84,18 @@ class Export extends \Magento\ImportExport\Model\AbstractModel
];
/**
- * @param \Psr\Log\LoggerInterface $logger
- * @param \Magento\Framework\Filesystem $filesystem
- * @param \Magento\ImportExport\Model\Export\ConfigInterface $exportConfig
- * @param \Magento\ImportExport\Model\Export\Entity\Factory $entityFactory
+ * @var LocaleEmulatorInterface
+ */
+ private $localeEmulator;
+
+ /**
+ * @param LoggerInterface $logger
+ * @param Filesystem $filesystem
+ * @param ConfigInterface $exportConfig
+ * @param Factory $entityFactory
* @param \Magento\ImportExport\Model\Export\Adapter\Factory $exportAdapterFac
* @param array $data
+ * @param LocaleEmulatorInterface|null $localeEmulator
*/
public function __construct(
\Psr\Log\LoggerInterface $logger,
@@ -91,12 +103,14 @@ public function __construct(
\Magento\ImportExport\Model\Export\ConfigInterface $exportConfig,
\Magento\ImportExport\Model\Export\Entity\Factory $entityFactory,
\Magento\ImportExport\Model\Export\Adapter\Factory $exportAdapterFac,
- array $data = []
+ array $data = [],
+ ?LocaleEmulatorInterface $localeEmulator = null
) {
$this->_exportConfig = $exportConfig;
$this->_entityFactory = $entityFactory;
$this->_exportAdapterFac = $exportAdapterFac;
parent::__construct($logger, $filesystem, $data);
+ $this->localeEmulator = $localeEmulator ?? ObjectManager::getInstance()->get(LocaleEmulatorInterface::class);
}
/**
@@ -188,6 +202,20 @@ protected function _getWriter()
* @throws \Magento\Framework\Exception\LocalizedException
*/
public function export()
+ {
+ return $this->localeEmulator->emulate(
+ $this->exportCallback(...),
+ $this->getData('locale') ?: null
+ );
+ }
+
+ /**
+ * Export data.
+ *
+ * @return string
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ private function exportCallback()
{
if (isset($this->_data[self::FILTER_ELEMENT_GROUP])) {
$this->addLogComment(__('Begin export of %1', $this->getEntity()));
diff --git a/app/code/Magento/ImportExport/Model/Export/Consumer.php b/app/code/Magento/ImportExport/Model/Export/Consumer.php
index e83f508037da1..7623677a4781d 100644
--- a/app/code/Magento/ImportExport/Model/Export/Consumer.php
+++ b/app/code/Magento/ImportExport/Model/Export/Consumer.php
@@ -11,7 +11,6 @@
use Magento\Framework\Exception\FileSystemException;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Filesystem;
-use Magento\Framework\Locale\ResolverInterface;
use Magento\ImportExport\Api\Data\LocalizedExportInfoInterface;
use Magento\ImportExport\Api\ExportManagementInterface;
use Magento\Framework\Notification\NotifierInterface;
@@ -41,31 +40,23 @@ class Consumer
*/
private $filesystem;
- /**
- * @var ResolverInterface
- */
- private $localeResolver;
-
/**
* Consumer constructor.
* @param \Psr\Log\LoggerInterface $logger
* @param ExportManagementInterface $exportManager
* @param Filesystem $filesystem
* @param NotifierInterface $notifier
- * @param ResolverInterface $localeResolver
*/
public function __construct(
\Psr\Log\LoggerInterface $logger,
ExportManagementInterface $exportManager,
Filesystem $filesystem,
- NotifierInterface $notifier,
- ResolverInterface $localeResolver
+ NotifierInterface $notifier
) {
$this->logger = $logger;
$this->exportManager = $exportManager;
$this->filesystem = $filesystem;
$this->notifier = $notifier;
- $this->localeResolver = $localeResolver;
}
/**
@@ -76,11 +67,6 @@ public function __construct(
*/
public function process(LocalizedExportInfoInterface $exportInfo)
{
- $currentLocale = $this->localeResolver->getLocale();
- if ($exportInfo->getLocale()) {
- $this->localeResolver->setLocale($exportInfo->getLocale());
- }
-
try {
$data = $this->exportManager->export($exportInfo);
$fileName = $exportInfo->getFileName();
@@ -97,8 +83,6 @@ public function process(LocalizedExportInfoInterface $exportInfo)
__('Error during export process occurred. Please check logs for detail')
);
$this->logger->critical('Something went wrong while export process. ' . $exception->getMessage());
- } finally {
- $this->localeResolver->setLocale($currentLocale);
}
}
}
diff --git a/app/code/Magento/ImportExport/Model/Export/Entity/AbstractEav.php b/app/code/Magento/ImportExport/Model/Export/Entity/AbstractEav.php
index d9dd98bc54cd6..3e0b403089ac2 100644
--- a/app/code/Magento/ImportExport/Model/Export/Entity/AbstractEav.php
+++ b/app/code/Magento/ImportExport/Model/Export/Entity/AbstractEav.php
@@ -286,7 +286,8 @@ protected function _addAttributeValuesToRow(\Magento\Framework\Model\AbstractMod
if ($this->isMultiselect($attributeCode)) {
$values = [];
- $attributeValue = explode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, $attributeValue);
+ $attributeValue =
+ $attributeValue ? explode(Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR, $attributeValue) : [];
foreach ($attributeValue as $value) {
$values[] = $this->getAttributeValueById($attributeCode, $value);
}
diff --git a/app/code/Magento/ImportExport/Model/Import.php b/app/code/Magento/ImportExport/Model/Import.php
index aa3af449237f9..f86722895a406 100644
--- a/app/code/Magento/ImportExport/Model/Import.php
+++ b/app/code/Magento/ImportExport/Model/Import.php
@@ -210,18 +210,23 @@ class Import extends AbstractModel
*/
private $upload;
+ /**
+ * @var LocaleEmulatorInterface
+ */
+ private $localeEmulator;
+
/**
* @param LoggerInterface $logger
* @param Filesystem $filesystem
* @param DataHelper $importExportData
* @param ScopeConfigInterface $coreConfig
- * @param Import\ConfigInterface $importConfig
- * @param Import\Entity\Factory $entityFactory
+ * @param ConfigInterface $importConfig
+ * @param Factory $entityFactory
* @param Data $importData
- * @param Export\Adapter\CsvFactory $csvFactory
+ * @param CsvFactory $csvFactory
* @param FileTransferFactory $httpFactory
* @param UploaderFactory $uploaderFactory
- * @param Source\Import\Behavior\Factory $behaviorFactory
+ * @param Factory $behaviorFactory
* @param IndexerRegistry $indexerRegistry
* @param History $importHistoryModel
* @param DateTime $localeDate
@@ -229,6 +234,7 @@ class Import extends AbstractModel
* @param ManagerInterface|null $messageManager
* @param Random|null $random
* @param Upload|null $upload
+ * @param LocaleEmulatorInterface|null $localeEmulator
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
@@ -249,7 +255,8 @@ public function __construct(
array $data = [],
ManagerInterface $messageManager = null,
Random $random = null,
- Upload $upload = null
+ Upload $upload = null,
+ LocaleEmulatorInterface $localeEmulator = null
) {
$this->_importExportData = $importExportData;
$this->_coreConfig = $coreConfig;
@@ -270,16 +277,36 @@ public function __construct(
->get(Random::class);
$this->upload = $upload ?: ObjectManager::getInstance()
->get(Upload::class);
+ $this->localeEmulator = $localeEmulator ?: ObjectManager::getInstance()
+ ->get(LocaleEmulatorInterface::class);
parent::__construct($logger, $filesystem, $data);
}
/**
- * Create instance of entity adapter and return it
+ * Returns or create existing instance of entity adapter
*
* @throws LocalizedException
* @return EntityInterface
*/
protected function _getEntityAdapter()
+ {
+ if (!$this->_entityAdapter) {
+ $this->_entityAdapter = $this->localeEmulator->emulate(
+ $this->createEntityAdapter(...),
+ $this->getData('locale') ?: null
+ );
+ }
+
+ return $this->_entityAdapter;
+ }
+
+ /**
+ * Create instance of entity adapter and return it
+ *
+ * @throws LocalizedException
+ * @return EntityInterface
+ */
+ private function createEntityAdapter()
{
if (!$this->_entityAdapter) {
$entities = $this->_importConfig->getEntities();
@@ -479,6 +506,20 @@ public function getWorkingDir()
* @throws LocalizedException
*/
public function importSource()
+ {
+ return $this->localeEmulator->emulate(
+ $this->importSourceCallback(...),
+ $this->getData('locale') ?: null
+ );
+ }
+
+ /**
+ * Import source file structure to DB.
+ *
+ * @return bool
+ * @throws LocalizedException
+ */
+ private function importSourceCallback()
{
$ids = $this->_getEntityAdapter()->getIds();
if (empty($ids)) {
@@ -629,6 +670,21 @@ protected function _removeBom($sourceFile)
return $this;
}
+ /**
+ * Validates source file and returns validation result
+ *
+ * @param AbstractSource $source
+ * @return bool
+ * @throws LocalizedException
+ */
+ public function validateSource(AbstractSource $source)
+ {
+ return $this->localeEmulator->emulate(
+ fn () => $this->validateSourceCallback($source),
+ $this->getData('locale') ?: null
+ );
+ }
+
/**
* Validates source file and returns validation result
*
@@ -639,7 +695,7 @@ protected function _removeBom($sourceFile)
* @return bool
* @throws LocalizedException
*/
- public function validateSource(AbstractSource $source)
+ private function validateSourceCallback(AbstractSource $source)
{
$this->addLogComment(__('Begin data validation'));
diff --git a/app/code/Magento/ImportExport/Model/Import/RenderErrorMessages.php b/app/code/Magento/ImportExport/Model/Import/RenderErrorMessages.php
new file mode 100644
index 0000000000000..cb163edb55ce5
--- /dev/null
+++ b/app/code/Magento/ImportExport/Model/Import/RenderErrorMessages.php
@@ -0,0 +1,165 @@
+reportProcessor = $reportProcessor;
+ $this->historyModel = $historyModel;
+ $this->reportHelper = $reportHelper;
+ $this->escaper = $escaper
+ ?? ObjectManager::getInstance()->get(Escaper::class);
+ $this->backendUrl = $backendUrl
+ ?? ObjectManager::getInstance()->get(UrlInterface::class);
+ }
+
+ /**
+ * Add Error Messages for Import
+ *
+ * @param ProcessingErrorAggregatorInterface $errorAggregator
+ * @return string
+ */
+ public function renderMessages(
+ ProcessingErrorAggregatorInterface $errorAggregator
+ ): string {
+ $message = '';
+ $counter = 0;
+ $escapedMessages = [];
+ foreach ($this->getErrorMessages($errorAggregator) as $error) {
+ $escapedMessages[] = (++$counter) . '. ' . $this->escaper->escapeHtml($error);
+ if ($counter >= ImportResult::LIMIT_ERRORS_MESSAGE) {
+ break;
+ }
+ }
+ if ($errorAggregator->hasFatalExceptions()) {
+ foreach ($this->getSystemExceptions($errorAggregator) as $error) {
+ $escapedMessages[] = $this->escaper->escapeHtml($error->getErrorMessage())
+ . ' '
+ . __('Show more') . ' ' . __('Additional data') . ': '
+ . $this->escaper->escapeHtml($error->getErrorDescription()) . '
';
+ }
+ }
+ $message .= implode(' ', $escapedMessages);
+ return '' . __('Following Error(s) has been occurred during importing process:') . ' '
+ . '';
+ }
+
+ /**
+ * Get all Error Messages from Import Results
+ *
+ * @param ProcessingErrorAggregatorInterface $errorAggregator
+ * @return array
+ */
+ public function getErrorMessages(ProcessingErrorAggregatorInterface $errorAggregator): array
+ {
+ $messages = [];
+ $rowMessages = $errorAggregator->getRowsGroupedByErrorCode([], [AbstractEntity::ERROR_CODE_SYSTEM_EXCEPTION]);
+ foreach ($rowMessages as $errorCode => $rows) {
+ $messages[] = $errorCode . ' ' . __('in row(s):') . ' ' . implode(', ', $rows);
+ }
+ return $messages;
+ }
+
+ /**
+ * Get System Generated Exception
+ *
+ * @param ProcessingErrorAggregatorInterface $errorAggregator
+ * @return ProcessingError[]
+ */
+ public function getSystemExceptions(ProcessingErrorAggregatorInterface $errorAggregator): array
+ {
+ return $errorAggregator->getErrorsByCode([AbstractEntity::ERROR_CODE_SYSTEM_EXCEPTION]);
+ }
+
+ /**
+ * Generate Error Report File
+ *
+ * @param ProcessingErrorAggregatorInterface $errorAggregator
+ * @return string
+ */
+ public function createErrorReport(ProcessingErrorAggregatorInterface $errorAggregator): string
+ {
+ $this->historyModel->loadLastInsertItem();
+ $sourceFile = $this->reportHelper->getReportAbsolutePath($this->historyModel->getImportedFile());
+ $writeOnlyErrorItems = true;
+ if ($this->historyModel->getData('execution_time') == ModelHistory::IMPORT_VALIDATION) {
+ $writeOnlyErrorItems = false;
+ }
+ $fileName = $this->reportProcessor->createReport($sourceFile, $errorAggregator, $writeOnlyErrorItems);
+ $this->historyModel->addErrorReportFile($fileName);
+ return $fileName;
+ }
+
+ /**
+ * Get Import History Url
+ *
+ * @param string $fileName
+ * @return string
+ */
+ public function createDownloadUrlImportHistoryFile($fileName): string
+ {
+ return $this->backendUrl->getUrl(ImportResult::IMPORT_HISTORY_FILE_DOWNLOAD_ROUTE, ['filename' => $fileName]);
+ }
+}
diff --git a/app/code/Magento/ImportExport/Model/Import/Source/Csv.php b/app/code/Magento/ImportExport/Model/Import/Source/Csv.php
index 71780d8ae8b0e..178ca38ede0ae 100644
--- a/app/code/Magento/ImportExport/Model/Import/Source/Csv.php
+++ b/app/code/Magento/ImportExport/Model/Import/Source/Csv.php
@@ -55,8 +55,6 @@ public function __construct(
$delimiter = ',',
$enclosure = '"'
) {
- // phpcs:ignore Magento2.Functions.DiscouragedFunction
- register_shutdown_function([$this, 'destruct']);
if ($file instanceof FileReadInterface) {
$this->filePath = '';
$this->_file = $file;
@@ -83,7 +81,7 @@ public function __construct(
*
* @return void
*/
- public function destruct()
+ public function __destruct()
{
if (is_object($this->_file) && !empty(self::$openFiles[$this->filePath])) {
$this->_file->close();
diff --git a/app/code/Magento/ImportExport/Model/LocaleEmulator.php b/app/code/Magento/ImportExport/Model/LocaleEmulator.php
new file mode 100644
index 0000000000000..48e781c505d0c
--- /dev/null
+++ b/app/code/Magento/ImportExport/Model/LocaleEmulator.php
@@ -0,0 +1,63 @@
+isEmulating) {
+ return $callback();
+ }
+ $this->isEmulating = true;
+ $locale ??= $this->defaultLocaleResolver->getLocale();
+ $initialLocale = $this->localeResolver->getLocale();
+ $initialPhraseRenderer = Phrase::getRenderer();
+ Phrase::setRenderer($this->phraseRenderer);
+ $this->localeResolver->setLocale($locale);
+ $this->translate->setLocale($locale);
+ $this->translate->loadData();
+ try {
+ $result = $callback();
+ } finally {
+ Phrase::setRenderer($initialPhraseRenderer);
+ $this->localeResolver->setLocale($initialLocale);
+ $this->translate->setLocale($initialLocale);
+ $this->translate->loadData();
+ $this->isEmulating = false;
+ }
+ return $result;
+ }
+}
diff --git a/app/code/Magento/ImportExport/Model/LocaleEmulatorInterface.php b/app/code/Magento/ImportExport/Model/LocaleEmulatorInterface.php
new file mode 100644
index 0000000000000..ab0743230e6e9
--- /dev/null
+++ b/app/code/Magento/ImportExport/Model/LocaleEmulatorInterface.php
@@ -0,0 +1,23 @@
+httpFactory->create();
if (!$adapter->isValid(Import::FIELD_NAME_SOURCE_FILE)) {
$errors = $adapter->getErrors();
- if ($errors[0] == \Zend_Validate_File_Upload::INI_SIZE) {
+ if ($errors[0] == FileUploadValidator::INI_SIZE) {
$errorMessage = $this->importExportData->getMaxUploadSizeMessage();
} else {
$errorMessage = __('The file was not uploaded.');
@@ -86,7 +90,9 @@ public function uploadSource(string $entity)
throw new LocalizedException($errorMessage);
}
- /** @var $uploader Uploader */
+ /**
+ * @var $uploader Uploader
+ */
$uploader = $this->uploaderFactory->create(['fileId' => Import::FIELD_NAME_SOURCE_FILE]);
$uploader->setAllowedExtensions(['csv', 'zip']);
$uploader->skipDbProcessing(true);
diff --git a/app/code/Magento/ImportExport/README.md b/app/code/Magento/ImportExport/README.md
index ef1a9acbcce0f..a7a395c291cbe 100644
--- a/app/code/Magento/ImportExport/README.md
+++ b/app/code/Magento/ImportExport/README.md
@@ -1,4 +1,4 @@
-# Magento_ImportExport module
+# Magento_ImportExport module
This module provides a framework and basic functionality for importing/exporting various entities in Magento.
It can be disabled and in such case all dependent import/export functionality (products, customers, orders etc.) will be disabled in Magento.
@@ -6,6 +6,7 @@ It can be disabled and in such case all dependent import/export functionality (p
## Installation
The Magento_ImportExport module creates the following tables in the database:
+
- `importexport_importdata`
- `import_history`
@@ -44,7 +45,7 @@ For more information about a layout in Magento 2, see the [Layout documentation]
You can extend an export updates using the configuration files located in the `view/adminhtml/ui_component` directory:
-- `export_grid`
+- `export_grid`
For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/).
@@ -80,6 +81,7 @@ For information about a public API in Magento 2, see [Public interfaces & APIs](
2. Create an export model
You can get more information about import/export processes in magento at the articles:
+
- [Create custom import entity](https://developer.adobe.com/commerce/php/tutorials/backend/create-custom-import-entity/)
- [Import](https://docs.magento.com/user-guide/system/data-import.html)
- [Export](https://docs.magento.com/user-guide/system/data-export.html)
diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPageNavigateMenuTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPageNavigateMenuTest.xml
index d2eee7c3c5f42..d2b10d3d541b1 100644
--- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPageNavigateMenuTest.xml
+++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPageNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPagerGridTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPagerGridTest.xml
index 361722ec10b9b..4c547e17f1c23 100644
--- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPagerGridTest.xml
+++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminExportPagerGridTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImagesFileDirectoryCorrectExplanationTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImagesFileDirectoryCorrectExplanationTest.xml
index 9f286d5148a08..1a8f993b5e3c3 100644
--- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImagesFileDirectoryCorrectExplanationTest.xml
+++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImagesFileDirectoryCorrectExplanationTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportCSVWithSpecialCharactersTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportCSVWithSpecialCharactersTest.xml
index b9039fa59f5cb..a06e9b61bea9a 100644
--- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportCSVWithSpecialCharactersTest.xml
+++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportCSVWithSpecialCharactersTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml
index 8d405d7813cc9..fbfd966eaac93 100644
--- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml
+++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithAddUpdateBehaviorTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.xml
index 503037401b9f7..e076931785313 100644
--- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.xml
+++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithDeleteBehaviorTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithErrorEntriesTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithErrorEntriesTest.xml
index 5fe42e7074031..a4a49118d001f 100644
--- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithErrorEntriesTest.xml
+++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithErrorEntriesTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml
index 95a6a453e1e06..f3a36e6fef704 100644
--- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml
+++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminImportProductsWithReplaceBehaviorTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductImportCSVFileCorrectDifferentFilesTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductImportCSVFileCorrectDifferentFilesTest.xml
index 0403649d7add5..cf64852c7b50e 100644
--- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductImportCSVFileCorrectDifferentFilesTest.xml
+++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductImportCSVFileCorrectDifferentFilesTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductVisibilityDifferentStoreViewsAfterImportTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductVisibilityDifferentStoreViewsAfterImportTest.xml
index 3a4bd2507e8b6..b7572f359ccf6 100644
--- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductVisibilityDifferentStoreViewsAfterImportTest.xml
+++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminProductVisibilityDifferentStoreViewsAfterImportTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminSystemImportNavigateMenuTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminSystemImportNavigateMenuTest.xml
index 2811852fefaf4..ec03c03d052a3 100644
--- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminSystemImportNavigateMenuTest.xml
+++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminSystemImportNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml
index 6c2d7f76cce32..9910c5f91886f 100644
--- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml
+++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Export/FilterTest.php b/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Export/FilterTest.php
index cf69cdf7ee367..a7b1501e32eaa 100644
--- a/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Export/FilterTest.php
+++ b/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Export/FilterTest.php
@@ -333,6 +333,6 @@ private function getAttributeMock(array $data): Attribute
*/
public function testPrepareForm()
{
- $this->markTestIncomplete('This test has not been implemented yet.');
+ $this->markTestSkipped('This test has not been implemented yet.');
}
}
diff --git a/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Import/Edit/FormTest.php b/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Import/Edit/FormTest.php
index d153c169bfdd0..d31d22a97a380 100644
--- a/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Import/Edit/FormTest.php
+++ b/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Import/Edit/FormTest.php
@@ -83,6 +83,6 @@ protected function setUp(): void
*/
public function testPrepareForm()
{
- $this->markTestIncomplete('This test has not been implemented yet.');
+ $this->markTestSkipped('This test has not been implemented yet.');
}
}
diff --git a/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/History/DownloadTest.php b/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/History/DownloadTest.php
index 57e33a1dd51a3..7c8e06d3f681d 100644
--- a/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/History/DownloadTest.php
+++ b/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/History/DownloadTest.php
@@ -9,14 +9,17 @@
use Magento\Backend\App\Action\Context;
use Magento\Backend\Model\View\Result\Redirect;
+use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\App\Request\Http;
use Magento\Framework\App\Response\Http\FileFactory;
+use Magento\Framework\App\ResponseInterface;
use Magento\Framework\Controller\Result\Raw;
use Magento\Framework\Controller\Result\RawFactory;
use Magento\Framework\Controller\Result\RedirectFactory;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper;
use Magento\ImportExport\Controller\Adminhtml\History\Download;
use Magento\ImportExport\Helper\Report;
+use Magento\ImportExport\Model\Import;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
@@ -155,8 +158,20 @@ public function testExecute($requestFilename, $processedFilename)
$this->reportHelper->method('importFileExists')
->with($processedFilename)
->willReturn(true);
- $this->resultRaw->expects($this->once())->method('setContents');
- $this->downloadController->execute();
+
+ $responseMock = $this->getMockBuilder(ResponseInterface::class)
+ ->getMock();
+ $this->fileFactory->expects($this->once())
+ ->method('create')
+ ->with(
+ $processedFilename,
+ ['type' => 'filename', 'value' =>Import::IMPORT_HISTORY_DIR . $processedFilename],
+ DirectoryList::VAR_IMPORT_EXPORT,
+ 'application/octet-stream',
+ 1
+ )
+ ->willReturn($responseMock);
+ $this->assertSame($responseMock, $this->downloadController->execute());
}
/**
diff --git a/app/code/Magento/ImportExport/Test/Unit/Helper/ReportTest.php b/app/code/Magento/ImportExport/Test/Unit/Helper/ReportTest.php
index 2f10ce42f84d4..1a5677c555b1b 100644
--- a/app/code/Magento/ImportExport/Test/Unit/Helper/ReportTest.php
+++ b/app/code/Magento/ImportExport/Test/Unit/Helper/ReportTest.php
@@ -27,6 +27,7 @@
use Magento\ImportExport\Model\Import;
use Magento\ImportExport\Model\Import\Config;
use Magento\ImportExport\Model\Import\Entity\Factory;
+use Magento\ImportExport\Model\LocaleEmulatorInterface;
use Magento\ImportExport\Model\Source\Upload;
use Magento\MediaStorage\Model\File\UploaderFactory;
use PHPUnit\Framework\MockObject\MockObject;
@@ -153,7 +154,7 @@ protected function setUp(): void
*/
public function testGetExecutionTime()
{
- $this->markTestIncomplete('Invalid mocks used for DateTime object. Investigate later.');
+ $this->markTestSkipped('Invalid mocks used for DateTime object. Investigate later.');
$startDate = '2000-01-01 01:01:01';
$endDate = '2000-01-01 02:03:04';
@@ -204,6 +205,9 @@ public function testGetSummaryStats()
$importHistoryModel = $this->createMock(History::class);
$localeDate = $this->createMock(\Magento\Framework\Stdlib\DateTime\DateTime::class);
$upload = $this->createMock(Upload::class);
+ $localeEmulator = $this->getMockForAbstractClass(LocaleEmulatorInterface::class);
+ $localeEmulator->method('emulate')
+ ->willReturnCallback(fn (callable $callback) => $callback());
$import = new Import(
$logger,
$filesystem,
@@ -222,7 +226,8 @@ public function testGetSummaryStats()
[],
null,
null,
- $upload
+ $upload,
+ $localeEmulator
);
$import->setData('entity', 'catalog_product');
$message = $this->report->getSummaryStats($import);
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Export/ConsumerTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Export/ConsumerTest.php
index abef693c9acad..815680df29083 100644
--- a/app/code/Magento/ImportExport/Test/Unit/Model/Export/ConsumerTest.php
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/Export/ConsumerTest.php
@@ -41,11 +41,6 @@ class ConsumerTest extends TestCase
*/
private $notifierMock;
- /**
- * @var ResolverInterface|MockObject
- */
- private $localeResolver;
-
/**
* @var Consumer
*/
@@ -57,36 +52,21 @@ protected function setUp(): void
$this->exportManagementMock = $this->createMock(ExportManagementInterface::class);
$this->filesystemMock = $this->createMock(Filesystem::class);
$this->notifierMock = $this->createMock(NotifierInterface::class);
- $this->localeResolver = $this->createMock(ResolverInterface::class);
$this->consumer = new Consumer(
$this->loggerMock,
$this->exportManagementMock,
$this->filesystemMock,
- $this->notifierMock,
- $this->localeResolver
+ $this->notifierMock
);
}
public function testProcess()
{
- $adminLocale = 'de_DE';
$exportInfoMock = $this->createMock(LocalizedExportInfoInterface::class);
- $exportInfoMock->expects($this->atLeastOnce())
- ->method('getLocale')
- ->willReturn($adminLocale);
$exportInfoMock->expects($this->atLeastOnce())
->method('getFileName')
->willReturn('file_name.csv');
- $defaultLocale = 'en_US';
- $this->localeResolver->expects($this->once())
- ->method('getLocale')
- ->willReturn($defaultLocale);
- $this->localeResolver->expects($this->exactly(2))
- ->method('setLocale')
- ->withConsecutive([$adminLocale], [$defaultLocale])
- ->willReturn($this->localeResolver);
-
$data = '1,2,3';
$this->exportManagementMock->expects($this->once())
->method('export')
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/ExportTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/ExportTest.php
index 03c6356fc1adc..40e9191c17bda 100644
--- a/app/code/Magento/ImportExport/Test/Unit/Model/ExportTest.php
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/ExportTest.php
@@ -16,6 +16,7 @@
use Magento\ImportExport\Model\Export\Adapter\AbstractAdapter;
use Magento\ImportExport\Model\Export\ConfigInterface;
use Magento\ImportExport\Model\Export\Entity\Factory;
+use Magento\ImportExport\Model\LocaleEmulatorInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
@@ -23,87 +24,93 @@
class ExportTest extends TestCase
{
/**
- * Extension for export file
- *
- * @var string
+ * @var ConfigInterface|MockObject
*/
- protected $_exportFileExtension = 'csv';
+ private $exportConfigMock;
/**
- * @var MockObject
+ * @var AbstractEntity|MockObject
*/
- protected $_exportConfigMock;
+ private $exportAbstractEntityMock;
/**
- * @var AbstractEntity|MockObject
+ * @var AbstractAdapter|MockObject
*/
- private $abstractMockEntity;
+ private $exportAdapterMock;
/**
- * Return mock for \Magento\ImportExport\Model\Export class
- *
- * @return Export
+ * @var Export
*/
- protected function _getMageImportExportModelExportMock()
- {
- $this->_exportConfigMock = $this->getMockForAbstractClass(ConfigInterface::class);
+ private $model;
- $this->abstractMockEntity = $this->getMockForAbstractClass(
- AbstractEntity::class,
- [],
- '',
- false
- );
+ /**
+ * @var string[]
+ */
+ private $entities = [
+ 'entityA' => [
+ 'model' => 'entityAClass'
+ ],
+ 'entityB' => [
+ 'model' => 'entityBClass'
+ ]
+ ];
+ /**
+ * @var string[]
+ */
+ private $fileFormats = [
+ 'csv' => [
+ 'model' => 'csvFormatClass'
+ ],
+ 'xml' => [
+ 'model' => 'xmlFormatClass'
+ ]
+ ];
- /** @var $mockAdapterTest \Magento\ImportExport\Model\Export\Adapter\AbstractAdapter */
- $mockAdapterTest = $this->getMockForAbstractClass(
- AbstractAdapter::class,
- [],
- '',
- false,
- true,
- true,
- ['getFileExtension']
- );
- $mockAdapterTest->expects(
- $this->any()
- )->method(
- 'getFileExtension'
- )->willReturn(
- $this->_exportFileExtension
- );
+ /**
+ * @var LocaleEmulatorInterface|MockObject
+ */
+ private $localeEmulator;
+
+ /**
+ * @inheritdoc
+ */
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->exportConfigMock = $this->getMockForAbstractClass(ConfigInterface::class);
+ $this->exportConfigMock->method('getEntities')
+ ->willReturn($this->entities);
+ $this->exportConfigMock->method('getFileFormats')
+ ->willReturn($this->fileFormats);
+
+ $this->exportAbstractEntityMock = $this->getMockBuilder(AbstractEntity::class)
+ ->disableOriginalConstructor()
+ ->getMockForAbstractClass();
+
+ $this->exportAdapterMock = $this->getMockBuilder(AbstractAdapter::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['getFileExtension'])
+ ->getMockForAbstractClass();
$logger = $this->getMockForAbstractClass(LoggerInterface::class);
$filesystem = $this->createMock(Filesystem::class);
$entityFactory = $this->createMock(Factory::class);
- $exportAdapterFac = $this->createMock(\Magento\ImportExport\Model\Export\Adapter\Factory::class);
- /** @var \Magento\ImportExport\Model\Export $mockModelExport */
- $mockModelExport = $this->getMockBuilder(Export::class)
- ->setMethods(['getEntityAdapter', '_getEntityAdapter', '_getWriter', 'setWriter'])
- ->setConstructorArgs([$logger, $filesystem, $this->_exportConfigMock, $entityFactory, $exportAdapterFac])
- ->getMock();
- $mockModelExport->expects(
- $this->any()
- )->method(
- 'getEntityAdapter'
- )->willReturn(
- $this->abstractMockEntity
- );
- $mockModelExport->expects(
- $this->any()
- )->method(
- '_getEntityAdapter'
- )->willReturn(
- $this->abstractMockEntity
- );
- $mockModelExport->method(
- 'setWriter'
- )->willReturn(
- $this->abstractMockEntity
+ $entityFactory->method('create')
+ ->willReturn($this->exportAbstractEntityMock);
+ $exportAdapterFac = $this->createMock(Export\Adapter\Factory::class);
+ $exportAdapterFac->method('create')
+ ->willReturn($this->exportAdapterMock);
+ $this->localeEmulator = $this->getMockForAbstractClass(LocaleEmulatorInterface::class);
+
+ $this->model = new Export(
+ $logger,
+ $filesystem,
+ $this->exportConfigMock,
+ $entityFactory,
+ $exportAdapterFac,
+ [],
+ $this->localeEmulator
);
- $mockModelExport->expects($this->any())->method('_getWriter')->willReturn($mockAdapterTest);
-
- return $mockModelExport;
}
/**
@@ -114,17 +121,28 @@ protected function _getMageImportExportModelExportMock()
*/
public function testExportDoesntTrimResult()
{
- $model = $this->_getMageImportExportModelExportMock();
- $this->abstractMockEntity->method('export')
- ->willReturn("export data \n\n");
- $model->setData([
+ $locale = 'fr_FR';
+ $this->localeEmulator->method('emulate')
+ ->with($this->callback(fn ($callback) => is_callable($callback)), $locale)
+ ->willReturnCallback(fn (callable $callback) => $callback());
+ $config = [
+ 'entity' => 'entityA',
+ 'file_format' => 'csv',
Export::FILTER_ELEMENT_GROUP => [],
- 'entity' => 'catalog_product'
- ]);
- $model->export();
+ 'locale' => $locale
+ ];
+ $this->model->setData($config);
+ $this->exportAbstractEntityMock->method('getEntityTypeCode')
+ ->willReturn($config['entity']);
+ $this->exportAdapterMock->method('getFileExtension')
+ ->willReturn($config['file_format']);
+
+ $this->exportAbstractEntityMock->method('export')
+ ->willReturn("export data \n\n");
+ $this->model->export();
$this->assertStringContainsString(
'Exported 2 rows',
- var_export($model->getFormatedLogTrace(), true)
+ var_export($this->model->getFormatedLogTrace(), true)
);
}
@@ -133,15 +151,23 @@ public function testExportDoesntTrimResult()
*/
public function testGetFileNameWithAdapterFileName()
{
- $model = $this->_getMageImportExportModelExportMock();
$basicFileName = 'test_file_name';
- $model->getEntityAdapter()->setFileName($basicFileName);
-
- $fileName = $model->getFileName();
+ $config = [
+ 'entity' => 'entityA',
+ 'file_format' => 'csv',
+ ];
+ $this->model->setData($config);
+ $this->exportAbstractEntityMock->method('getEntityTypeCode')
+ ->willReturn($config['entity']);
+ $this->exportAdapterMock->method('getFileExtension')
+ ->willReturn($config['file_format']);
+ $this->exportAbstractEntityMock->setFileName($basicFileName);
+
+ $fileName = $this->model->getFileName();
$correctDateTime = $this->_getCorrectDateTime($fileName);
$this->assertNotNull($correctDateTime);
- $correctFileName = $basicFileName . '_' . $correctDateTime . '.' . $this->_exportFileExtension;
+ $correctFileName = $basicFileName . '_' . $correctDateTime . '.' . $config['file_format'];
$this->assertEquals($correctFileName, $fileName);
}
@@ -150,16 +176,22 @@ public function testGetFileNameWithAdapterFileName()
*/
public function testGetFileNameWithoutAdapterFileName()
{
- $model = $this->_getMageImportExportModelExportMock();
- $model->getEntityAdapter()->setFileName(null);
- $basicFileName = 'test_entity';
- $model->setEntity($basicFileName);
-
- $fileName = $model->getFileName();
+ $config = [
+ 'entity' => 'entityA',
+ 'file_format' => 'csv',
+ ];
+ $this->model->setData($config);
+ $this->exportAbstractEntityMock->method('getEntityTypeCode')
+ ->willReturn($config['entity']);
+ $this->exportAdapterMock->method('getFileExtension')
+ ->willReturn($config['file_format']);
+ $this->exportAbstractEntityMock->setFileName(null);
+
+ $fileName = $this->model->getFileName();
$correctDateTime = $this->_getCorrectDateTime($fileName);
$this->assertNotNull($correctDateTime);
- $correctFileName = $basicFileName . '_' . $correctDateTime . '.' . $this->_exportFileExtension;
+ $correctFileName = $config['entity'] . '_' . $correctDateTime . '.' . $config['file_format'];
$this->assertEquals($correctFileName, $fileName);
}
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/ZipTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/ZipTest.php
index 295b706e3e0b2..129a4cd3a9449 100644
--- a/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/ZipTest.php
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/Import/Source/ZipTest.php
@@ -42,7 +42,7 @@ protected function setUp(): void
*/
public function testConstructorFileDestinationMatch($fileName, $expectedfileName): void
{
- $this->markTestIncomplete('The implementation of constructor has changed. Rewrite test to cover changes.');
+ $this->markTestSkipped('The implementation of constructor has changed. Rewrite test to cover changes.');
$this->directory->method('getRelativePath')
->withConsecutive([$fileName], [$expectedfileName]);
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php
index 3f5b40cef7982..cc78111c53a6c 100644
--- a/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/ImportTest.php
@@ -30,6 +30,7 @@
use Magento\ImportExport\Model\Import\Entity\Factory;
use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface;
use Magento\ImportExport\Model\Import\Source\Csv;
+use Magento\ImportExport\Model\LocaleEmulatorInterface;
use Magento\ImportExport\Model\Source\Upload;
use Magento\ImportExport\Test\Unit\Model\Import\AbstractImportTestCase;
use Magento\MediaStorage\Model\File\UploaderFactory;
@@ -138,6 +139,11 @@ class ImportTest extends AbstractImportTestCase
*/
private $upload;
+ /**
+ * @var LocaleEmulatorInterface|MockObject
+ */
+ private $localeEmulator;
+
/**
* Set up
*
@@ -232,6 +238,7 @@ protected function setUp(): void
->method('getDriver')
->willReturn($this->_driver);
$this->upload = $this->createMock(Upload::class);
+ $this->localeEmulator = $this->getMockForAbstractClass(LocaleEmulatorInterface::class);
$this->import = $this->getMockBuilder(Import::class)
->setConstructorArgs(
[
@@ -252,7 +259,8 @@ protected function setUp(): void
[],
null,
null,
- $this->upload
+ $this->upload,
+ $this->localeEmulator
]
)
->setMethods(
@@ -281,6 +289,17 @@ protected function setUp(): void
public function testImportSource()
{
$entityTypeCode = 'code';
+ $locale = 'fr_FR';
+ $this->localeEmulator->method('emulate')
+ ->with($this->callback(fn ($callback) => is_callable($callback)), $locale)
+ ->willReturnCallback(fn (callable $callback) => $callback());
+ $this->import->expects($this->any())
+ ->method('getData')
+ ->willReturnMap(
+ [
+ ['locale', null, $locale],
+ ]
+ );
$this->_importData->expects($this->any())
->method('getEntityTypeCode')
->willReturn($entityTypeCode);
@@ -333,6 +352,17 @@ public function testImportSourceException()
__('URL key for specified store already exists.')
);
$entityTypeCode = 'code';
+ $locale = 'fr_FR';
+ $this->localeEmulator->method('emulate')
+ ->with($this->callback(fn ($callback) => is_callable($callback)), $locale)
+ ->willReturnCallback(fn (callable $callback) => $callback());
+ $this->import->expects($this->any())
+ ->method('getData')
+ ->willReturnMap(
+ [
+ ['locale', null, $locale],
+ ]
+ );
$this->_importData->expects($this->any())
->method('getEntityTypeCode')
->willReturn($entityTypeCode);
@@ -363,7 +393,7 @@ public function testImportSourceException()
*/
public function testGetOperationResultMessages()
{
- $this->markTestIncomplete('This test has not been implemented yet.');
+ $this->markTestSkipped('This test has not been implemented yet.');
}
/**
@@ -386,7 +416,7 @@ public function testGetAttributeType()
*/
public function testGetEntity()
{
- $this->markTestIncomplete('This test has not been implemented yet.');
+ $this->markTestSkipped('This test has not been implemented yet.');
}
/**
@@ -394,7 +424,7 @@ public function testGetEntity()
*/
public function testGetErrorsCount()
{
- $this->markTestIncomplete('This test has not been implemented yet.');
+ $this->markTestSkipped('This test has not been implemented yet.');
}
/**
@@ -402,7 +432,7 @@ public function testGetErrorsCount()
*/
public function testGetErrorsLimit()
{
- $this->markTestIncomplete('This test has not been implemented yet.');
+ $this->markTestSkipped('This test has not been implemented yet.');
}
/**
@@ -410,7 +440,7 @@ public function testGetErrorsLimit()
*/
public function testGetInvalidRowsCount()
{
- $this->markTestIncomplete('This test has not been implemented yet.');
+ $this->markTestSkipped('This test has not been implemented yet.');
}
/**
@@ -418,7 +448,7 @@ public function testGetInvalidRowsCount()
*/
public function testGetNotices()
{
- $this->markTestIncomplete('This test has not been implemented yet.');
+ $this->markTestSkipped('This test has not been implemented yet.');
}
/**
@@ -426,7 +456,7 @@ public function testGetNotices()
*/
public function testGetProcessedEntitiesCount()
{
- $this->markTestIncomplete('This test has not been implemented yet.');
+ $this->markTestSkipped('This test has not been implemented yet.');
}
/**
@@ -434,7 +464,7 @@ public function testGetProcessedEntitiesCount()
*/
public function testGetProcessedRowsCount()
{
- $this->markTestIncomplete('This test has not been implemented yet.');
+ $this->markTestSkipped('This test has not been implemented yet.');
}
/**
@@ -442,7 +472,7 @@ public function testGetProcessedRowsCount()
*/
public function testGetWorkingDir()
{
- $this->markTestIncomplete('This test has not been implemented yet.');
+ $this->markTestSkipped('This test has not been implemented yet.');
}
/**
@@ -450,7 +480,7 @@ public function testGetWorkingDir()
*/
public function testIsImportAllowed()
{
- $this->markTestIncomplete('This test has not been implemented yet.');
+ $this->markTestSkipped('This test has not been implemented yet.');
}
/**
@@ -458,7 +488,7 @@ public function testIsImportAllowed()
*/
public function testUploadSource()
{
- $this->markTestIncomplete('This test has not been implemented yet.');
+ $this->markTestSkipped('This test has not been implemented yet.');
}
/**
@@ -471,6 +501,10 @@ public function testValidateSource()
{
$validationStrategy = ProcessingErrorAggregatorInterface::VALIDATION_STRATEGY_STOP_ON_ERROR;
$allowedErrorCount = 1;
+ $locale = 'fr_FR';
+ $this->localeEmulator->method('emulate')
+ ->with($this->callback(fn ($callback) => is_callable($callback)), $locale)
+ ->willReturnCallback(fn (callable $callback) => $callback());
$this->errorAggregatorMock->expects($this->once())
->method('initValidationStrategy')
@@ -503,6 +537,7 @@ public function testValidateSource()
[
[Import::FIELD_NAME_VALIDATION_STRATEGY, null, $validationStrategy],
[Import::FIELD_NAME_ALLOWED_ERROR_COUNT, null, $allowedErrorCount],
+ ['locale', null, $locale],
]
);
@@ -704,7 +739,7 @@ public function unknownEntitiesProvider()
*/
public function testGetUniqueEntityBehaviors()
{
- $this->markTestIncomplete('This test has not been implemented yet.');
+ $this->markTestSkipped('This test has not been implemented yet.');
}
/**
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/LocaleEmulatorTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/LocaleEmulatorTest.php
new file mode 100644
index 0000000000000..5e9224713fc03
--- /dev/null
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/LocaleEmulatorTest.php
@@ -0,0 +1,154 @@
+translate = $this->getMockForAbstractClass(TranslateInterface::class);
+ $this->phraseRenderer = $this->getMockForAbstractClass(RendererInterface::class);
+ $this->localeResolver = $this->getMockForAbstractClass(ResolverInterface::class);
+ $this->defaultLocaleResolver = $this->getMockForAbstractClass(ResolverInterface::class);
+ $this->model = new LocaleEmulator(
+ $this->translate,
+ $this->phraseRenderer,
+ $this->localeResolver,
+ $this->defaultLocaleResolver
+ );
+ }
+
+ public function testEmulateWithSpecificLocale(): void
+ {
+ $initialLocale = 'en_US';
+ $initialPhraseRenderer = Phrase::getRenderer();
+ $locale = 'fr_FR';
+ $mock = $this->getMockBuilder(\stdClass::class)
+ ->addMethods(['assertPhraseRenderer'])
+ ->getMock();
+ $mock->expects($this->once())
+ ->method('assertPhraseRenderer')
+ ->willReturnCallback(
+ fn () => $this->assertSame($this->phraseRenderer, Phrase::getRenderer())
+ );
+ $this->defaultLocaleResolver->expects($this->never())
+ ->method('getLocale');
+ $this->localeResolver->expects($this->once())
+ ->method('getLocale')
+ ->willReturn($initialLocale);
+ $this->localeResolver->expects($this->exactly(2))
+ ->method('setLocale')
+ ->withConsecutive([$locale], [$initialLocale]);
+ $this->translate->expects($this->exactly(2))
+ ->method('setLocale')
+ ->withConsecutive([$locale], [$initialLocale]);
+ $this->translate->expects($this->exactly(2))
+ ->method('loadData');
+ $this->model->emulate($mock->assertPhraseRenderer(...), $locale);
+ $this->assertSame($initialPhraseRenderer, Phrase::getRenderer());
+ }
+
+ public function testEmulateWithDefaultLocale(): void
+ {
+ $initialLocale = 'en_US';
+ $initialPhraseRenderer = Phrase::getRenderer();
+ $locale = 'fr_FR';
+ $mock = $this->getMockBuilder(\stdClass::class)
+ ->addMethods(['assertPhraseRenderer'])
+ ->getMock();
+ $mock->expects($this->once())
+ ->method('assertPhraseRenderer')
+ ->willReturnCallback(
+ fn () => $this->assertSame($this->phraseRenderer, Phrase::getRenderer())
+ );
+ $this->defaultLocaleResolver->expects($this->once())
+ ->method('getLocale')
+ ->willReturn($locale);
+ $this->localeResolver->expects($this->once())
+ ->method('getLocale')
+ ->willReturn($initialLocale);
+ $this->localeResolver->expects($this->exactly(2))
+ ->method('setLocale')
+ ->withConsecutive([$locale], [$initialLocale]);
+ $this->translate->expects($this->exactly(2))
+ ->method('setLocale')
+ ->withConsecutive([$locale], [$initialLocale]);
+ $this->translate->expects($this->exactly(2))
+ ->method('loadData');
+ $this->model->emulate($mock->assertPhraseRenderer(...));
+ $this->assertSame($initialPhraseRenderer, Phrase::getRenderer());
+ }
+
+ public function testEmulateWithException(): void
+ {
+ $exception = new \Exception('Oops! Something went wrong.');
+ $this->expectExceptionObject($exception);
+ $initialLocale = 'en_US';
+ $initialPhraseRenderer = Phrase::getRenderer();
+ $locale = 'fr_FR';
+ $mock = $this->getMockBuilder(\stdClass::class)
+ ->addMethods(['callbackThatThrowsException'])
+ ->getMock();
+ $mock->expects($this->once())
+ ->method('callbackThatThrowsException')
+ ->willThrowException($exception);
+ $this->defaultLocaleResolver->expects($this->once())
+ ->method('getLocale')
+ ->willReturn($locale);
+ $this->localeResolver->expects($this->once())
+ ->method('getLocale')
+ ->willReturn($initialLocale);
+ $this->localeResolver->expects($this->exactly(2))
+ ->method('setLocale')
+ ->withConsecutive([$locale], [$initialLocale]);
+ $this->translate->expects($this->exactly(2))
+ ->method('setLocale')
+ ->withConsecutive([$locale], [$initialLocale]);
+ $this->translate->expects($this->exactly(2))
+ ->method('loadData');
+ $this->model->emulate($mock->callbackThatThrowsException(...));
+ $this->assertSame($initialPhraseRenderer, Phrase::getRenderer());
+ }
+}
diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Source/UploadTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Source/UploadTest.php
new file mode 100644
index 0000000000000..dd13dc6b4c97e
--- /dev/null
+++ b/app/code/Magento/ImportExport/Test/Unit/Model/Source/UploadTest.php
@@ -0,0 +1,113 @@
+httpFactoryMock = $this->createPartialMock(FileTransferFactory::class, ['create']);
+ $this->importExportDataMock = $this->createMock(DataHelper::class);
+ $this->uploaderFactoryMock = $this->getMockBuilder(UploaderFactory::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->randomMock = $this->getMockBuilder(Random::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->filesystemMock = $this->createMock(Filesystem::class);
+ $this->adapterMock = $this->createMock(Http::class);
+ $directoryWriteMock = $this->getMockBuilder(WriteInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $directoryWriteMock->expects($this->once())->method('getAbsolutePath')->willReturn($directoryAbsolutePath);
+ $this->filesystemMock->expects($this->once())->method('getDirectoryWrite')->willReturn($directoryWriteMock);
+ $this->upload = new Upload(
+ $this->httpFactoryMock,
+ $this->importExportDataMock,
+ $this->uploaderFactoryMock,
+ $this->randomMock,
+ $this->filesystemMock
+ );
+ }
+
+ /**
+ * @return void
+ */
+ public function testValidateFileUploadReturnsSavedFileArray(): void
+ {
+ $allowedExtensions = ['csv', 'zip'];
+ $savedFileName = 'testString';
+ $importFileId = 'import_file';
+ $randomStringLength=32;
+ $this->adapterMock->method('isValid')->willReturn(true);
+ $this->httpFactoryMock->method('create')->willReturn($this->adapterMock);
+ $this->uploaderMock = $this->createMock(Uploader::class);
+ $this->uploaderMock->method('setAllowedExtensions')->with($allowedExtensions);
+ $this->uploaderMock->method('skipDbProcessing')->with(true);
+ $this->uploaderFactoryMock->method('create')
+ ->with(['fileId' => $importFileId])
+ ->willReturn($this->uploaderMock);
+ $this->randomMock->method('getRandomString')->with($randomStringLength);
+ $this->uploaderMock->method('save')->willReturn(['file' => $savedFileName]);
+ $result = $this->upload->uploadSource($savedFileName);
+ $this->assertIsArray($result);
+ $this->assertEquals($savedFileName, $result['file']);
+ }
+}
diff --git a/app/code/Magento/ImportExport/etc/adminhtml/di.xml b/app/code/Magento/ImportExport/etc/adminhtml/di.xml
index 7b124957d5f57..cb09c448cf0c4 100644
--- a/app/code/Magento/ImportExport/etc/adminhtml/di.xml
+++ b/app/code/Magento/ImportExport/etc/adminhtml/di.xml
@@ -28,4 +28,9 @@
Magento\Framework\Filesystem\Driver\File
+
+
+ Magento\Backend\Model\Locale\Resolver
+
+
diff --git a/app/code/Magento/ImportExport/etc/di.xml b/app/code/Magento/ImportExport/etc/di.xml
index b4c65aaf5ef11..76c06d382256e 100644
--- a/app/code/Magento/ImportExport/etc/di.xml
+++ b/app/code/Magento/ImportExport/etc/di.xml
@@ -13,6 +13,7 @@
+
@@ -39,4 +40,15 @@
+
+
+ Magento\Directory\Helper\Data::XML_PATH_DEFAULT_LOCALE
+ Magento\Framework\App\Config\ScopeConfigInterface::SCOPE_TYPE_DEFAULT
+
+
+
+
+ Magento\ImportExport\Model\DefaultLocaleResolver
+
+
diff --git a/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand.php b/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand.php
index 51d67e2116a06..0020a0592aa31 100644
--- a/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand.php
+++ b/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand.php
@@ -7,23 +7,24 @@
namespace Magento\Indexer\Console\Command;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Console\Input\InputArgument;
-use Magento\Framework\App\ObjectManagerFactory;
use Magento\Framework\App\Config\ScopeConfigInterface;
+use Magento\Framework\App\ObjectManagerFactory;
use Magento\Framework\Console\Cli;
+use Magento\Indexer\Console\Command\IndexerSetDimensionsModeCommand\ModeInputArgument;
use Magento\Indexer\Model\ModeSwitcherInterface;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
/**
* Command to set indexer dimensions mode
*/
class IndexerSetDimensionsModeCommand extends AbstractIndexerCommand
{
- const INPUT_KEY_MODE = 'mode';
- const INPUT_KEY_INDEXER = 'indexer';
- const DIMENSION_MODE_NONE = 'none';
- const XML_PATH_DIMENSIONS_MODE_MASK = 'indexer/%s/dimensions_mode';
+ public const INPUT_KEY_MODE = 'mode';
+ public const INPUT_KEY_INDEXER = 'indexer';
+ public const DIMENSION_MODE_NONE = 'none';
+ public const XML_PATH_DIMENSIONS_MODE_MASK = 'indexer/%s/dimensions_mode';
/**
* @var string
@@ -58,7 +59,7 @@ public function __construct(
}
/**
- * {@inheritdoc}
+ * @inheritdoc
*/
protected function configure()
{
@@ -69,7 +70,7 @@ protected function configure()
}
/**
- * {@inheritdoc}
+ * @inheritdoc
* @param InputInterface $input
* @param OutputInterface $output
* @return int
@@ -144,17 +145,19 @@ private function getInputList(): array
InputArgument::OPTIONAL,
$indexerOptionDescription
);
- $modeOptionDescription = 'Indexer dimension modes' . PHP_EOL;
- foreach ($this->dimensionProviders as $indexer => $provider) {
- $availableModes = implode(',', array_keys($provider->getDimensionModes()->getDimensions()));
- $modeOptionDescription .= sprintf('%-30s ', $indexer) . $availableModes . PHP_EOL;
- }
- $arguments[] = new InputArgument(
+ $modeOptionDescriptionClosure = function () {
+ $modeOptionDescription = 'Indexer dimension modes' . PHP_EOL;
+ foreach ($this->dimensionProviders as $indexer => $provider) {
+ $availableModes = implode(',', array_keys($provider->getDimensionModes()->getDimensions()));
+ $modeOptionDescription .= sprintf('%-30s ', $indexer) . $availableModes . PHP_EOL;
+ }
+ return $modeOptionDescription;
+ };
+ $arguments[] = new ModeInputArgument(
self::INPUT_KEY_MODE,
InputArgument::OPTIONAL,
- $modeOptionDescription
+ $modeOptionDescriptionClosure
);
-
return $arguments;
}
diff --git a/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand/ModeInputArgument.php b/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand/ModeInputArgument.php
new file mode 100644
index 0000000000000..67a2de8dacac2
--- /dev/null
+++ b/app/code/Magento/Indexer/Console/Command/IndexerSetDimensionsModeCommand/ModeInputArgument.php
@@ -0,0 +1,48 @@
+callableDescription = $callableDescription;
+ parent::__construct($name, $mode, '', $default);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getDescription()
+ {
+ if (null !== $this->callableDescription) {
+ $description = ($this->callableDescription)();
+ $this->callableDescription = null;
+ return $description;
+ }
+ return parent::getDescription();
+ }
+}
diff --git a/app/code/Magento/Indexer/Model/Indexer.php b/app/code/Magento/Indexer/Model/Indexer.php
index ac8b9590e58f4..7be1d5a3a9e21 100644
--- a/app/code/Magento/Indexer/Model/Indexer.php
+++ b/app/code/Magento/Indexer/Model/Indexer.php
@@ -441,8 +441,10 @@ public function reindexAll()
}
try {
$this->getActionInstance()->executeFull();
- $state->setStatus(StateInterface::STATUS_VALID);
- $state->save();
+ if ($this->workingStateProvider->isWorking($this->getId())) {
+ $state->setStatus(StateInterface::STATUS_VALID);
+ $state->save();
+ }
if (!empty($sharedIndexers)) {
$this->resumeSharedViews($sharedIndexers);
}
diff --git a/app/code/Magento/Indexer/Model/Processor.php b/app/code/Magento/Indexer/Model/Processor.php
index 78b8fa070b155..7846421daa704 100644
--- a/app/code/Magento/Indexer/Model/Processor.php
+++ b/app/code/Magento/Indexer/Model/Processor.php
@@ -59,7 +59,7 @@ public function __construct(
IndexerInterfaceFactory $indexerFactory,
Indexer\CollectionFactory $indexersFactory,
ProcessorInterface $mviewProcessor,
- MakeSharedIndexValid $makeSharedValid = null
+ ?MakeSharedIndexValid $makeSharedValid = null
) {
$this->config = $config;
$this->indexerFactory = $indexerFactory;
@@ -86,9 +86,11 @@ public function reindexAllInvalid()
$sharedIndex = $indexerConfig['shared_index'] ?? null;
if (!in_array($sharedIndex, $this->sharedIndexesComplete)) {
$indexer->reindexAll();
-
- if (!empty($sharedIndex) && $this->makeSharedValid->execute($sharedIndex)) {
- $this->sharedIndexesComplete[] = $sharedIndex;
+ $indexer->load($indexer->getId());
+ if ($indexer->isValid()) {
+ if (!empty($sharedIndex) && $this->makeSharedValid->execute($sharedIndex)) {
+ $this->sharedIndexesComplete[] = $sharedIndex;
+ }
}
}
}
diff --git a/app/code/Magento/Indexer/README.md b/app/code/Magento/Indexer/README.md
index ed9ee7ec9723f..0285d3400924a 100644
--- a/app/code/Magento/Indexer/README.md
+++ b/app/code/Magento/Indexer/README.md
@@ -2,6 +2,7 @@
This module provides Magento Indexing functionality.
It allows to:
+
- read indexers configuration
- represent indexers in admin
- regenerate indexes by cron schedule
@@ -19,6 +20,7 @@ This module is dependent on the following modules:
- `Magento_AdminNotification`
The Magento_Indexer module creates the following tables in the database:
+
- `indexer_state`
- `mview_state`
@@ -45,7 +47,7 @@ The module dispatches the following events:
- `clean_cache_by_tags` event in the `\Magento\Indexer\Model\Indexer\CacheCleaner::cleanCache` method. Parameters:
- `object` is a `cacheContext` object (`Magento\Framework\Indexer\CacheContext` class)
-#### Plugin
+#### Plugin
- `clean_cache_after_reindex` event in the `\Magento\Indexer\Model\Processor\CleanCache::afterUpdateMview` method. Parameters:
- `object` is a `context` object (`Magento\Framework\Indexer\CacheContext` class)
@@ -58,6 +60,7 @@ For information about an event in Magento 2, see [Events and observers](https://
### Layouts
This module introduces the following layout handles in the `view/adminhtml/layout` directory:
+
- `indexer_indexer_list`
- `indexer_indexer_list_grid`
@@ -75,6 +78,7 @@ There are 2 modes of the Indexers:
### Console commands
Magento_Indexers provides console commands:
+
- `bin/magento indexer:info` - view a list of all indexers
- `bin/magento indexer:status [indexer]` - view index status
- `bin/magento indexer:reindex [indexer]` - run reindex
@@ -87,6 +91,7 @@ Magento_Indexers provides console commands:
### Cron options
Cron group configuration can be set at `etc/crontab.xml`:
+
- `indexer_reindex_all_invalid` - regenerate indexes for all invalid indexers
- `indexer_update_all_views` - update indexer views
- `indexer_clean_all_changelogs` - clean indexer view changelogs
@@ -94,8 +99,9 @@ Cron group configuration can be set at `etc/crontab.xml`:
[Learn how to configure and run cron in Magento.](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configure-cron-jobs.html).
More information can get at articles:
+
- [Learn more about indexing](https://developer.adobe.com/commerce/php/development/components/indexing/)
-- [Learn more about Indexer optimization](https://devdocs.magento.com/guides/v2.4/extension-dev-guide/indexer-batch.html)
+- [Learn more about Indexer optimization](https://developer.adobe.com/commerce/php/development/components/indexing/optimization/)
- [Learn more how to add custom indexer](https://developer.adobe.com/commerce/php/development/components/indexing/custom-indexer/)
- [Learn how to manage indexers](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/manage-indexers.html)
- [Learn more about Index Management](https://docs.magento.com/user-guide/system/index-management.html)
diff --git a/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementNavigateMenuTest.xml b/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementNavigateMenuTest.xml
index d92e70cd1993f..75d61fe2261f5 100644
--- a/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementNavigateMenuTest.xml
+++ b/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php
index bcdfbea78b0b3..451d4c211eadd 100644
--- a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php
+++ b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php
@@ -144,7 +144,8 @@ public function testLoadWithException()
public function testGetView()
{
$indexId = 'indexer_internal_name';
- $this->viewMock->expects($this->once())->method('load')->with('view_test')->willReturnSelf();
+ $this->viewMock->expects($this->once())
+ ->method('load')->with('view_test')->willReturnSelf();
$this->loadIndexer($indexId);
$this->assertEquals($this->viewMock, $this->model->getView());
@@ -224,11 +225,14 @@ public function testReindexAll()
$indexId = 'indexer_internal_name';
$this->loadIndexer($indexId);
+ $this->workingStateProvider->method('isWorking')->willReturnOnConsecutiveCalls(false, true);
+
$stateMock = $this->createPartialMock(
State::class,
['load', 'getId', 'setIndexerId', '__wakeup', 'getStatus', 'setStatus', 'save']
);
- $stateMock->expects($this->once())->method('load')->with($indexId, 'indexer_id')->willReturnSelf();
+ $stateMock->expects($this->once())
+ ->method('load')->with($indexId, 'indexer_id')->willReturnSelf();
$stateMock->expects($this->never())->method('setIndexerId');
$stateMock->expects($this->once())->method('getId')->willReturn(1);
$stateMock->expects($this->exactly(2))->method('setStatus')->willReturnSelf();
@@ -268,7 +272,8 @@ public function testReindexAllWithException()
State::class,
['load', 'getId', 'setIndexerId', '__wakeup', 'getStatus', 'setStatus', 'save']
);
- $stateMock->expects($this->once())->method('load')->with($indexId, 'indexer_id')->willReturnSelf();
+ $stateMock->expects($this->once())
+ ->method('load')->with($indexId, 'indexer_id')->willReturnSelf();
$stateMock->expects($this->never())->method('setIndexerId');
$stateMock->expects($this->once())->method('getId')->willReturn(1);
$stateMock->expects($this->exactly(2))->method('setStatus')->willReturnSelf();
@@ -313,7 +318,8 @@ public function testReindexAllWithError()
State::class,
['load', 'getId', 'setIndexerId', '__wakeup', 'getStatus', 'setStatus', 'save']
);
- $stateMock->expects($this->once())->method('load')->with($indexId, 'indexer_id')->willReturnSelf();
+ $stateMock->expects($this->once())
+ ->method('load')->with($indexId, 'indexer_id')->willReturnSelf();
$stateMock->expects($this->never())->method('setIndexerId');
$stateMock->expects($this->once())->method('getId')->willReturn(1);
$stateMock->expects($this->exactly(2))->method('setStatus')->willReturnSelf();
@@ -483,7 +489,8 @@ public function testInvalidate()
);
$this->stateFactoryMock->expects($this->once())->method('create')->willReturn($stateMock);
- $stateMock->expects($this->once())->method('setStatus')->with(StateInterface::STATUS_INVALID)->willReturnSelf();
+ $stateMock->expects($this->once())
+ ->method('setStatus')->with(StateInterface::STATUS_INVALID)->willReturnSelf();
$stateMock->expects($this->once())->method('save')->willReturnSelf();
$this->model->invalidate();
}
diff --git a/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php b/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php
index b0a3395519551..ba6216f37f7da 100644
--- a/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php
+++ b/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php
@@ -102,18 +102,14 @@ public function testReindexAllInvalid(): void
$this->configMock->expects($this->once())->method('getIndexers')->willReturn($indexers);
$state1Mock = $this->createPartialMock(State::class, ['getStatus', '__wakeup']);
- $state1Mock->expects(
- $this->once()
- )->method(
- 'getStatus'
- )->willReturn(
- StateInterface::STATUS_INVALID
- );
+ $state1Mock->expects($this->exactly(2))
+ ->method('getStatus')
+ ->willReturnOnConsecutiveCalls(StateInterface::STATUS_INVALID, StateInterface::STATUS_VALID);
$indexer1Mock = $this->createPartialMock(
Indexer::class,
['load', 'getState', 'reindexAll']
);
- $indexer1Mock->expects($this->once())->method('getState')->willReturn($state1Mock);
+ $indexer1Mock->expects($this->exactly(2))->method('getState')->willReturn($state1Mock);
$indexer1Mock->expects($this->once())->method('reindexAll');
$state2Mock = $this->createPartialMock(State::class, ['getStatus', '__wakeup']);
@@ -169,7 +165,10 @@ function ($elem) {
$stateMock = $this->createPartialMock(State::class, ['getStatus', '__wakeup']);
$stateMock->expects($this->any())
->method('getStatus')
- ->willReturn($indexerStates[$indexerData['indexer_id']]);
+ ->willReturnOnConsecutiveCalls(
+ $indexerStates[$indexerData['indexer_id']],
+ StateInterface::STATUS_VALID
+ );
$indexerMock = $this->createPartialMock(Indexer::class, ['load', 'getState', 'reindexAll']);
$indexerMock->expects($this->any())->method('getState')->willReturn($stateMock);
$indexerMock->expects($expectedReindexAllCalls[$indexerData['indexer_id']])->method('reindexAll');
diff --git a/app/code/Magento/Indexer/etc/di.xml b/app/code/Magento/Indexer/etc/di.xml
index 482ca591811b7..e609f9eace9bb 100644
--- a/app/code/Magento/Indexer/etc/di.xml
+++ b/app/code/Magento/Indexer/etc/di.xml
@@ -37,6 +37,9 @@
Magento\Framework\Mview\View\CollectionInterface
+
+ - updated_at
+
diff --git a/app/code/Magento/InstantPurchase/README.md b/app/code/Magento/InstantPurchase/README.md
index a4dfa6500e19a..f92335e4c4701 100644
--- a/app/code/Magento/InstantPurchase/README.md
+++ b/app/code/Magento/InstantPurchase/README.md
@@ -34,13 +34,13 @@ Extension developers can interact with the Magento_InstantPurchase module. For m
- `\Magento\InstantPurchase\Model\ShippingMethodChoose\ShippingMethodChooserInterface`
- choose shipping method for customer address if available
-
+
- `\Magento\InstantPurchase\Model\InstantPurchaseInterface`
- detects instant purchase options for a customer in a store
-
+
- `\Magento\InstantPurchase\PaymentMethodIntegration\AvailabilityCheckerInterface`
- checks if payment method may be used for instant purchase
-
+
- `\Magento\InstantPurchase\PaymentMethodIntegration\PaymentAdditionalInformationProviderInterface`
- provides additional information part specific for payment method
diff --git a/app/code/Magento/InstantPurchase/Test/Mftf/Test/StorefrontInstantPurchaseFunctionalityTest.xml b/app/code/Magento/InstantPurchase/Test/Mftf/Test/StorefrontInstantPurchaseFunctionalityTest.xml
index 4248c15b50e05..7cb5b9a12f4d0 100644
--- a/app/code/Magento/InstantPurchase/Test/Mftf/Test/StorefrontInstantPurchaseFunctionalityTest.xml
+++ b/app/code/Magento/InstantPurchase/Test/Mftf/Test/StorefrontInstantPurchaseFunctionalityTest.xml
@@ -19,6 +19,8 @@
+
+
diff --git a/app/code/Magento/Integration/README.md b/app/code/Magento/Integration/README.md
index 0e17dc80c1355..c9caeb63a9555 100644
--- a/app/code/Magento/Integration/README.md
+++ b/app/code/Magento/Integration/README.md
@@ -10,11 +10,13 @@ model for request and access token management.
The Magento_Integration module is one of the base Magento 2 modules. You cannot disable or uninstall this module.
This module is dependent on the following modules:
+
- `Magento_Store`
- `Magento_User`
- `Magento_Security`
The Magento_Integration module creates the following tables in the database:
+
- `oauth_consumer`
- `oauth_token`
- `oauth_nonce`
@@ -34,6 +36,7 @@ Extension developers can interact with the Magento_Integration module. For more
The module dispatches the following events:
#### Model
+
- `customer_login` event in the `\Magento\Integration\Model\CustomerTokenService::createCustomerAccessToken` method. Parameters:
- `customer` is an object (`\Magento\Customer\Api\Data\CustomerInterface` class)
@@ -42,6 +45,7 @@ For information about an event in Magento 2, see [Events and observers](https://
### Layouts
This module introduces the following layout handles in the `view/adminhtml/layout` directory:
+
- `adminhtml_integration_edit`
- `adminhtml_integration_grid`
- `adminhtml_integration_grid_block`
@@ -82,7 +86,7 @@ For more information about a layout in Magento 2, see the [Layout documentation]
- create a new consumer account
- create access token for provided consumer
- retrieve access token assigned to the consumer
- - load consumer by its ID
+ - load consumer by its ID
- load consumer by its key
- execute post to integration (consumer) HTTP Post URL. Generate and return oauth_verifier
- delete the consumer data associated with the integration including its token and nonce
@@ -95,11 +99,13 @@ For information about a public API in Magento 2, see [Public interfaces & APIs](
### Cron options
Cron group configuration can be set at `etc/crontab.xml`:
+
- `outdated_authentication_failures_cleanup` - clearing log of outdated token request authentication failures
- `expired_tokens_cleanups` - delete expired customer and admin tokens
[Learn how to configure and run cron in Magento.](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configure-cron-jobs.html).
More information can get at articles:
+
- [Learn more about an Integration](https://docs.magento.com/user-guide/system/integrations.html)
- [Lear how to create an Integration](https://developer.adobe.com/commerce/webapi/get-started/create-integration/)
diff --git a/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminFillIntegrationFormActionGroup.xml b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminFillIntegrationFormActionGroup.xml
index 6d4e4ed39f6e2..2e51b34b6b3b9 100644
--- a/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminFillIntegrationFormActionGroup.xml
+++ b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AdminFillIntegrationFormActionGroup.xml
@@ -10,7 +10,7 @@
-
+
diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminCreateIntegrationEntityWithDuplicatedNameTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminCreateIntegrationEntityWithDuplicatedNameTest.xml
index d7dca53888f9d..a735a49cabee5 100644
--- a/app/code/Magento/Integration/Test/Mftf/Test/AdminCreateIntegrationEntityWithDuplicatedNameTest.xml
+++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminCreateIntegrationEntityWithDuplicatedNameTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminDeleteIntegrationEntityTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminDeleteIntegrationEntityTest.xml
index 0148278ac7aaa..dbb3d005f724f 100644
--- a/app/code/Magento/Integration/Test/Mftf/Test/AdminDeleteIntegrationEntityTest.xml
+++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminDeleteIntegrationEntityTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminReAuthorizeTokensIntegrationEntityTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminReAuthorizeTokensIntegrationEntityTest.xml
index 509521038d4f0..ed1de47c64afb 100644
--- a/app/code/Magento/Integration/Test/Mftf/Test/AdminReAuthorizeTokensIntegrationEntityTest.xml
+++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminReAuthorizeTokensIntegrationEntityTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminSystemIntegrationsNavigateMenuTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminSystemIntegrationsNavigateMenuTest.xml
index a1a9641f6be31..a29c8e5b56e66 100644
--- a/app/code/Magento/Integration/Test/Mftf/Test/AdminSystemIntegrationsNavigateMenuTest.xml
+++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminSystemIntegrationsNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityTest.xml
index 49557be6657bf..d8ad46887e27e 100644
--- a/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityTest.xml
+++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityWithIncorrectPasswordTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityWithIncorrectPasswordTest.xml
index c88571ca5ada6..c44396910c146 100644
--- a/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityWithIncorrectPasswordTest.xml
+++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminUpdateIntegrationEntityWithIncorrectPasswordTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/LayeredNavigation/README.md b/app/code/Magento/LayeredNavigation/README.md
index b27fa3d5360ed..0d324c2a6c2f0 100644
--- a/app/code/Magento/LayeredNavigation/README.md
+++ b/app/code/Magento/LayeredNavigation/README.md
@@ -17,6 +17,7 @@ Extension developers can interact with the Magento_LayeredNavigation module. For
### Layouts
This module introduces the following layout handles in the `view/frontend/layout` directory:
+
- `catalog_category_view_type_layered`
- `catalog_category_view_type_layered_without_children`
- `catalogsearch_result_index`
@@ -26,6 +27,7 @@ For more information about a layout in Magento 2, see the [Layout documentation]
### UI components
This module extends following ui components located in the `view/adminhtml/ui_component` directory:
+
- `product_attribute_add_form`
- `product_attributes_grid`
- `product_attributes_listing`
@@ -42,7 +44,9 @@ For information about a public API in Magento 2, see [Public interfaces & APIs](
## Additional information
### Page Layout
-This module modifies the following page_layout in the `view/frontend.page_layout` directory:
+
+This module modifies the following page_layout in the `view/frontend.page_layout` directory:
+
- `1columns` - moves block `catalog.leftnav` into the `content.top` container
- `2columns-left` - moves block `catalog.leftnav` into the `sidebar.main"` container
- `2columns-right` - moves block `catalog.leftnav` into the `sidebar.main"` container
@@ -50,5 +54,6 @@ This module modifies the following page_layout in the `view/frontend.page_layout
- `empty` - moves block `catalog.leftnav` into the `category.product.list.additional` container
More information can be found in:
+
- [Learn more about Layered Navigation](https://docs.magento.com/user-guide/catalog/navigation-layered.html)
- [Learn how to Configuring Layered Navigation](https://docs.magento.com/user-guide/catalog/navigation-layered-configuration.html)
diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml
index 5f74a0c044672..a66662080e221 100644
--- a/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml
+++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml
@@ -17,5 +17,6 @@
+
diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/AdminSpecifyLayerNavigationConfigurationTest.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/AdminSpecifyLayerNavigationConfigurationTest.xml
index 80280178e4593..19bbf499906cf 100644
--- a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/AdminSpecifyLayerNavigationConfigurationTest.xml
+++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/AdminSpecifyLayerNavigationConfigurationTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminEditUserRoleActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminEditUserRoleActionGroup.xml
index 52f5b190c3cb8..6440392164848 100644
--- a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminEditUserRoleActionGroup.xml
+++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminEditUserRoleActionGroup.xml
@@ -22,7 +22,7 @@
-
+
+
+
diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml
index 3a80bbb7a6f2e..555588585d02e 100644
--- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml
+++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml
@@ -17,6 +17,7 @@
value="Verify Admin can access customer's personal cabinet and change his default shipping and billing addresses using Login as Customer functionality"/>
+
+
+
+
+
+
diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontStickyLoginAsCustomerNotificationBannerTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontStickyLoginAsCustomerNotificationBannerTest.xml
index 611bc1044fd00..a372159857f7a 100644
--- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontStickyLoginAsCustomerNotificationBannerTest.xml
+++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontStickyLoginAsCustomerNotificationBannerTest.xml
@@ -19,6 +19,7 @@
+
= $block->escapeHtml(__('Partner search')) ?>
= $block->escapeHtml(__(
- 'Magento has a thriving ecosystem of technology partners to help merchants and brands deliver ' .
- 'the best possible customer experiences. They are recognized as experts in eCommerce, ' .
+ 'Magento has a thriving ecosystem of technology partners to help merchants and brands deliver '
+ . 'the best possible customer experiences. They are recognized as experts in eCommerce, ' .
'search, email marketing, payments, tax, fraud, optimization and analytics, fulfillment, ' .
'and more. Visit the Magento Partner Directory to see all of our trusted partners.'
)); ?>
@@ -61,7 +61,7 @@
)); ?>
+ href="https://commercemarketplace.adobe.com/">
= $block->escapeHtml(__('Visit Magento Marketplaces')) ?>
diff --git a/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetEntityContent.php b/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetEntityContent.php
index c3766484ce4f1..9136f24549289 100644
--- a/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetEntityContent.php
+++ b/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetEntityContent.php
@@ -7,7 +7,6 @@
namespace Magento\MediaContentCatalog\Model\ResourceModel;
-use Magento\Catalog\Model\ResourceModel\Product;
use Magento\Framework\App\ResourceConnection;
use Magento\MediaContentApi\Model\GetEntityContentsInterface;
use Magento\MediaContentApi\Api\Data\ContentIdentityInterface;
@@ -23,11 +22,6 @@ class GetEntityContent implements GetEntityContentsInterface
*/
private $config;
- /**
- * @var Product
- */
- private $productResource;
-
/**
* @var ResourceConnection
*/
@@ -36,15 +30,12 @@ class GetEntityContent implements GetEntityContentsInterface
/**
* @param Config $config
* @param ResourceConnection $resourceConnection
- * @param Product $productResource
*/
public function __construct(
Config $config,
- ResourceConnection $resourceConnection,
- Product $productResource
+ ResourceConnection $resourceConnection
) {
$this->config = $config;
- $this->productResource = $productResource;
$this->resourceConnection = $resourceConnection;
}
diff --git a/app/code/Magento/MediaContentSynchronization/etc/di.xml b/app/code/Magento/MediaContentSynchronization/etc/di.xml
index e5347f1a11561..622fe7cb2de99 100644
--- a/app/code/Magento/MediaContentSynchronization/etc/di.xml
+++ b/app/code/Magento/MediaContentSynchronization/etc/di.xml
@@ -19,4 +19,9 @@
+
+
+ Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface\Proxy
+
+
diff --git a/app/code/Magento/MediaGalleryApi/README.md b/app/code/Magento/MediaGalleryApi/README.md
index 8db7d800a9a78..c7a389384e5fe 100644
--- a/app/code/Magento/MediaGalleryApi/README.md
+++ b/app/code/Magento/MediaGalleryApi/README.md
@@ -34,7 +34,7 @@ Extension developers can interact with the Magento_MediaGalleryApi module. For m
- `\Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface`:
- get media gallery assets by id attribute
-
+
- `\Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface`:
- get media gallery assets by paths in media storage
diff --git a/app/code/Magento/MediaGalleryCatalogUi/README.md b/app/code/Magento/MediaGalleryCatalogUi/README.md
index f53cc0f8f328c..e6a9655d4adba 100644
--- a/app/code/Magento/MediaGalleryCatalogUi/README.md
+++ b/app/code/Magento/MediaGalleryCatalogUi/README.md
@@ -15,6 +15,7 @@ Extension developers can interact with the Magento_MediaGalleryCatalogUi module.
### Layouts
This module introduces the following layouts in the `view/adminhtml/layout` directory:
+
- `media_gallery_catalog_category_index`
For more information about a layout in Magento 2, see the [Layout documentation](https://developer.adobe.com/commerce/frontend-core/guide/layouts/).
@@ -24,9 +25,11 @@ For more information about a layout in Magento 2, see the [Layout documentation]
The configuration files located in the directory `view/adminhtml/ui_component`.
You can extend media gallery listing updates using the following configuration files:
+
- `media_gallery_category_listing`
This module extends ui components:
+
- `media_gallery_listing`
- `standalone_media_gallery_listing`
diff --git a/app/code/Magento/MediaGalleryCmsUi/README.md b/app/code/Magento/MediaGalleryCmsUi/README.md
index a7e8446de77cf..eaa218995ae16 100644
--- a/app/code/Magento/MediaGalleryCmsUi/README.md
+++ b/app/code/Magento/MediaGalleryCmsUi/README.md
@@ -17,6 +17,7 @@ Extension developers can interact with the Magento_MediaGalleryCmsUi module. For
The configuration files located in the directory `view/adminhtml/ui_component`.
This module extends ui components:
+
- `media_gallery_listing`
- `standalone_media_gallery_listing`
diff --git a/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml b/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml
index 7596de07b8922..05d2c30066518 100644
--- a/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml
+++ b/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml
@@ -9,7 +9,7 @@
/{{media url=(?:"|")(?:.renditions)?(.*?)(?:"|")}}/
- /{{media url="?(?:.*?\.renditions\/)(.*?)"?}}/
+ /{{media url="?(?:.*?\.renditions\/)?(.*?)"?}}/
/src=".*\/media\/(?:.renditions\/)*(.*?)"/
/^\/?media\/(?:.renditions\/)?(.*)/
/^\/pub\/?media\/(?:.renditions\/)?(.*)/
diff --git a/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php b/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php
index eebb172e48202..231b7e92f065e 100644
--- a/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php
+++ b/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php
@@ -12,7 +12,7 @@
use Magento\Framework\Filesystem;
use Magento\Framework\Filesystem\Directory\ReadInterface;
use Magento\Framework\Filesystem\Driver\File;
-use Magento\Framework\Stdlib\DateTime\DateTime;
+use Magento\Framework\Stdlib\DateTime\DateTimeFactory;
use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface;
use Magento\MediaGallerySynchronizationApi\Model\ImportFilesInterface;
use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface;
@@ -60,14 +60,14 @@ class SynchronizeFiles implements SynchronizeFilesInterface
private $importFiles;
/**
- * @var DateTime
+ * @var DateTimeFactory
*/
- private $date;
+ private $dateFactory;
/**
* @param File $driver
* @param Filesystem $filesystem
- * @param DateTime $date
+ * @param DateTimeFactory $dateFactory
* @param LoggerInterface $log
* @param GetFileInfo $getFileInfo
* @param GetAssetsByPathsInterface $getAssetsByPaths
@@ -76,7 +76,7 @@ class SynchronizeFiles implements SynchronizeFilesInterface
public function __construct(
File $driver,
Filesystem $filesystem,
- DateTime $date,
+ DateTimeFactory $dateFactory,
LoggerInterface $log,
GetFileInfo $getFileInfo,
GetAssetsByPathsInterface $getAssetsByPaths,
@@ -84,7 +84,7 @@ public function __construct(
) {
$this->driver = $driver;
$this->filesystem = $filesystem;
- $this->date = $date;
+ $this->dateFactory = $dateFactory;
$this->log = $log;
$this->getFileInfo = $getFileInfo;
$this->getAssetsByPaths = $getAssetsByPaths;
@@ -148,7 +148,7 @@ private function getPathsToUpdate(array $paths): array
*/
private function getFileModificationTime(string $path): string
{
- return $this->date->gmtDate(
+ return $this->dateFactory->create()->gmtDate(
self::DATE_FORMAT,
$this->getFileInfo->execute($this->getMediaDirectory()->getAbsolutePath($path))->getMTime()
);
diff --git a/app/code/Magento/MediaGallerySynchronization/etc/di.xml b/app/code/Magento/MediaGallerySynchronization/etc/di.xml
index 82bd1303eda74..9f088dbf2915a 100644
--- a/app/code/Magento/MediaGallerySynchronization/etc/di.xml
+++ b/app/code/Magento/MediaGallerySynchronization/etc/di.xml
@@ -50,4 +50,9 @@
+
+
+ Magento\MediaGallerySynchronizationApi\Api\SynchronizeInterface\Proxy
+
+
diff --git a/app/code/Magento/MediaGalleryUi/README.md b/app/code/Magento/MediaGalleryUi/README.md
index 584f242ccd425..c1dc448bc7990 100644
--- a/app/code/Magento/MediaGalleryUi/README.md
+++ b/app/code/Magento/MediaGalleryUi/README.md
@@ -17,6 +17,7 @@ Extension developers can interact with the Magento_MediaGalleryUi module. For mo
### Layouts
This module introduces the following layouts in the `view/adminhtml/layout` directory:
+
- `media_gallery_index_index`
- `media_gallery_media_index`
@@ -32,6 +33,7 @@ You can extend media gallery listing updates using the following configuration f
- `standalone_media_gallery_listing`
This module extends ui components:
+
- `cms_block_listing`
- `cms_page_listing`
- `product_listing`
diff --git a/app/code/Magento/MediaGalleryUiApi/README.md b/app/code/Magento/MediaGalleryUiApi/README.md
index f4b0d1a1d6dce..585428276f13e 100644
--- a/app/code/Magento/MediaGalleryUiApi/README.md
+++ b/app/code/Magento/MediaGalleryUiApi/README.md
@@ -11,4 +11,3 @@ For information about module installation in Magento 2, see [Enable or disable m
For information about significant changes in patch releases, see [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html).
[Learn more about New Media Gallery](https://docs.magento.com/user-guide/cms/media-gallery.html).
-
diff --git a/app/code/Magento/MediaStorage/README.md b/app/code/Magento/MediaStorage/README.md
index f77ddac816cb4..3e401c7aa6058 100644
--- a/app/code/Magento/MediaStorage/README.md
+++ b/app/code/Magento/MediaStorage/README.md
@@ -36,5 +36,6 @@ Extension developers can interact with the Magento_MediaStorage module. For more
[Learn how to manage Message Queues](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/message-queues/manage-message-queues.html).
More information can get at articles:
+
- [Learn how to configure Media Storage Database](https://docs.magento.com/user-guide/system/media-storage-database.html).
- [Learn how to Resize catalog images](https://developer.adobe.com/commerce/frontend-core/guide/themes/configure/#resize-catalog-images)
diff --git a/app/code/Magento/MediaStorage/etc/di.xml b/app/code/Magento/MediaStorage/etc/di.xml
index 5cdcbb3b2b9a9..db03601835fd7 100644
--- a/app/code/Magento/MediaStorage/etc/di.xml
+++ b/app/code/Magento/MediaStorage/etc/di.xml
@@ -28,6 +28,8 @@
+ Magento\Framework\App\State\Proxy
+ Magento\MediaStorage\Service\ImageResize\Proxy
Magento\MediaStorage\Service\ImageResizeScheduler\Proxy
diff --git a/app/code/Magento/MessageQueue/Model/CheckIsAvailableMessagesInQueue.php b/app/code/Magento/MessageQueue/Model/CheckIsAvailableMessagesInQueue.php
index c097f461e621b..49540e248319b 100644
--- a/app/code/Magento/MessageQueue/Model/CheckIsAvailableMessagesInQueue.php
+++ b/app/code/Magento/MessageQueue/Model/CheckIsAvailableMessagesInQueue.php
@@ -7,6 +7,7 @@
namespace Magento\MessageQueue\Model;
+use Magento\Framework\MessageQueue\CountableQueueInterface;
use Magento\Framework\MessageQueue\QueueRepository;
/**
@@ -40,6 +41,9 @@ public function __construct(QueueRepository $queueRepository)
public function execute($connectionName, $queueName)
{
$queue = $this->queueRepository->get($connectionName, $queueName);
+ if ($queue instanceof CountableQueueInterface) {
+ return $queue->count() > 0;
+ }
$message = $queue->dequeue();
if ($message) {
$queue->reject($message);
diff --git a/app/code/Magento/MessageQueue/Test/Unit/Model/CheckIsAvailableMessagesInQueueTest.php b/app/code/Magento/MessageQueue/Test/Unit/Model/CheckIsAvailableMessagesInQueueTest.php
new file mode 100644
index 0000000000000..2ea95960ae8e0
--- /dev/null
+++ b/app/code/Magento/MessageQueue/Test/Unit/Model/CheckIsAvailableMessagesInQueueTest.php
@@ -0,0 +1,116 @@
+queueRepository = $this->createMock(QueueRepository::class);
+ $this->model = new CheckIsAvailableMessagesInQueue(
+ $this->queueRepository
+ );
+ }
+
+ public function testExecuteNotCountableAndNotEmptyQueue(): void
+ {
+ $connectionName = 'test';
+ $queueName = 'test';
+
+ $queue = $this->getMockForAbstractClass(QueueInterface::class);
+ $message = $this->getMockForAbstractClass(EnvelopeInterface::class);
+ $this->queueRepository->expects($this->once())
+ ->method('get')
+ ->with($connectionName, $queueName)
+ ->willReturn($queue);
+ $queue->expects($this->once())
+ ->method('dequeue')
+ ->willReturn($message);
+ $queue->expects($this->once())
+ ->method('reject')
+ ->willReturn($message);
+ $this->assertTrue($this->model->execute($connectionName, $queueName));
+ }
+
+ public function testExecuteNotCountableAndEmptyQueue(): void
+ {
+ $connectionName = 'test';
+ $queueName = 'test';
+
+ $queue = $this->getMockForAbstractClass(QueueInterface::class);
+ $this->queueRepository->expects($this->once())
+ ->method('get')
+ ->with($connectionName, $queueName)
+ ->willReturn($queue);
+ $queue->expects($this->once())
+ ->method('dequeue')
+ ->willReturn(null);
+ $this->assertFalse($this->model->execute($connectionName, $queueName));
+ }
+
+ public function testExecuteCountableAndNotEmptyQueue(): void
+ {
+ $connectionName = 'test';
+ $queueName = 'test';
+
+ $queue = $this->getMockForAbstractClass(CountableQueueInterface::class);
+ $this->queueRepository->expects($this->once())
+ ->method('get')
+ ->with($connectionName, $queueName)
+ ->willReturn($queue);
+ $queue->expects($this->once())
+ ->method('count')
+ ->willReturn(1);
+ $queue->expects($this->never())
+ ->method('dequeue');
+ $this->assertTrue($this->model->execute($connectionName, $queueName));
+ }
+
+ public function testExecuteCountableAndEmptyQueue(): void
+ {
+ $connectionName = 'test';
+ $queueName = 'test';
+
+ $queue = $this->getMockForAbstractClass(CountableQueueInterface::class);
+ $this->queueRepository->expects($this->once())
+ ->method('get')
+ ->with($connectionName, $queueName)
+ ->willReturn($queue);
+ $queue->expects($this->once())
+ ->method('count')
+ ->willReturn(0);
+ $queue->expects($this->never())
+ ->method('dequeue');
+ $this->assertFalse($this->model->execute($connectionName, $queueName));
+ }
+}
diff --git a/app/code/Magento/Msrp/README.md b/app/code/Magento/Msrp/README.md
index deef2e0dcef5e..a82a5b391c5df 100644
--- a/app/code/Magento/Msrp/README.md
+++ b/app/code/Magento/Msrp/README.md
@@ -1,9 +1,10 @@
# Magento_Msrp module
-The **Magento_Msrp** module is responsible for Manufacturer’s Suggested Retail Price functionality.
+The **Magento_Msrp** module is responsible for Manufacturer's Suggested Retail Price functionality.
A current module provides base functional for msrp pricing rendering, configuration and calculation.
## Installation
+
The Magento_Msrp module creates the following attributes:
Entity type - `catalog_product`.
@@ -14,7 +15,8 @@ Attribute group - `Advanced Pricing`.
- `msrp_display_actual_price_type` -Display Actual Price
**Pay attention** if described attributes not removed when the module is removed/disabled, it would trigger errors
-because they use models and blocks from Magento_Msrp module:
+because they use models and blocks from Magento_Msrp module:
+
- `\Magento\Msrp\Block\Adminhtml\Product\Helper\Form\Type`
- `\Magento\Msrp\Model\Product\Attribute\Source\Type\Price`
- `\Magento\Msrp\Block\Adminhtml\Product\Helper\Form\Type\Price`
@@ -22,36 +24,39 @@ because they use models and blocks from Magento_Msrp module:
For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html).
## Structure
+
`Pricing\` - directory contains interfaces and implementation for msrp pricing calculations
- (`\Magento\Msrp\Pricing\MsrpPriceCalculatorInterface`), price renderers
+ (`\Magento\Msrp\Pricing\MsrpPriceCalculatorInterface`), price renderers
and price models.
-
+
`Pricing\Price\` - the directory contains declares msrp price model interfaces and implementations.
`Pricing\Renderer\` - contains price renderers implementations.
For information about a typical file structure of a module in Magento 2,
see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/#module-file-structure).
-
+
## Extensibility
-
- Developers can pass custom `msrpPriceCalculators` for `Magento\Msrp\Pricing\MsrpPriceCalculator` using type configuration using `di.xml`.
-
+
+ Developers can pass custom `msrpPriceCalculators` for `Magento\Msrp\Pricing\MsrpPriceCalculator` using type configuration using `di.xml`.
+
For example:
- ```
-
-
-
- -
-
- Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE
- - Magento\MsrpConfigurableProduct\Pricing\MsrpPriceCalculator
-
-
-
-
-```
+
+ ```xml
+
+
+
+ -
+
- Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE
+ - Magento\MsrpConfigurableProduct\Pricing\MsrpPriceCalculator
+
+
+
+
+```
+
More information about [type configuration](https://developer.adobe.com/commerce/php/development/build/dependency-injection-file/).
-
+
Extension developers can interact with the Magento_Msrp module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/).
[The Magento dependency injection mechanism](https://developer.adobe.com/commerce/php/development/components/dependency-injection/) enables you to override the functionality of the Magento_Msrp module.
@@ -62,13 +67,15 @@ This module observes the following event:
`etc/frontend/`
- - `sales_quote_collect_totals_after` in the `Magento\Msrp\Observer\Frontend\Quote\SetCanApplyMsrpObserver` file.
+ - `sales_quote_collect_totals_after` in the `Magento\Msrp\Observer\Frontend\Quote\SetCanApplyMsrpObserver` file.
`etc/webapi_rest`
- - `sales_quote_collect_totals_after` in the `Magento\Msrp\Observer\Frontend\Quote\SetCanApplyMsrpObserver` file.
+
+ - `sales_quote_collect_totals_after` in the `Magento\Msrp\Observer\Frontend\Quote\SetCanApplyMsrpObserver` file.
`etc/webapi_soap`
- - `sales_quote_collect_totals_after` in the `Magento\Msrp\Observer\Frontend\Quote\SetCanApplyMsrpObserver` file.
+
+ - `sales_quote_collect_totals_after` in the `Magento\Msrp\Observer\Frontend\Quote\SetCanApplyMsrpObserver` file.
For information about an event in Magento 2, see [Events and observers](https://developer.adobe.com/commerce/php/development/components/events-and-observers/#events).
@@ -105,7 +112,7 @@ This module introduces the following layouts and layout handles:
### UI components
-Module provides product admin form modifier:
+Module provides product admin form modifier:
`Magento\Msrp\Ui\DataProvider\Product\Form\Modifier\Msrp` - removes `msrp_display_actual_price_type` field from the form if config disabled else adds `validate-zero-or-greater` validation to the fild.
@@ -114,10 +121,13 @@ Module provides product admin form modifier:
### Catalog attributes
A current module extends `etc/catalog_attributes.xml` and provides following attributes for `quote_item` group:
+
- `msrp`
- `msrp_display_actual_price_type`
### Extension Attributes
+
The Magento_Msrp provides extension attributes for `Magento\Catalog\Api\Data\ProductRender\PriceInfoInterface`
+
- attribute code: `msrp`
- attribute type: `Magento\Msrp\Api\Data\ProductRender\MsrpPriceInfoInterface`
diff --git a/app/code/Magento/Msrp/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml b/app/code/Magento/Msrp/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml
index 874edf0dff9e3..941ede7c3538d 100644
--- a/app/code/Magento/Msrp/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml
+++ b/app/code/Magento/Msrp/Test/Mftf/Test/AdminCheckProductListPriceAttributesTest.xml
@@ -11,6 +11,7 @@
+
diff --git a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontAddMapProductToCartFromPopupOnCategoryPageTest.xml b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontAddMapProductToCartFromPopupOnCategoryPageTest.xml
index 86732ba1e18bf..2bb27f8ac9487 100644
--- a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontAddMapProductToCartFromPopupOnCategoryPageTest.xml
+++ b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontAddMapProductToCartFromPopupOnCategoryPageTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml
index 42bf5772e96e0..a12c3c69e058e 100644
--- a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml
+++ b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/MsrpConfigurableProduct/README.md b/app/code/Magento/MsrpConfigurableProduct/README.md
index 7afb0d834693c..de3160ad7c51c 100644
--- a/app/code/Magento/MsrpConfigurableProduct/README.md
+++ b/app/code/Magento/MsrpConfigurableProduct/README.md
@@ -9,8 +9,8 @@ For information about a module installation in Magento 2, see [Enable or disable
## Structure
-`Pricing\` - directory contains implementation of msrp price calculation
-for Grouped Product (`Magento\MsrpGroupedProduct\Pricing\MsrpPriceCalculator` class).
+`Pricing\` - directory contains implementation of msrp price calculation
+for Grouped Product (`Magento\MsrpGroupedProduct\Pricing\MsrpPriceCalculator` class).
For information about a typical file structure of a module in Magento 2,
see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/#module-file-structure).
diff --git a/app/code/Magento/MsrpGroupedProduct/README.md b/app/code/Magento/MsrpGroupedProduct/README.md
index 1c2a5c15a146d..605ca4714a0bb 100644
--- a/app/code/Magento/MsrpGroupedProduct/README.md
+++ b/app/code/Magento/MsrpGroupedProduct/README.md
@@ -9,8 +9,8 @@ For information about a module installation in Magento 2, see [Enable or disable
## Structure
-`Pricing\` - directory contains implementation of msrp price calculation
-for Configurable Product (`Magento\MsrpConfigurableProduct\Pricing\MsrpPriceCalculator` class).
+`Pricing\` - directory contains implementation of msrp price calculation
+for Configurable Product (`Magento\MsrpConfigurableProduct\Pricing\MsrpPriceCalculator` class).
For information about a typical file structure of a module in Magento 2,
see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/#module-file-structure).
@@ -33,7 +33,7 @@ For information about a UI component in Magento 2, see [Overview of UI component
### collection attributes
-Module adds attribute `msrp` to select for the `Magento\Catalog\Model\ResourceModel\Product\Link\Product\Collection`
+Module adds attribute `msrp` to select for the `Magento\Catalog\Model\ResourceModel\Product\Link\Product\Collection`
in `Magento\MsrpGroupedProduct\Plugin\Model\Product\Type\Grouped` plugin.
For information about significant changes in patch releases, see [2.4.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html).
diff --git a/app/code/Magento/Multishipping/README.md b/app/code/Magento/Multishipping/README.md
index 436357afa0436..2e1c88dc1818b 100644
--- a/app/code/Magento/Multishipping/README.md
+++ b/app/code/Magento/Multishipping/README.md
@@ -11,26 +11,27 @@ For information about a module installation in Magento 2, see [Enable or disable
For information about a typical file structure of a module in Magento 2,
see [Module file structure](https://developer.adobe.com/commerce/php/development/build/component-file-structure/#module-file-structure).
-
- ## Extensibility
+
+## Extensibility
Developers can interact with the module and change behaviour using type configuration feature.
-Namely, we can change `paymentSpecification` for `Magento\Multishipping\Block\Checkout\Billing` and `Magento\Multishipping\Model\Checkout\Type\Multishipping` classes.
-As result, we will get changed behaviour, new logic or something what our business need.
+Namely, we can change `paymentSpecification` for `Magento\Multishipping\Block\Checkout\Billing` and `Magento\Multishipping\Model\Checkout\Type\Multishipping` classes.
+As result, we will get changed behaviour, new logic or something what our business need.
For example:
-```
+
+```xml
multishippingPaymentSpecification
```
+
Yo can check this configuration and find more examples in the `etc/frontend/di.xml` file.
-
-More information about [type configuration](https://developer.adobe.com/commerce/php/development/build/dependency-injection-file/).
+More information about [type configuration](https://developer.adobe.com/commerce/php/development/build/dependency-injection-file/).
Extension developers can interact with the Magento_Multishipping module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://developer.adobe.com/commerce/php/development/components/plugins/).
@@ -42,7 +43,7 @@ This module observes the following event:
`etc/frontend/`
- - `checkout_cart_save_before` in the `Magento\Multishipping\Observer\DisableMultishippingObserver` file.
+ - `checkout_cart_save_before` in the `Magento\Multishipping\Observer\DisableMultishippingObserver` file.
The module dispatches the following events:
@@ -78,7 +79,7 @@ The module interacts with the following layout handles:
`view/frontend/layout` directory:
- `checkout_cart_index`
-
+
This module introduces the following layouts and layout handles:
`view/frontend/layout` directory:
@@ -135,5 +136,4 @@ Module introduces the new pages:
More information about [layout types](https://developer.adobe.com/commerce/frontend-core/guide/layouts/types/).
-
For information about significant changes in patch releases, see [2.3.x Release information](https://experienceleague.adobe.com/docs/commerce-operations/release/notes/overview.html).
diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/AssertStorefrontMultishippingAddressAndItemUKGEActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/AssertStorefrontMultishippingAddressAndItemUKGEActionGroup.xml
new file mode 100644
index 0000000000000..a31fc9a0e02fb
--- /dev/null
+++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/AssertStorefrontMultishippingAddressAndItemUKGEActionGroup.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ Verify item information on Ship to Multiple Addresses page for UK and Germany.
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontAddConfigurableProductOfSpecificColorToTheCartActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontAddConfigurableProductOfSpecificColorToTheCartActionGroup.xml
new file mode 100644
index 0000000000000..012648deae5e9
--- /dev/null
+++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontAddConfigurableProductOfSpecificColorToTheCartActionGroup.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+ Goes to the provided Storefront URL. Selects the provided Product Option under the Product Attribute. Fills in the provided Quantity. Clicks Add to Cart. Validates that the Success Message is present.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontAssertBillingAddressInBillingInfoStepGEActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontAssertBillingAddressInBillingInfoStepGEActionGroup.xml
new file mode 100644
index 0000000000000..c401098a07943
--- /dev/null
+++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontAssertBillingAddressInBillingInfoStepGEActionGroup.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ Assert that Billing Address block contains provided Address data for Germany.
+
+
+
+
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection/MultishippingSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection/MultishippingSection.xml
index 304f0a9c7a12a..5273a56bb9edf 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection/MultishippingSection.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection/MultishippingSection.xml
@@ -18,5 +18,8 @@
+
+
+
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/ShippingMethodSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/ShippingMethodSection.xml
index ef41ed3f47f3a..af62a0fc28337 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Section/ShippingMethodSection.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Section/ShippingMethodSection.xml
@@ -11,6 +11,8 @@
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutAddressesToolbarSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutAddressesToolbarSection.xml
index cf6bd10b0e8df..279967c2a3be0 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutAddressesToolbarSection.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutAddressesToolbarSection.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutBillingToolbarSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutBillingToolbarSection.xml
index 6cfc09c1653fd..c2ae07b987976 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutBillingToolbarSection.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Section/StorefrontMultishippingCheckoutBillingToolbarSection.xml
@@ -10,5 +10,7 @@
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/AdminDisablesMultishippingFunctionalityTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/AdminDisablesMultishippingFunctionalityTest.xml
index 39ad54fc66710..3972821a9871f 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Test/AdminDisablesMultishippingFunctionalityTest.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Test/AdminDisablesMultishippingFunctionalityTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/MultishipmentCheckoutWithDifferentProductTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/MultishipmentCheckoutWithDifferentProductTest.xml
new file mode 100644
index 0000000000000..cb09866438a79
--- /dev/null
+++ b/app/code/Magento/Multishipping/Test/Mftf/Test/MultishipmentCheckoutWithDifferentProductTest.xml
@@ -0,0 +1,395 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 10
+
+
+
+ 15
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml
index e22df0a8f3063..001b86c841a65 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithMultishipmentTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml
index 084e0ffc9f3ac..457a145e2c083 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontCheckingWithSingleShipmentTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontGuestCheckingWithMultishipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontGuestCheckingWithMultishipmentTest.xml
index 82563e5055c2a..07b2834fa04d4 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontGuestCheckingWithMultishipmentTest.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontGuestCheckingWithMultishipmentTest.xml
@@ -44,6 +44,7 @@
+
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml
index f05c6e355bb1d..3c40bc4f6f286 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontMinicartWithMultishipmentTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontVerifyMultishippingCheckoutForVirtualProductTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontVerifyMultishippingCheckoutForVirtualProductTest.xml
index 8108de8f9e2de..260ff06c3c5b7 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontVerifyMultishippingCheckoutForVirtualProductTest.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StoreFrontVerifyMultishippingCheckoutForVirtualProductTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml
index 815d406c68bfa..339459f66f2be 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckingWithCartPriceRuleMatchingSubtotalForMultiShipmentTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutMiniCartSubtotalMatchesAfterRemoveProductFromCartTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutMiniCartSubtotalMatchesAfterRemoveProductFromCartTest.xml
index 1d9b6e99a1ea7..5d9dd1999835f 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutMiniCartSubtotalMatchesAfterRemoveProductFromCartTest.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutMiniCartSubtotalMatchesAfterRemoveProductFromCartTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutSubtotalAfterQuantityUpdateTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutSubtotalAfterQuantityUpdateTest.xml
index 8c0df3c70677d..9d4ddd35c217a 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutSubtotalAfterQuantityUpdateTest.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutSubtotalAfterQuantityUpdateTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml
index 8205ab962b9fe..9d9d00223c725 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithMultipleAddressesTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithWithVirtualProductTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithWithVirtualProductTest.xml
index 632950120474d..582a9265fd4ba 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithWithVirtualProductTest.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithWithVirtualProductTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontDisableMultishippingModeCheckoutOnBackToCartTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontDisableMultishippingModeCheckoutOnBackToCartTest.xml
index 02ef596cda7c9..d9dc06342e3d3 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontDisableMultishippingModeCheckoutOnBackToCartTest.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontDisableMultishippingModeCheckoutOnBackToCartTest.xml
@@ -18,6 +18,7 @@
+
@@ -64,6 +65,7 @@
+
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontMultishippingUpdateProductQtyTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontMultishippingUpdateProductQtyTest.xml
index 79d2a6942e6de..00334e182b753 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontMultishippingUpdateProductQtyTest.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontMultishippingUpdateProductQtyTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml
index 4377b8cfd8c18..e5a514fcb46e2 100644
--- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml
+++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/MysqlMq/Model/Driver/Queue.php b/app/code/Magento/MysqlMq/Model/Driver/Queue.php
index cbc2e951782f2..6d29fc8aee576 100644
--- a/app/code/Magento/MysqlMq/Model/Driver/Queue.php
+++ b/app/code/Magento/MysqlMq/Model/Driver/Queue.php
@@ -5,16 +5,18 @@
*/
namespace Magento\MysqlMq\Model\Driver;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\MessageQueue\CountableQueueInterface;
use Magento\Framework\MessageQueue\EnvelopeInterface;
-use Magento\Framework\MessageQueue\QueueInterface;
use Magento\MysqlMq\Model\QueueManagement;
use Magento\Framework\MessageQueue\EnvelopeFactory;
+use Magento\MysqlMq\Model\ResourceModel\Queue as QueueResourceModel;
use Psr\Log\LoggerInterface;
/**
* Queue based on MessageQueue protocol
*/
-class Queue implements QueueInterface
+class Queue implements CountableQueueInterface
{
/**
* @var QueueManagement
@@ -46,6 +48,11 @@ class Queue implements QueueInterface
*/
private $logger;
+ /**
+ * @var QueueResourceModel
+ */
+ private $queueResourceModel;
+
/**
* Queue constructor.
*
@@ -55,6 +62,7 @@ class Queue implements QueueInterface
* @param string $queueName
* @param int $interval
* @param int $maxNumberOfTrials
+ * @param QueueResourceModel|null $queueResourceModel
*/
public function __construct(
QueueManagement $queueManagement,
@@ -62,7 +70,8 @@ public function __construct(
LoggerInterface $logger,
$queueName,
$interval = 5,
- $maxNumberOfTrials = 3
+ $maxNumberOfTrials = 3,
+ ?QueueResourceModel $queueResourceModel = null
) {
$this->queueManagement = $queueManagement;
$this->envelopeFactory = $envelopeFactory;
@@ -70,6 +79,8 @@ public function __construct(
$this->interval = $interval;
$this->maxNumberOfTrials = $maxNumberOfTrials;
$this->logger = $logger;
+ $this->queueResourceModel = $queueResourceModel
+ ?? ObjectManager::getInstance()->get(QueueResourceModel::class);
}
/**
@@ -151,4 +162,12 @@ public function push(EnvelopeInterface $envelope)
[$this->queueName]
);
}
+
+ /**
+ * @inheritDoc
+ */
+ public function count(): int
+ {
+ return $this->queueResourceModel->getMessagesCount($this->queueName);
+ }
}
diff --git a/app/code/Magento/MysqlMq/Model/ResourceModel/Queue.php b/app/code/Magento/MysqlMq/Model/ResourceModel/Queue.php
index 2a45eafc63f24..a110f1efdd0c5 100644
--- a/app/code/Magento/MysqlMq/Model/ResourceModel/Queue.php
+++ b/app/code/Magento/MysqlMq/Model/ResourceModel/Queue.php
@@ -5,6 +5,8 @@
*/
namespace Magento\MysqlMq\Model\ResourceModel;
+use Magento\Framework\DB\Select;
+use Magento\Framework\DB\Sql\Expression;
use Magento\MysqlMq\Model\QueueManagement;
/**
@@ -240,6 +242,35 @@ public function changeStatus($relationIds, $status)
);
}
+ /**
+ * Get number of pending messages in the queue
+ *
+ * @param string $queueName
+ * @return int
+ */
+ public function getMessagesCount(string $queueName): int
+ {
+ $connection = $this->getConnection();
+ $select = $connection->select()
+ ->from(
+ ['queue_message' => $this->getMessageTable()],
+ )->join(
+ ['queue_message_status' => $this->getMessageStatusTable()],
+ 'queue_message.id = queue_message_status.message_id'
+ )->join(
+ ['queue' => $this->getQueueTable()],
+ 'queue.id = queue_message_status.queue_id'
+ )->where(
+ 'queue_message_status.status IN (?)',
+ [QueueManagement::MESSAGE_STATUS_NEW, QueueManagement::MESSAGE_STATUS_RETRY_REQUIRED]
+ )->where('queue.name = ?', $queueName);
+
+ $select->reset(Select::COLUMNS);
+ $select->columns(new Expression('COUNT(*)'));
+
+ return (int) $connection->fetchOne($select);
+ }
+
/**
* Get name of table storing message statuses and associations to queues.
*
diff --git a/app/code/Magento/MysqlMq/README.md b/app/code/Magento/MysqlMq/README.md
index be9c23dda9bda..9da1e54fd787e 100644
--- a/app/code/Magento/MysqlMq/README.md
+++ b/app/code/Magento/MysqlMq/README.md
@@ -2,7 +2,7 @@
**Magento_MysqlMq** provides message queue implementation based on MySQL.
-Module contain recurring script, declared in `Magento\MysqlMq\Setup\Recurring`
+Module contain recurring script, declared in `Magento\MysqlMq\Setup\Recurring`
class. This script is executed by Magento post each schema installation or upgrade
stage and populates the queue table.
@@ -14,7 +14,6 @@ Module creates the following tables:
- `queue_message` - Queue messages
- `queue_message_status` - Relation table to keep associations between queues and messages
-
For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html).
## Additional information
diff --git a/app/code/Magento/NewRelicReporting/Model/Apm/Deployments.php b/app/code/Magento/NewRelicReporting/Model/Apm/Deployments.php
index 2b9013d594709..d1013c454a17b 100644
--- a/app/code/Magento/NewRelicReporting/Model/Apm/Deployments.php
+++ b/app/code/Magento/NewRelicReporting/Model/Apm/Deployments.php
@@ -9,6 +9,7 @@
use Laminas\Http\Request;
use Magento\Framework\HTTP\LaminasClient;
use Magento\Framework\HTTP\LaminasClientFactory;
+use Magento\Framework\Serialize\SerializerInterface;
use Magento\NewRelicReporting\Model\Config;
use Psr\Log\LoggerInterface;
@@ -37,21 +38,29 @@ class Deployments
*/
protected $clientFactory;
+ /**
+ * @var SerializerInterface
+ */
+ private $serializer;
+
/**
* Constructor
*
* @param Config $config
* @param LoggerInterface $logger
* @param LaminasClientFactory $clientFactory
+ * @param SerializerInterface $serializer
*/
public function __construct(
Config $config,
LoggerInterface $logger,
- LaminasClientFactory $clientFactory
+ LaminasClientFactory $clientFactory,
+ SerializerInterface $serializer
) {
$this->config = $config;
$this->logger = $logger;
$this->clientFactory = $clientFactory;
+ $this->serializer = $serializer;
}
/**
@@ -97,8 +106,7 @@ public function setDeployment($description, $change = false, $user = false, $rev
'revision' => $revision
]
];
-
- $client->setParameterPost($params);
+ $client->setRawBody($this->serializer->serialize($params));
try {
$response = $client->send();
diff --git a/app/code/Magento/NewRelicReporting/README.md b/app/code/Magento/NewRelicReporting/README.md
index 97bceff86da3e..a2cebb0ee45ff 100644
--- a/app/code/Magento/NewRelicReporting/README.md
+++ b/app/code/Magento/NewRelicReporting/README.md
@@ -1,10 +1,11 @@
# Magento_NewRelicReporting module
-This module implements integration New Relic APM and New Relic Insights with Magento, giving real-time visibility into business and performance metrics for data-driven decision making.
+This module implements integration New Relic APM and New Relic Insights with Magento, giving real-time visibility into business and performance metrics for data-driven decision making.
## Installation
Before installing this module, note that the Magento_NewRelicReporting is dependent on the following modules:
+
- `Magento_Store`
- `Magento_Customer`
- `Magento_Backend`
@@ -13,6 +14,7 @@ Before installing this module, note that the Magento_NewRelicReporting is depend
- `Magento_Config`
This module creates the following tables in the database:
+
- `reporting_counts`
- `reporting_module_status`
- `reporting_orders`
@@ -34,6 +36,7 @@ Extension developers can interact with the Magento_NewRelicReporting module. For
### Console commands
The Magento_NewRelicReporting provides console commands:
+
- `bin/magento newrelic:create:deploy-marker []` - check the deploy queue for entries and create an appropriate deploy marker
[Learn more about command's parameters](https://experienceleague.adobe.com/docs/commerce-operations/reference/magento-open-source.html#newreliccreatedeploy-marker).
@@ -41,6 +44,7 @@ The Magento_NewRelicReporting provides console commands:
### Cron options
Cron group configuration can be set at `etc/crontab.xml`:
+
- `magento_newrelicreporting_cron` - runs collecting all new relic reports
[Learn how to configure and run cron in Magento.](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configure-cron-jobs.html).
diff --git a/app/code/Magento/NewRelicReporting/Test/Mftf/Test/AdminCheckNewRelicSystemConfigDependencyTest.xml b/app/code/Magento/NewRelicReporting/Test/Mftf/Test/AdminCheckNewRelicSystemConfigDependencyTest.xml
index 3be9d2d8445de..a202dcf23a291 100644
--- a/app/code/Magento/NewRelicReporting/Test/Mftf/Test/AdminCheckNewRelicSystemConfigDependencyTest.xml
+++ b/app/code/Magento/NewRelicReporting/Test/Mftf/Test/AdminCheckNewRelicSystemConfigDependencyTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Apm/DeploymentsTest.php b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Apm/DeploymentsTest.php
index ae632441f1074..d64458fe8e763 100644
--- a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Apm/DeploymentsTest.php
+++ b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Apm/DeploymentsTest.php
@@ -12,6 +12,7 @@
use Laminas\Http\Response;
use Magento\Framework\HTTP\LaminasClient;
use Magento\Framework\HTTP\LaminasClientFactory;
+use Magento\Framework\Serialize\SerializerInterface;
use Magento\NewRelicReporting\Model\Apm\Deployments;
use Magento\NewRelicReporting\Model\Config;
use PHPUnit\Framework\MockObject\MockObject;
@@ -45,31 +46,24 @@ class DeploymentsTest extends TestCase
*/
protected $loggerMock;
+ /**
+ * @var SerializerInterface|MockObject
+ */
+ private $serializerMock;
+
protected function setUp(): void
{
- $this->httpClientFactoryMock = $this->getMockBuilder(LaminasClientFactory::class)
- ->setMethods(['create'])
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->httpClientMock = $this->getMockBuilder(LaminasClient::class)
- ->setMethods(['send', 'setUri', 'setMethod', 'setHeaders', 'setParameterPost'])
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->loggerMock = $this->getMockBuilder(LoggerInterface::class)
- ->disableOriginalConstructor()
- ->getMockForAbstractClass();
-
- $this->configMock = $this->getMockBuilder(Config::class)
- ->setMethods(['getNewRelicApiUrl', 'getNewRelicApiKey', 'getNewRelicAppId'])
- ->disableOriginalConstructor()
- ->getMock();
+ $this->httpClientFactoryMock = $this->createMock(LaminasClientFactory::class);
+ $this->httpClientMock = $this->createMock(LaminasClient::class);
+ $this->loggerMock = $this->createMock(LoggerInterface::class);
+ $this->configMock = $this->createMock(Config::class);
+ $this->serializerMock = $this->createMock(SerializerInterface::class);
$this->model = new Deployments(
$this->configMock,
$this->loggerMock,
- $this->httpClientFactoryMock
+ $this->httpClientFactoryMock,
+ $this->serializerMock
);
}
@@ -97,9 +91,13 @@ public function testSetDeploymentRequestOk()
->with($data['headers'])
->willReturnSelf();
- $this->httpClientMock->expects($this->once())
- ->method('setParameterPost')
+ $this->serializerMock->expects($this->once())
+ ->method('serialize')
->with($data['params'])
+ ->willReturn(json_encode($data['params']));
+ $this->httpClientMock->expects($this->once())
+ ->method('setRawBody')
+ ->with(json_encode($data['params']))
->willReturnSelf();
$this->configMock->expects($this->once())
@@ -163,9 +161,13 @@ public function testSetDeploymentBadStatus()
->with($data['headers'])
->willReturnSelf();
- $this->httpClientMock->expects($this->once())
- ->method('setParameterPost')
+ $this->serializerMock->expects($this->once())
+ ->method('serialize')
->with($data['params'])
+ ->willReturn(json_encode($data['params']));
+ $this->httpClientMock->expects($this->once())
+ ->method('setRawBody')
+ ->with(json_encode($data['params']))
->willReturnSelf();
$this->configMock->expects($this->once())
@@ -225,9 +227,13 @@ public function testSetDeploymentRequestFail()
->with($data['headers'])
->willReturnSelf();
- $this->httpClientMock->expects($this->once())
- ->method('setParameterPost')
+ $this->serializerMock->expects($this->once())
+ ->method('serialize')
->with($data['params'])
+ ->willReturn(json_encode($data['params']));
+ $this->httpClientMock->expects($this->once())
+ ->method('setRawBody')
+ ->with(json_encode($data['params']))
->willReturnSelf();
$this->configMock->expects($this->once())
diff --git a/app/code/Magento/NewRelicReporting/etc/di.xml b/app/code/Magento/NewRelicReporting/etc/di.xml
index cd8b0f46087a4..0fdce7f722e03 100644
--- a/app/code/Magento/NewRelicReporting/etc/di.xml
+++ b/app/code/Magento/NewRelicReporting/etc/di.xml
@@ -53,4 +53,9 @@
+
+
+ Magento\Framework\Serialize\Serializer\Json
+
+
diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php b/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php
index f4e72c61953f0..bd7c62e82e630 100644
--- a/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php
+++ b/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php
@@ -30,8 +30,6 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab
protected $_isStoreFilter = false;
/**
- * Date
- *
* @var \Magento\Framework\Stdlib\DateTime\DateTime
*/
protected $_date;
diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber/Collection.php b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber/Collection.php
index 64f6c066f01b6..d59bf28310778 100644
--- a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber/Collection.php
+++ b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber/Collection.php
@@ -32,7 +32,7 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab
protected $_storeTable;
/**
- * Queue joined flag
+ * Flag for joined queue
*
* @var boolean
*/
@@ -82,8 +82,7 @@ public function __construct(
}
/**
- * Constructor
- * Configures collection
+ * Constructor configures collection
*
* @return void
*/
diff --git a/app/code/Magento/Newsletter/README.md b/app/code/Magento/Newsletter/README.md
index b9aac71e222b2..b51cc7508d3fb 100644
--- a/app/code/Magento/Newsletter/README.md
+++ b/app/code/Magento/Newsletter/README.md
@@ -5,15 +5,18 @@ This module allows clients to subscribe for information about new promotions and
## Installation
Before installing this module, note that the Magento_Newsletter is dependent on the following modules:
+
- `Magento_Store`
- `Magento_Customer`
- `Magento_Eav`
- `Magento_Widget`
Before disabling or uninstalling this module, note that the following modules depends on this module:
+
- `Magento_NewsletterGraphQl`
This module creates the following tables in the database:
+
- `newsletter_subscriber`
- `newsletter_template`
- `newsletter_queue`
@@ -34,6 +37,7 @@ A lot of functionality in the module is on JavaScript, use [mixins](https://deve
### Layouts
This module introduces the following layouts in the `view/frontend/layout` and `view/adminhtml/layout` directories:
+
- `view/adminhtml/layout`:
- `newsletter_problem_block`
- `newsletter_problem_grid`
@@ -53,7 +57,7 @@ This module introduces the following layouts in the `view/frontend/layout` and `
- `newsletter_template_preview`
- `newsletter_template_preview_popup`
- `preview`
-
+
- `view/frontend/layout`:
- `customer_account`
- `customer_account_create`
@@ -65,6 +69,7 @@ For more information about a layout in Magento 2, see the [Layout documentation]
### UI components
This module extends customer form ui component the configuration file located in the `view/base/ui_component` directory:
+
- `customer_form`
For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/).
@@ -76,7 +81,7 @@ For information about a UI component in Magento 2, see [Overview of UI component
### Cron options
Cron group configuration can be set at `etc/crontab.xml`:
+
- `newsletter_send_all` - schedules newsletter sending
[Learn how to configure and run cron in Magento.](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configure-cron-jobs.html).
-
diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewsletterSubscriberActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewsletterSubscriberActionGroup.xml
index 44104f3adf0d9..e2954fcbb6f97 100644
--- a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewsletterSubscriberActionGroup.xml
+++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewsletterSubscriberActionGroup.xml
@@ -12,6 +12,7 @@
+
diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml
index be8849ce99391..de5e0bbedcbb4 100644
--- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml
+++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml
index b12629a666afc..ddd6b8ab16945 100644
--- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml
+++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddWidgetToWYSIWYGNewsletterTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingDeleteNewsletterSubscriberTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingDeleteNewsletterSubscriberTest.xml
index c472d262a34c8..5e6c618debcf9 100644
--- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingDeleteNewsletterSubscriberTest.xml
+++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingDeleteNewsletterSubscriberTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterQueueNavigateMenuTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterQueueNavigateMenuTest.xml
index d6bee2c618849..34c68efb05a0b 100644
--- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterQueueNavigateMenuTest.xml
+++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterQueueNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterSubscribersNavigateMenuTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterSubscribersNavigateMenuTest.xml
index 0db48811e8026..f02e0f61a3da1 100644
--- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterSubscribersNavigateMenuTest.xml
+++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterSubscribersNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterTemplateNavigateMenuTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterTemplateNavigateMenuTest.xml
index 93429bfd14f85..012270cf1c63f 100644
--- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterTemplateNavigateMenuTest.xml
+++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingNewsletterTemplateNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminNameEmptyForGuestTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminNameEmptyForGuestTest.xml
index 5e35f5aab60cd..ec07415e4a768 100644
--- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminNameEmptyForGuestTest.xml
+++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminNameEmptyForGuestTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminReportsNewsletterProblemReportsNavigateMenuTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminReportsNewsletterProblemReportsNavigateMenuTest.xml
index 746c786ef3dac..7e9c52f183a2f 100644
--- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminReportsNewsletterProblemReportsNavigateMenuTest.xml
+++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminReportsNewsletterProblemReportsNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml
index 1b56f12049973..54fd53d09bc6b 100644
--- a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml
+++ b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyTinyMCEIsNativeWYSIWYGOnNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyTinyMCEIsNativeWYSIWYGOnNewsletterTest.xml
index a3b2a5f93a12d..cdbab53b65784 100644
--- a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyTinyMCEIsNativeWYSIWYGOnNewsletterTest.xml
+++ b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyTinyMCEIsNativeWYSIWYGOnNewsletterTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/OfflinePayments/README.md b/app/code/Magento/OfflinePayments/README.md
index 1e9c3fb5426fb..8b34b3f2c4999 100644
--- a/app/code/Magento/OfflinePayments/README.md
+++ b/app/code/Magento/OfflinePayments/README.md
@@ -1,7 +1,8 @@
# Magento_OfflinePayments module
-This module implements the payment methods which do not require interaction with a payment gateway (so called offline methods).
+This module implements the payment methods which do not require interaction with a payment gateway (so called offline methods).
These methods are the following:
+
- Bank transfer
- Cash on delivery
- Check / Money Order
@@ -10,6 +11,7 @@ These methods are the following:
## Installation
Before installing this module, note that the Magento_OfflinePayments is dependent on the following modules:
+
- `Magento_Store`
- `Magento_Catalog`
@@ -26,6 +28,7 @@ A lot of functionality in the module is on JavaScript, use [mixins](https://deve
### Layouts
This module introduces the following layouts in the `view/frontend/layout` directory:
+
- `checkout_index_index`
- `multishipping_checkout_billing`
diff --git a/app/code/Magento/OfflinePayments/Test/Mftf/ActionGroup/StorefrontSelectCheckMoneyOrderActionGroup.xml b/app/code/Magento/OfflinePayments/Test/Mftf/ActionGroup/StorefrontSelectCheckMoneyOrderActionGroup.xml
new file mode 100644
index 0000000000000..f25b23f8b9d9b
--- /dev/null
+++ b/app/code/Magento/OfflinePayments/Test/Mftf/ActionGroup/StorefrontSelectCheckMoneyOrderActionGroup.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ Select "Check / Money Order payment method if radio button available otherwise continue."
+
+
+
+
+
diff --git a/app/code/Magento/OfflineShipping/README.md b/app/code/Magento/OfflineShipping/README.md
index c45767d3190da..440f2bc3e1541 100644
--- a/app/code/Magento/OfflineShipping/README.md
+++ b/app/code/Magento/OfflineShipping/README.md
@@ -1,7 +1,8 @@
# Magento_OfflineShipping module
-This module implements the shipping methods which do not involve a direct interaction with shipping carriers, so called offline shipping methods.
+This module implements the shipping methods which do not involve a direct interaction with shipping carriers, so called offline shipping methods.
Namely, the following:
+
- Free Shipping
- Flat Rate
- Table Rates
@@ -10,6 +11,7 @@ Namely, the following:
## Installation
Before installing this module, note that the Magento_OfflineShipping is dependent on the following modules:
+
- `Magento_Store`
- `Magento_Sales`
- `Magento_Quote`
@@ -19,6 +21,7 @@ Before installing this module, note that the Magento_OfflineShipping is dependen
The Magento_OfflineShipping module creates the `shipping_tablerate` table in the database.
This module modifies the following tables in the database:
+
- `salesrule` - adds column `simple_free_shipping`
- `sales_order_item` - adds column `free_shipping`
- `quote_address` - adds column `free_shipping`
@@ -38,6 +41,7 @@ A lot of functionality in the module is on JavaScript, use [mixins](https://deve
### Layouts
This module introduces the following layouts in the `view/frontend/layout` directory:
+
- `checkout_cart_index`
- `checkout_index_index`
@@ -46,6 +50,7 @@ For more information about a layout in Magento 2, see the [Layout documentation]
### UI components
This module extends following ui components located in the `view/adminhtml/ui_component` directory:
+
- `sales_rule_form`
- `salesrulestaging_update_form`
@@ -54,6 +59,7 @@ For information about a UI component in Magento 2, see [Overview of UI component
## Additional information
You can get more information about offline shipping methods in magento at the articles:
+
- [How to configure Free Shipping](https://docs.magento.com/user-guide/shipping/shipping-free.html)
- [How to configure Flat Rate](https://docs.magento.com/user-guide/shipping/shipping-flat-rate.html)
- [How to configure Table Rates](https://docs.magento.com/user-guide/shipping/shipping-table-rate.html)
diff --git a/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml b/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml
index d225e5fa28f97..fb3b950cd2afa 100644
--- a/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml
+++ b/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/OfflineShipping/Test/Mftf/Test/StorefrontFreeShippingShouldNotApplyIfOtherDiscountAppliedTest.xml b/app/code/Magento/OfflineShipping/Test/Mftf/Test/StorefrontFreeShippingShouldNotApplyIfOtherDiscountAppliedTest.xml
new file mode 100644
index 0000000000000..ae55756990c3a
--- /dev/null
+++ b/app/code/Magento/OfflineShipping/Test/Mftf/Test/StorefrontFreeShippingShouldNotApplyIfOtherDiscountAppliedTest.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 100.00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/OpenSearch/README.md b/app/code/Magento/OpenSearch/README.md
index 36ab12072d572..eeeffcd968ef3 100644
--- a/app/code/Magento/OpenSearch/README.md
+++ b/app/code/Magento/OpenSearch/README.md
@@ -1,3 +1,3 @@
-#Magento_OpenSearch module
+# Magento_OpenSearch module
Magento_OpenSearch module allows using OpenSearch 1.x engine for the product searching capabilities.
diff --git a/app/code/Magento/OpenSearch/Test/Mftf/Test/OpenSearchUpgradeVersion2xTest.xml b/app/code/Magento/OpenSearch/Test/Mftf/Test/OpenSearchUpgradeVersion2xTest.xml
index 7f694a1168f6c..c725bcaf69a40 100644
--- a/app/code/Magento/OpenSearch/Test/Mftf/Test/OpenSearchUpgradeVersion2xTest.xml
+++ b/app/code/Magento/OpenSearch/Test/Mftf/Test/OpenSearchUpgradeVersion2xTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/PageCache/Controller/Block.php b/app/code/Magento/PageCache/Controller/Block.php
index e69614496c66d..b32866524d9d9 100644
--- a/app/code/Magento/PageCache/Controller/Block.php
+++ b/app/code/Magento/PageCache/Controller/Block.php
@@ -1,6 +1,5 @@
translateInline = $translateInline;
$this->jsonSerializer = $jsonSerializer
- ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Json::class);
+ ?: ObjectManager::getInstance()->get(Json::class);
$this->base64jsonSerializer = $base64jsonSerializer
- ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Base64Json::class);
+ ?: ObjectManager::getInstance()->get(Base64Json::class);
$this->layoutCacheKey = $layoutCacheKey
- ?: \Magento\Framework\App\ObjectManager::getInstance()->get(LayoutCacheKeyInterface::class);
+ ?: ObjectManager::getInstance()->get(LayoutCacheKeyInterface::class);
+ $this->regexValidatorFactory = $regexValidatorFactory
+ ?: ObjectManager::getInstance()->get(RegexFactory::class);
}
/**
@@ -79,6 +94,9 @@ protected function _getBlocks()
}
$blocks = $this->jsonSerializer->unserialize($blocks);
$handles = $this->base64jsonSerializer->unserialize($handles);
+ if (!$this->validateHandleParam($handles)) {
+ return [];
+ }
$layout = $this->_view->getLayout();
$this->layoutCacheKey->addCacheKeys($this->layoutCacheKeyName);
@@ -95,4 +113,22 @@ protected function _getBlocks()
return $data;
}
+
+ /**
+ * Validates handles parameter
+ *
+ * @param array $handles
+ * @return bool
+ */
+ private function validateHandleParam($handles): bool
+ {
+ $validator = $this->regexValidatorFactory->create(['pattern' => self::VALIDATION_RULE_PATTERN]);
+ foreach ($handles as $handle) {
+ if ($handle && !$validator->isValid($handle)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
}
diff --git a/app/code/Magento/PageCache/Model/App/FrontController/BuiltinPlugin.php b/app/code/Magento/PageCache/Model/App/FrontController/BuiltinPlugin.php
index 5340f5204e21e..061cc801d5d1e 100644
--- a/app/code/Magento/PageCache/Model/App/FrontController/BuiltinPlugin.php
+++ b/app/code/Magento/PageCache/Model/App/FrontController/BuiltinPlugin.php
@@ -5,6 +5,7 @@
*/
namespace Magento\PageCache\Model\App\FrontController;
+use Magento\Framework\App\PageCache\NotCacheableInterface;
use Magento\Framework\App\Response\Http as ResponseHttp;
/**
@@ -73,7 +74,7 @@ public function aroundDispatch(
$result = $this->kernel->load();
if ($result === false) {
$result = $proceed($request);
- if ($result instanceof ResponseHttp) {
+ if ($result instanceof ResponseHttp && !$result instanceof NotCacheableInterface) {
$this->addDebugHeaders($result);
$this->kernel->process($result);
}
diff --git a/app/code/Magento/PageCache/Model/App/Request/Http/IdentifierForSave.php b/app/code/Magento/PageCache/Model/App/Request/Http/IdentifierForSave.php
new file mode 100644
index 0000000000000..26b8715c36447
--- /dev/null
+++ b/app/code/Magento/PageCache/Model/App/Request/Http/IdentifierForSave.php
@@ -0,0 +1,47 @@
+request->isSecure(),
+ $this->request->getUriString(),
+ $this->context->getVaryString()
+ ];
+
+ return sha1($this->serializer->serialize($data));
+ }
+}
diff --git a/app/code/Magento/PageCache/README.md b/app/code/Magento/PageCache/README.md
index 1b109926fd9f0..30e46cb560d55 100644
--- a/app/code/Magento/PageCache/README.md
+++ b/app/code/Magento/PageCache/README.md
@@ -1,4 +1,4 @@
The PageCache module provides functionality of caching full pages content in Magento application. An administrator may switch between built-in caching and Varnish caching. Built-in caching is default and ready to use without the need of any external tools.
Requests and responses are managed by PageCache plugin. It loads data from cache and returns a response. If data is not present in cache, it passes the request to Magento and waits for the response. Response is then saved in cache.
Blocks can be set as private blocks by setting the property '_isScopePrivate' to true. These blocks contain personalized information and are not cached in the server. These blocks are being rendered using AJAX call after the page is loaded. Contents are cached in browser instead.
-Blocks can also be set as non-cacheable by setting the 'cacheable' attribute in layout XML files. For example ` `. Pages containing such blocks are not cached.
\ No newline at end of file
+Blocks can also be set as non-cacheable by setting the 'cacheable' attribute in layout XML files. For example ` `. Pages containing such blocks are not cached.
diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml
index eeac1c2fe1124..5851c8dbac5c9 100644
--- a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml
+++ b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/FlushStaticFilesCacheButtonVisibilityTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/FlushStaticFilesCacheButtonVisibilityTest.xml
index a7cf367ff3030..4d1a174632661 100644
--- a/app/code/Magento/PageCache/Test/Mftf/Test/FlushStaticFilesCacheButtonVisibilityTest.xml
+++ b/app/code/Magento/PageCache/Test/Mftf/Test/FlushStaticFilesCacheButtonVisibilityTest.xml
@@ -31,6 +31,7 @@
-
+
+
diff --git a/app/code/Magento/PageCache/Test/Unit/Controller/Block/EsiTest.php b/app/code/Magento/PageCache/Test/Unit/Controller/Block/EsiTest.php
index c0c4eac7d5255..a003d6aa3bd1f 100644
--- a/app/code/Magento/PageCache/Test/Unit/Controller/Block/EsiTest.php
+++ b/app/code/Magento/PageCache/Test/Unit/Controller/Block/EsiTest.php
@@ -18,6 +18,8 @@
use Magento\Framework\View\Element\AbstractBlock;
use Magento\Framework\View\Layout;
use Magento\Framework\View\Layout\LayoutCacheKeyInterface;
+use Magento\Framework\Validator\Regex;
+use Magento\Framework\Validator\RegexFactory;
use Magento\PageCache\Controller\Block;
use Magento\PageCache\Controller\Block\Esi;
use Magento\PageCache\Test\Unit\Block\Controller\StubBlock;
@@ -64,6 +66,11 @@ class EsiTest extends TestCase
*/
protected $translateInline;
+ /**
+ * Validation pattern for handles array
+ */
+ private const VALIDATION_RULE_PATTERN = '/^[a-z0-9]+[a-z0-9_]*$/i';
+
/**
* Set up before test
*/
@@ -98,6 +105,16 @@ protected function setUp(): void
$this->translateInline = $this->getMockForAbstractClass(InlineInterface::class);
+ $regexFactoryMock = $this->getMockBuilder(RegexFactory::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['create'])
+ ->getMock();
+
+ $regexObject = new Regex(self::VALIDATION_RULE_PATTERN);
+
+ $regexFactoryMock->expects($this->any())->method('create')
+ ->willReturn($regexObject);
+
$helperObjectManager = new ObjectManager($this);
$this->action = $helperObjectManager->getObject(
Esi::class,
@@ -106,7 +123,8 @@ protected function setUp(): void
'translateInline' => $this->translateInline,
'jsonSerializer' => new Json(),
'base64jsonSerializer' => new Base64Json(),
- 'layoutCacheKey' => $this->layoutCacheKeyMock
+ 'layoutCacheKey' => $this->layoutCacheKeyMock,
+ 'regexValidatorFactory' => $regexFactoryMock
]
);
}
diff --git a/app/code/Magento/PageCache/Test/Unit/Controller/Block/RenderTest.php b/app/code/Magento/PageCache/Test/Unit/Controller/Block/RenderTest.php
index 89e4b06994a71..7cec177a3a0bb 100644
--- a/app/code/Magento/PageCache/Test/Unit/Controller/Block/RenderTest.php
+++ b/app/code/Magento/PageCache/Test/Unit/Controller/Block/RenderTest.php
@@ -18,6 +18,8 @@
use Magento\Framework\View\Layout;
use Magento\Framework\View\Layout\LayoutCacheKeyInterface;
use Magento\Framework\View\Layout\ProcessorInterface;
+use Magento\Framework\Validator\Regex;
+use Magento\Framework\Validator\RegexFactory;
use Magento\PageCache\Controller\Block;
use Magento\PageCache\Controller\Block\Render;
use Magento\PageCache\Test\Unit\Block\Controller\StubBlock;
@@ -69,6 +71,11 @@ class RenderTest extends TestCase
*/
protected $layoutCacheKeyMock;
+ /**
+ * Validation pattern for handles array
+ */
+ private const VALIDATION_RULE_PATTERN = '/^[a-z0-9]+[a-z0-9_]*$/i';
+
/**
* @inheritDoc
*/
@@ -111,6 +118,16 @@ protected function setUp(): void
$this->translateInline = $this->getMockForAbstractClass(InlineInterface::class);
+ $regexFactoryMock = $this->getMockBuilder(RegexFactory::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['create'])
+ ->getMock();
+
+ $regexObject = new Regex(self::VALIDATION_RULE_PATTERN);
+
+ $regexFactoryMock->expects($this->any())->method('create')
+ ->willReturn($regexObject);
+
$helperObjectManager = new ObjectManager($this);
$this->action = $helperObjectManager->getObject(
Render::class,
@@ -119,7 +136,8 @@ protected function setUp(): void
'translateInline' => $this->translateInline,
'jsonSerializer' => new Json(),
'base64jsonSerializer' => new Base64Json(),
- 'layoutCacheKey' => $this->layoutCacheKeyMock
+ 'layoutCacheKey' => $this->layoutCacheKeyMock,
+ 'regexValidatorFactory' => $regexFactoryMock
]
);
}
diff --git a/app/code/Magento/PageCache/Test/Unit/Model/App/FrontController/BuiltinPluginTest.php b/app/code/Magento/PageCache/Test/Unit/Model/App/FrontController/BuiltinPluginTest.php
index 0827f84a21192..30e0e6a0276ad 100644
--- a/app/code/Magento/PageCache/Test/Unit/Model/App/FrontController/BuiltinPluginTest.php
+++ b/app/code/Magento/PageCache/Test/Unit/Model/App/FrontController/BuiltinPluginTest.php
@@ -11,6 +11,7 @@
use Laminas\Http\Header\GenericHeader;
use Magento\Framework\App\FrontControllerInterface;
use Magento\Framework\App\PageCache\Kernel;
+use Magento\Framework\App\PageCache\NotCacheableInterface;
use Magento\Framework\App\PageCache\Version;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\App\Response\Http;
@@ -243,6 +244,41 @@ public function testAroundDispatchDisabled($state): void
);
}
+ /**
+ * @return void
+ */
+ public function testAroundNotCacheableResponse(): void
+ {
+ $this->configMock
+ ->expects($this->once())
+ ->method('getType')
+ ->willReturn(Config::BUILT_IN);
+ $this->configMock->expects($this->once())
+ ->method('isEnabled')
+ ->willReturn(true);
+ $this->versionMock
+ ->expects($this->once())
+ ->method('process');
+ $this->kernelMock->expects($this->once())
+ ->method('load')
+ ->willReturn(false);
+ $this->stateMock->expects($this->never())
+ ->method('getMode');
+ $this->kernelMock->expects($this->never())
+ ->method('process');
+ $this->responseMock->expects($this->never())
+ ->method('setHeader');
+ $notCacheableResponse = $this->createMock(NotCacheableInterface::class);
+ $this->assertSame(
+ $notCacheableResponse,
+ $this->plugin->aroundDispatch(
+ $this->frontControllerMock,
+ fn () => $notCacheableResponse,
+ $this->requestMock
+ )
+ );
+ }
+
/**
* @return array
*/
diff --git a/app/code/Magento/PageCache/Test/Unit/Model/App/Request/Http/IdentifierForSaveTest.php b/app/code/Magento/PageCache/Test/Unit/Model/App/Request/Http/IdentifierForSaveTest.php
new file mode 100644
index 0000000000000..4a9b884e6c5cb
--- /dev/null
+++ b/app/code/Magento/PageCache/Test/Unit/Model/App/Request/Http/IdentifierForSaveTest.php
@@ -0,0 +1,107 @@
+requestMock = $this->getMockBuilder(HttpRequest::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->contextMock = $this->getMockBuilder(Context::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->serializerMock = $this->getMockBuilder(Json::class)
+ ->onlyMethods(['serialize'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->serializerMock->expects($this->any())
+ ->method('serialize')
+ ->willReturnCallback(
+ function ($value) {
+ return json_encode($value);
+ }
+ );
+
+ $this->model = new IdentifierForSave(
+ $this->requestMock,
+ $this->contextMock,
+ $this->serializerMock
+ );
+ parent::setUp();
+ }
+
+ /**
+ * Test get identifier for save value.
+ *
+ * @return void
+ */
+ public function testGetValue(): void
+ {
+ $this->requestMock->expects($this->any())
+ ->method('isSecure')
+ ->willReturn(true);
+
+ $this->requestMock->expects($this->any())
+ ->method('getUriString')
+ ->willReturn('http://example.com/path1/');
+
+ $this->contextMock->expects($this->any())
+ ->method('getVaryString')
+ ->willReturn(self::VARY);
+
+ $this->assertEquals(
+ sha1(
+ json_encode(
+ [
+ true,
+ 'http://example.com/path1/',
+ self::VARY
+ ]
+ )
+ ),
+ $this->model->getValue()
+ );
+ }
+}
diff --git a/app/code/Magento/PageCache/etc/frontend/di.xml b/app/code/Magento/PageCache/etc/frontend/di.xml
index 1aaa331da7025..7f4d05ae206bf 100644
--- a/app/code/Magento/PageCache/etc/frontend/di.xml
+++ b/app/code/Magento/PageCache/etc/frontend/di.xml
@@ -26,4 +26,9 @@
+
+
+ Magento\PageCache\Model\App\Request\Http\IdentifierForSave
+
+
diff --git a/app/code/Magento/PageCache/etc/varnish4.vcl b/app/code/Magento/PageCache/etc/varnish4.vcl
index eb0c2194848ea..7622025cc296e 100644
--- a/app/code/Magento/PageCache/etc/varnish4.vcl
+++ b/app/code/Magento/PageCache/etc/varnish4.vcl
@@ -180,6 +180,18 @@ sub vcl_backend_response {
# validate if we need to cache it and prevent from setting cookie
if (beresp.ttl > 0s && (bereq.method == "GET" || bereq.method == "HEAD")) {
+ # Collapse beresp.http.set-cookie in order to merge multiple set-cookie headers
+ # Although it is not recommended to collapse set-cookie header,
+ # it is safe to do it here as the set-cookie header is removed below
+ std.collect(beresp.http.set-cookie);
+ # Do not cache the response under current cache key (hash),
+ # if the response has X-Magento-Vary but the request does not.
+ if ((bereq.url !~ "/graphql" || !bereq.http.X-Magento-Cache-Id)
+ && bereq.http.cookie !~ "X-Magento-Vary="
+ && beresp.http.set-cookie ~ "X-Magento-Vary=") {
+ set beresp.ttl = 0s;
+ set beresp.uncacheable = true;
+ }
unset beresp.http.set-cookie;
}
diff --git a/app/code/Magento/PageCache/etc/varnish5.vcl b/app/code/Magento/PageCache/etc/varnish5.vcl
index 6acd160b183aa..335ffe289e721 100644
--- a/app/code/Magento/PageCache/etc/varnish5.vcl
+++ b/app/code/Magento/PageCache/etc/varnish5.vcl
@@ -179,6 +179,18 @@ sub vcl_backend_response {
# validate if we need to cache it and prevent from setting cookie
if (beresp.ttl > 0s && (bereq.method == "GET" || bereq.method == "HEAD")) {
+ # Collapse beresp.http.set-cookie in order to merge multiple set-cookie headers
+ # Although it is not recommended to collapse set-cookie header,
+ # it is safe to do it here as the set-cookie header is removed below
+ std.collect(beresp.http.set-cookie);
+ # Do not cache the response under current cache key (hash),
+ # if the response has X-Magento-Vary but the request does not.
+ if ((bereq.url !~ "/graphql" || !bereq.http.X-Magento-Cache-Id)
+ && bereq.http.cookie !~ "X-Magento-Vary="
+ && beresp.http.set-cookie ~ "X-Magento-Vary=") {
+ set beresp.ttl = 0s;
+ set beresp.uncacheable = true;
+ }
unset beresp.http.set-cookie;
}
diff --git a/app/code/Magento/PageCache/etc/varnish6.vcl b/app/code/Magento/PageCache/etc/varnish6.vcl
index e1b2e3184613b..ee89dc8d22d7e 100644
--- a/app/code/Magento/PageCache/etc/varnish6.vcl
+++ b/app/code/Magento/PageCache/etc/varnish6.vcl
@@ -183,6 +183,18 @@ sub vcl_backend_response {
# validate if we need to cache it and prevent from setting cookie
if (beresp.ttl > 0s && (bereq.method == "GET" || bereq.method == "HEAD")) {
+ # Collapse beresp.http.set-cookie in order to merge multiple set-cookie headers
+ # Although it is not recommended to collapse set-cookie header,
+ # it is safe to do it here as the set-cookie header is removed below
+ std.collect(beresp.http.set-cookie);
+ # Do not cache the response under current cache key (hash),
+ # if the response has X-Magento-Vary but the request does not.
+ if ((bereq.url !~ "/graphql" || !bereq.http.X-Magento-Cache-Id)
+ && bereq.http.cookie !~ "X-Magento-Vary="
+ && beresp.http.set-cookie ~ "X-Magento-Vary=") {
+ set beresp.ttl = 0s;
+ set beresp.uncacheable = true;
+ }
unset beresp.http.set-cookie;
}
diff --git a/app/code/Magento/Payment/Block/Transparent/Redirect.php b/app/code/Magento/Payment/Block/Transparent/Redirect.php
index b62e86e0f831c..f52fb081cd7dd 100644
--- a/app/code/Magento/Payment/Block/Transparent/Redirect.php
+++ b/app/code/Magento/Payment/Block/Transparent/Redirect.php
@@ -67,7 +67,7 @@ public function getPostParams(): array
$params = [];
foreach ($this->_request->getPostValue() as $name => $value) {
if (!empty($value) && mb_detect_encoding($value, 'UTF-8', true) === false) {
- $value = utf8_encode($value);
+ $value = mb_convert_encoding($value, 'UTF-8', 'ISO-8859-1');
}
$params[$name] = $value;
}
diff --git a/app/code/Magento/Paypal/Model/PayLaterConfig.php b/app/code/Magento/Paypal/Model/PayLaterConfig.php
index 438ec0f0235d8..c638e7427971b 100644
--- a/app/code/Magento/Paypal/Model/PayLaterConfig.php
+++ b/app/code/Magento/Paypal/Model/PayLaterConfig.php
@@ -15,17 +15,17 @@ class PayLaterConfig
/**
* Configuration key for Styles settings
*/
- const CONFIG_KEY_STYLE = 'style';
+ public const CONFIG_KEY_STYLE = 'style';
/**
* Configuration key for Position setting
*/
- const CONFIG_KEY_POSITION = 'position';
+ public const CONFIG_KEY_POSITION = 'position';
/**
* Checkout payment step placement
*/
- const CHECKOUT_PAYMENT_PLACEMENT = 'checkout_payment';
+ public const CHECKOUT_PAYMENT_PLACEMENT = 'checkout_payment';
/**
* @var Config
@@ -91,11 +91,11 @@ public function getSectionConfig(string $section, string $key)
{
if (!array_key_exists($section, $this->configData)) {
$sectionName = $section === self::CHECKOUT_PAYMENT_PLACEMENT
- ? self::CHECKOUT_PAYMENT_PLACEMENT : "${section}page";
+ ? self::CHECKOUT_PAYMENT_PLACEMENT : "{$section}page";
$this->configData[$section] = [
- 'display' => (boolean)$this->config->getPayLaterConfigValue("${sectionName}_display"),
- 'position' => $this->config->getPayLaterConfigValue("${sectionName}_position"),
+ 'display' => (boolean)$this->config->getPayLaterConfigValue("{$sectionName}_display"),
+ 'position' => $this->config->getPayLaterConfigValue("{$sectionName}_position"),
'style' => $this->getConfigStyles($sectionName)
];
}
@@ -113,17 +113,17 @@ private function getConfigStyles(string $sectionName): array
{
$logoType = $logoPosition = $textColor = $textSize = null;
$color = $ratio = null;
- $styleLayout = $this->config->getPayLaterConfigValue("${sectionName}_stylelayout");
+ $styleLayout = $this->config->getPayLaterConfigValue("{$sectionName}_stylelayout");
if ($styleLayout === 'text') {
- $logoType = $this->config->getPayLaterConfigValue("${sectionName}_logotype");
+ $logoType = $this->config->getPayLaterConfigValue("{$sectionName}_logotype");
if ($logoType === 'primary' || $logoType === 'alternative') {
- $logoPosition = $this->config->getPayLaterConfigValue("${sectionName}_logoposition");
+ $logoPosition = $this->config->getPayLaterConfigValue("{$sectionName}_logoposition");
}
- $textColor = $this->config->getPayLaterConfigValue("${sectionName}_textcolor");
- $textSize = $this->config->getPayLaterConfigValue("${sectionName}_textsize");
+ $textColor = $this->config->getPayLaterConfigValue("{$sectionName}_textcolor");
+ $textSize = $this->config->getPayLaterConfigValue("{$sectionName}_textsize");
} elseif ($styleLayout === 'flex') {
- $color = $this->config->getPayLaterConfigValue("${sectionName}_color");
- $ratio = $this->config->getPayLaterConfigValue("${sectionName}_ratio");
+ $color = $this->config->getPayLaterConfigValue("{$sectionName}_color");
+ $ratio = $this->config->getPayLaterConfigValue("{$sectionName}_ratio");
}
return [
diff --git a/app/code/Magento/Paypal/Model/Payflow/Service/Response/Validator/CVV2Match.php b/app/code/Magento/Paypal/Model/Payflow/Service/Response/Validator/CVV2Match.php
index 705b667ab2f65..53c4a4e083181 100644
--- a/app/code/Magento/Paypal/Model/Payflow/Service/Response/Validator/CVV2Match.php
+++ b/app/code/Magento/Paypal/Model/Payflow/Service/Response/Validator/CVV2Match.php
@@ -9,41 +9,38 @@
use Magento\Paypal\Model\Payflow\Service\Response\ValidatorInterface;
use Magento\Paypal\Model\Payflow\Transparent;
-/**
- * Class CVV2Match
- */
class CVV2Match implements ValidatorInterface
{
/**
* Result of the card security code (CVV2) check
*/
- const CVV2MATCH = 'cvv2match';
+ public const CVV2MATCH = 'cvv2match';
/**
* This field returns the transaction amount, or if performing a partial authorization,
* the amount approved for the partial authorization.
*/
- const AMT = 'amt';
+ public const AMT = 'amt';
/**
* Message if validation fail
*/
- const ERROR_MESSAGE = 'Card security code does not match.';
+ public const ERROR_MESSAGE = 'Card security code does not match.';
/**#@+ Values of the response */
- const RESPONSE_YES = 'y';
+ public const RESPONSE_YES = 'y';
- const RESPONSE_NO = 'n';
+ public const RESPONSE_NO = 'n';
- const RESPONSE_NOT_SUPPORTED = 'x';
+ public const RESPONSE_NOT_SUPPORTED = 'x';
/**#@-*/
/**#@+ Validation settings payments */
- const CONFIG_ON = 1;
+ public const CONFIG_ON = 1;
- const CONFIG_OFF = 0;
+ public const CONFIG_OFF = 0;
- const CONFIG_NAME = 'avs_security_code';
+ public const CONFIG_NAME = 'avs_security_code';
/**#@-*/
/**
@@ -55,7 +52,7 @@ class CVV2Match implements ValidatorInterface
*/
public function validate(DataObject $response, Transparent $transparentModel)
{
- if ($transparentModel->getConfig()->getValue(static::CONFIG_NAME) === static::CONFIG_OFF) {
+ if ((int)$transparentModel->getConfig()->getValue(static::CONFIG_NAME) === static::CONFIG_OFF) {
return true;
}
diff --git a/app/code/Magento/Paypal/README.md b/app/code/Magento/Paypal/README.md
index 0ed4f2e90291b..555449257de5c 100644
--- a/app/code/Magento/Paypal/README.md
+++ b/app/code/Magento/Paypal/README.md
@@ -1,4 +1,5 @@
Module Magento\PayPal implements integration with the PayPal payment system. Namely, it enables the following payment methods:
+
* PayPal Express Checkout
* PayPal Payments Standard
* PayPal Payments Pro
diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalPayflowProWithValutActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalPayflowProWithValutActionGroup.xml
new file mode 100644
index 0000000000000..c97e580a2c93a
--- /dev/null
+++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalPayflowProWithValutActionGroup.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+ Goes to the 'Configuration' page for 'Payment Methods'. Fills in the provided Sample PayPal Payflow pro credentials and other details. Clicks on Save.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/EnablePayPalConfigurationActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/EnablePayPalConfigurationActionGroup.xml
index b653858f770e9..e20b38638cadb 100644
--- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/EnablePayPalConfigurationActionGroup.xml
+++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/EnablePayPalConfigurationActionGroup.xml
@@ -23,8 +23,10 @@
+
+
diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml
index a2c7b7d82a349..0a1077e0c18eb 100644
--- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml
+++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml
@@ -19,11 +19,11 @@
-
+
-
+
diff --git a/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml
index 95e69cf6e93cf..4e88bbe73e2e6 100644
--- a/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml
+++ b/app/code/Magento/Paypal/Test/Mftf/Data/PaypalData.xml
@@ -214,7 +214,7 @@
1111
- 01/2030
+ 1/2030
rlus_1349181941_biz@ebay.com
@@ -223,4 +223,10 @@
AFcWxV21C7fd0v3bYYYRCpSSRl31AqoP3QLd.JUUpDPuPpQIgT0-m401
54Z2EE6T7PRB4
+
+ PayPal
+ MksGLTest
+ MksGLTest
+ Abcd@123
+
diff --git a/app/code/Magento/Paypal/Test/Mftf/Section/OtherPayPalPaymentsConfigSection/PayPalPayflowProConfigSection.xml b/app/code/Magento/Paypal/Test/Mftf/Section/OtherPayPalPaymentsConfigSection/PayPalPayflowProConfigSection.xml
new file mode 100644
index 0000000000000..9f4b2a6a47f19
--- /dev/null
+++ b/app/code/Magento/Paypal/Test/Mftf/Section/OtherPayPalPaymentsConfigSection/PayPalPayflowProConfigSection.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInFranceTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInFranceTest.xml
index 3b70bc84037ce..c66df74869e5a 100644
--- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInFranceTest.xml
+++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInFranceTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInHongKongTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInHongKongTest.xml
index 038ee1c04c482..8286f30fd515b 100644
--- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInHongKongTest.xml
+++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInHongKongTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInItalyTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInItalyTest.xml
index ad24d2c2c95d5..561c98131c744 100644
--- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInItalyTest.xml
+++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInItalyTest.xml
@@ -15,7 +15,9 @@
+
+
diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInJapanTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInJapanTest.xml
index 846f4e6dd5ae4..7c7a2e6100f1e 100644
--- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInJapanTest.xml
+++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInJapanTest.xml
@@ -15,7 +15,9 @@
+
+
diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInSpainTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInSpainTest.xml
index b0317f9ac7a3d..05ae84d70d3a1 100644
--- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInSpainTest.xml
+++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInSpainTest.xml
@@ -15,7 +15,9 @@
+
+
diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml
index a616c0bb2c68b..2af6a73cf1a14 100644
--- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml
+++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminReportsPayPalSettlementNavigateMenuTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminReportsPayPalSettlementNavigateMenuTest.xml
index 778473abb2cc7..b46565fcc99f9 100644
--- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminReportsPayPalSettlementNavigateMenuTest.xml
+++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminReportsPayPalSettlementNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminSalesBillingAgreementsNavigateMenuTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminSalesBillingAgreementsNavigateMenuTest.xml
index 3bd778620f563..f66d2bae639dc 100644
--- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminSalesBillingAgreementsNavigateMenuTest.xml
+++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminSalesBillingAgreementsNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/DeleteSavedWithPayflowProCreditCardFromCustomerAccountTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/DeleteSavedWithPayflowProCreditCardFromCustomerAccountTest.xml
new file mode 100644
index 0000000000000..5fc0469a1b096
--- /dev/null
+++ b/app/code/Magento/Paypal/Test/Mftf/Test/DeleteSavedWithPayflowProCreditCardFromCustomerAccountTest.xml
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/EditOrderFromAdminWithSavedWithinPayPalPayflowProCreditCardForRegisteredCustomerTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/EditOrderFromAdminWithSavedWithinPayPalPayflowProCreditCardForRegisteredCustomerTest.xml
new file mode 100644
index 0000000000000..f16c1df71a129
--- /dev/null
+++ b/app/code/Magento/Paypal/Test/Mftf/Test/EditOrderFromAdminWithSavedWithinPayPalPayflowProCreditCardForRegisteredCustomerTest.xml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/EnablePaypalExpressCheckoutAndSubmitAnOrderUsingPaypalExpressCheckoutTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/EnablePaypalExpressCheckoutAndSubmitAnOrderUsingPaypalExpressCheckoutTest.xml
new file mode 100644
index 0000000000000..42606464c4a9d
--- /dev/null
+++ b/app/code/Magento/Paypal/Test/Mftf/Test/EnablePaypalExpressCheckoutAndSubmitAnOrderUsingPaypalExpressCheckoutTest.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ John1
+ Doe1
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Paypal/Test/Unit/Controller/ExpressTest.php b/app/code/Magento/Paypal/Test/Unit/Controller/ExpressTest.php
index c164d832ad460..25f99bf7acc38 100644
--- a/app/code/Magento/Paypal/Test/Unit/Controller/ExpressTest.php
+++ b/app/code/Magento/Paypal/Test/Unit/Controller/ExpressTest.php
@@ -32,6 +32,7 @@ abstract class ExpressTest extends TestCase
/** @var Express */
protected $model;
+ /** @var string */
protected $name = '';
/** @var Session|MockObject */
@@ -75,7 +76,7 @@ abstract class ExpressTest extends TestCase
protected function setUp(): void
{
- $this->markTestIncomplete();
+ $this->markTestSkipped();
$this->messageManager = $this->getMockForAbstractClass(ManagerInterface::class);
$this->config = $this->createMock(Config::class);
$this->request = $this->createMock(Http::class);
diff --git a/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Response/Validator/CVV2MatchTest.php b/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Response/Validator/CVV2MatchTest.php
index affb335491c52..b2179fb32fe58 100644
--- a/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Response/Validator/CVV2MatchTest.php
+++ b/app/code/Magento/Paypal/Test/Unit/Model/Payflow/Service/Response/Validator/CVV2MatchTest.php
@@ -137,6 +137,15 @@ public function validationDataProvider()
'response' => new DataObject(),
'configValue' => '1',
],
+ [
+ 'expectedResult' => true,
+ 'response' => new DataObject(
+ [
+ 'cvv2match' => 'N',
+ ]
+ ),
+ 'configValue' => '0',
+ ],
];
}
}
diff --git a/app/code/Magento/PaypalCaptcha/README.md b/app/code/Magento/PaypalCaptcha/README.md
index 71588599a5ecd..02b1cd3c3f93a 100644
--- a/app/code/Magento/PaypalCaptcha/README.md
+++ b/app/code/Magento/PaypalCaptcha/README.md
@@ -1 +1 @@
-The PayPal Captcha module provides a possibility to enable Captcha validation on Payflow Pro payment form.
\ No newline at end of file
+The PayPal Captcha module provides a possibility to enable Captcha validation on Payflow Pro payment form.
diff --git a/app/code/Magento/Persistent/Model/Plugin/LoginAsCustomerCleanUp.php b/app/code/Magento/Persistent/Model/Plugin/LoginAsCustomerCleanUp.php
new file mode 100644
index 0000000000000..4611cc5f04876
--- /dev/null
+++ b/app/code/Magento/Persistent/Model/Plugin/LoginAsCustomerCleanUp.php
@@ -0,0 +1,41 @@
+persistentSession = $persistentSession;
+ }
+
+ /**
+ * Disable persistence for sales representative login
+ *
+ * @param AuthenticateCustomerBySecretInterface $subject
+ * @return void
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function afterExecute(AuthenticateCustomerBySecretInterface $subject)
+ {
+ if ($this->persistentSession->isPersistent()) {
+ $this->persistentSession->getSession()->removePersistentCookie();
+ }
+ }
+}
diff --git a/app/code/Magento/Persistent/README.md b/app/code/Magento/Persistent/README.md
index abbf4482eda6c..3d2f19e4fc91b 100644
--- a/app/code/Magento/Persistent/README.md
+++ b/app/code/Magento/Persistent/README.md
@@ -9,12 +9,14 @@ checkbox during first login.
## Installation
Before installing this module, note that the Magento_Persistent is dependent on the following modules:
+
- `Magento_Checkout`
- `Magento_PageCache`
The Magento_Persistent module creates the `persistent_session` table in the database.
This module modifies the following tables in the database:
+
- `quote` - adds column `is_persistent`
All database schema changes made by this module are rolled back when the module gets disabled and setup:upgrade command is run.
@@ -50,12 +52,14 @@ For more information about a layout in Magento 2, see the [Layout documentation]
## Additional information
More information can get at articles:
+
- [Persistent Shopping Cart](https://docs.magento.com/user-guide/configuration/customers/persistent-shopping-cart.html)
- [Persistent Cart](https://experienceleague.adobe.com/docs/commerce-admin/stores-sales/point-of-purchase/cart/cart-persistent.html)
### Cron options
Cron group configuration can be set at `etc/crontab.xml`:
+
- `persistent_clear_expired` - clear expired persistent sessions
[Learn how to configure and run cron in Magento.](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configure-cron-jobs.html).
diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/GuestCheckoutWithEnabledPersistentTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/GuestCheckoutWithEnabledPersistentTest.xml
index 1f944432ac1d1..816b63d8748ec 100644
--- a/app/code/Magento/Persistent/Test/Mftf/Test/GuestCheckoutWithEnabledPersistentTest.xml
+++ b/app/code/Magento/Persistent/Test/Mftf/Test/GuestCheckoutWithEnabledPersistentTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml
index f094c4f07475d..b98ec88222e2d 100644
--- a/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml
+++ b/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml
index ebc3aee9d2fd2..3dcd52629e1fc 100644
--- a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml
+++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceTest.xml
index 814ca782e28d2..e47ab9187010c 100644
--- a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceTest.xml
+++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Persistent/Test/Unit/Model/Plugin/LoginAsCustomerCleanUpTest.php b/app/code/Magento/Persistent/Test/Unit/Model/Plugin/LoginAsCustomerCleanUpTest.php
new file mode 100644
index 0000000000000..43519f362f590
--- /dev/null
+++ b/app/code/Magento/Persistent/Test/Unit/Model/Plugin/LoginAsCustomerCleanUpTest.php
@@ -0,0 +1,54 @@
+persistentSessionMock = $this->createMock(PersistentSession::class);
+ $this->persistentSessionModelMock = $this->createMock(\Magento\Persistent\Model\Session::class);
+ $this->persistentSessionMock->method('getSession')->willReturn($this->persistentSessionModelMock);
+ $this->subjectMock = $this->createMock(AuthenticateCustomerBySecretInterface::class);
+ $this->plugin = new LoginAsCustomerCleanUp($this->persistentSessionMock);
+ }
+
+ public function testBeforeExecute()
+ {
+ $this->persistentSessionMock->expects($this->once())->method('isPersistent')->willReturn(true);
+ $this->persistentSessionModelMock->expects($this->once())->method('removePersistentCookie');
+ $result = $this->plugin->afterExecute($this->subjectMock);
+ $this->assertEquals(null, $result);
+ }
+}
diff --git a/app/code/Magento/Persistent/composer.json b/app/code/Magento/Persistent/composer.json
index 5a8ff5d7f3d5f..6c943c4b37f82 100644
--- a/app/code/Magento/Persistent/composer.json
+++ b/app/code/Magento/Persistent/composer.json
@@ -14,6 +14,9 @@
"magento/module-quote": "*",
"magento/module-store": "*"
},
+ "suggest": {
+ "magento/module-login-as-customer-api": "*"
+ },
"type": "magento2-module",
"license": [
"OSL-3.0",
diff --git a/app/code/Magento/Persistent/etc/frontend/di.xml b/app/code/Magento/Persistent/etc/frontend/di.xml
index 3351963231277..498b59b7e4c45 100644
--- a/app/code/Magento/Persistent/etc/frontend/di.xml
+++ b/app/code/Magento/Persistent/etc/frontend/di.xml
@@ -57,4 +57,7 @@
Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentProcessor\Proxy
+
+
+
diff --git a/app/code/Magento/ProductAlert/Model/Mailing/AlertProcessor.php b/app/code/Magento/ProductAlert/Model/Mailing/AlertProcessor.php
index 988e5e91e1e81..a77ca851ac746 100644
--- a/app/code/Magento/ProductAlert/Model/Mailing/AlertProcessor.php
+++ b/app/code/Magento/ProductAlert/Model/Mailing/AlertProcessor.php
@@ -7,7 +7,6 @@
namespace Magento\ProductAlert\Model\Mailing;
-use Magento\Framework\App\Area;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Helper\Data;
@@ -25,15 +24,11 @@
use Magento\Store\Api\Data\WebsiteInterface;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Store\Model\Website;
-use Magento\Framework\App\ObjectManager;
-use Magento\Framework\View\DesignInterface;
/**
* Class for mailing Product Alerts
*
- * @SuppressWarnings(PHPMD.ExcessiveParameterList)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
- * @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
class AlertProcessor
{
@@ -85,11 +80,6 @@ class AlertProcessor
*/
private $errorEmailSender;
- /**
- * @var DesignInterface
- */
- private $design;
-
/**
* @param EmailFactory $emailFactory
* @param PriceCollectionFactory $priceCollectionFactory
@@ -100,7 +90,6 @@ class AlertProcessor
* @param ProductSalability $productSalability
* @param StoreManagerInterface $storeManager
* @param ErrorEmailSender $errorEmailSender
- * @param DesignInterface|null $design
*/
public function __construct(
EmailFactory $emailFactory,
@@ -111,8 +100,7 @@ public function __construct(
Data $catalogData,
ProductSalability $productSalability,
StoreManagerInterface $storeManager,
- ErrorEmailSender $errorEmailSender,
- DesignInterface $design = null
+ ErrorEmailSender $errorEmailSender
) {
$this->emailFactory = $emailFactory;
$this->priceCollectionFactory = $priceCollectionFactory;
@@ -123,8 +111,6 @@ public function __construct(
$this->productSalability = $productSalability;
$this->storeManager = $storeManager;
$this->errorEmailSender = $errorEmailSender;
- $this->design = $design ?: ObjectManager::getInstance()
- ->get(DesignInterface::class);
}
/**
@@ -159,12 +145,6 @@ public function process(string $alertType, array $customerIds, int $websiteId):
*/
private function processAlerts(string $alertType, array $customerIds, int $websiteId): array
{
- //Set the current design theme
- $this->design->setDesignTheme(
- $this->design->getConfigurationDesignTheme(Area::AREA_FRONTEND),
- Area::AREA_FRONTEND
- );
-
/** @var Email $email */
$email = $this->emailFactory->create();
$email->setType($alertType);
diff --git a/app/code/Magento/ProductAlert/README.md b/app/code/Magento/ProductAlert/README.md
index 76ee9f8066cd5..1d54f5e7b811b 100644
--- a/app/code/Magento/ProductAlert/README.md
+++ b/app/code/Magento/ProductAlert/README.md
@@ -5,16 +5,18 @@ This module enables product alerts, which allow customers to sign up for emails
## Installation
Before installing this module, note that the Magento_ProductAlert is dependent on the following modules:
+
- `Magento_Catalog`
- `Magento_Customer`
The Magento_ProductAlert module creates the following tables in the database:
+
- `product_alert_price`
- `product_alert_stock`
All database schema changes made by this module are rolled back when the module gets disabled and setup:upgrade command is run.
-The Magento_ProductAlert module contains the recurring script. Script's modifications don't need to be manually reverted upon uninstallation.
+The Magento_ProductAlert module contains the recurring script. Script's modifications don't need to be manually reverted upon uninstallation.
For information about a module installation in Magento 2, see [Enable or disable modules](https://experienceleague.adobe.com/docs/commerce-operations/installation-guide/tutorials/manage-modules.html).
@@ -27,6 +29,7 @@ Extension developers can interact with the Magento_ProductAlert module. For more
### Layouts
This module introduces the following layouts in the `view/frontend/layout` directory:
+
- `catalog_product_view`
- `productalert_unsubscribe_email`
@@ -35,13 +38,14 @@ For more information about a layout in Magento 2, see the [Layout documentation]
## Additional information
More information can get at articles:
+
- [Product Alerts](https://docs.magento.com/user-guide/catalog/inventory-product-alerts.html)
- [Product Alert Run Settings](https://docs.magento.com/user-guide/catalog/inventory-product-alert-run-settings.html)
### Cron options
Cron group configuration can be set at `etc/crontab.xml`:
+
- `catalog_product_alert` - send product alerts to customers
[Learn how to configure and run cron in Magento.](https://experienceleague.adobe.com/docs/commerce-operations/configuration-guide/cli/configure-cron-jobs.html).
-
diff --git a/app/code/Magento/ProductAlert/Test/Fixture/PriceAlert.php b/app/code/Magento/ProductAlert/Test/Fixture/PriceAlert.php
new file mode 100644
index 0000000000000..c9b8e331ad7a6
--- /dev/null
+++ b/app/code/Magento/ProductAlert/Test/Fixture/PriceAlert.php
@@ -0,0 +1,79 @@
+ null,
+ 'product_id' => null,
+ 'store_id' => 1,
+ 'website_id' => null,
+ 'price' => 11,
+ ];
+
+ /**
+ * @var PriceFactory
+ */
+ private PriceFactory $factory;
+
+ /**
+ * @var Price
+ */
+ private Price $resourceModel;
+
+ /**
+ * @var StoreManagerInterface
+ */
+ private StoreManagerInterface $storeManager;
+
+ /**
+ * @param PriceFactory $factory
+ * @param Price $resourceModel
+ * @param StoreManagerInterface $storeManager
+ */
+ public function __construct(
+ PriceFactory $factory,
+ Price $resourceModel,
+ StoreManagerInterface $storeManager
+ ) {
+ $this->factory = $factory;
+ $this->resourceModel = $resourceModel;
+ $this->storeManager = $storeManager;
+ }
+
+ /**
+ * {@inheritdoc}
+ * @param array $data Parameters
+ *
+ * $data = [
+ * 'customer_id' => (int) Customer ID. Required.
+ * 'product_id' => (int) Product ID. Required.
+ * 'store_id' => (int) Store ID. Optional. Default: default store.
+ * 'website_id' => (int) Website ID. Optional. Default: default website.
+ * 'price' => (float) Initial Price. Optional. Default: 11.
+ * ]
+ *
+ */
+ public function apply(array $data = []): ?DataObject
+ {
+ $data = array_merge(self::DEFAULT_DATA, $data);
+ $data['website_id'] ??= $this->storeManager->getStore($data['store_id'])->getWebsiteId();
+ $model = $this->factory->create();
+ $model->addData($data);
+ $this->resourceModel->save($model);
+
+ return $model;
+ }
+}
diff --git a/app/code/Magento/ProductAlert/Test/Fixture/StockAlert.php b/app/code/Magento/ProductAlert/Test/Fixture/StockAlert.php
new file mode 100644
index 0000000000000..09d47ddc2b1aa
--- /dev/null
+++ b/app/code/Magento/ProductAlert/Test/Fixture/StockAlert.php
@@ -0,0 +1,79 @@
+ null,
+ 'product_id' => null,
+ 'store_id' => 1,
+ 'website_id' => null,
+ 'status' => 0,
+ ];
+
+ /**
+ * @var StockFactory
+ */
+ private StockFactory $factory;
+
+ /**
+ * @var Stock
+ */
+ private Stock $resourceModel;
+
+ /**
+ * @var StoreManagerInterface
+ */
+ private StoreManagerInterface $storeManager;
+
+ /**
+ * @param StockFactory $factory
+ * @param Stock $resourceModel
+ * @param StoreManagerInterface $storeManager
+ */
+ public function __construct(
+ StockFactory $factory,
+ Stock $resourceModel,
+ StoreManagerInterface $storeManager
+ ) {
+ $this->factory = $factory;
+ $this->resourceModel = $resourceModel;
+ $this->storeManager = $storeManager;
+ }
+
+ /**
+ * {@inheritdoc}
+ * @param array $data Parameters
+ *
+ * $data = [
+ * 'customer_id' => (int) Customer ID. Required.
+ * 'product_id' => (int) Product ID. Required.
+ * 'store_id' => (int) Store ID. Optional. Default: default store.
+ * 'website_id' => (int) Website ID. Optional. Default: default website.
+ * 'status' => (int) Alert Status. Optional. Default: 0.
+ * ]
+ *
+ */
+ public function apply(array $data = []): ?DataObject
+ {
+ $data = array_merge(self::DEFAULT_DATA, $data);
+ $data['website_id'] ??= $this->storeManager->getStore($data['store_id'])->getWebsiteId();
+ $model = $this->factory->create();
+ $model->addData($data);
+ $this->resourceModel->save($model);
+
+ return $model;
+ }
+}
diff --git a/app/code/Magento/ProductVideo/README.md b/app/code/Magento/ProductVideo/README.md
index a36a1f777c655..f3b9926dd111b 100644
--- a/app/code/Magento/ProductVideo/README.md
+++ b/app/code/Magento/ProductVideo/README.md
@@ -5,6 +5,7 @@ This module implements functionality related with linking video files from exter
## Installation
Before installing this module, note that the Magento_ProductAlert is dependent on the following modules:
+
- `Magento_Catalog`
- `Magento_Backend`
@@ -25,6 +26,7 @@ A lot of functionality in the module is on JavaScript, use [mixins](https://deve
### Layouts
This module introduces the following layouts in the `view/frontend/layout` and `view/adminhtml/layout` directories:
+
- `view/adminhtml/layout`
- `catalog_product_new`
- `view/frontend/layout`
@@ -35,6 +37,7 @@ For more information about a layout in Magento 2, see the [Layout documentation]
### UI components
This module extends following ui components located in the `view/adminhtml/ui_component` directory:
+
- `product_form`
For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/).
@@ -42,5 +45,6 @@ For information about a UI component in Magento 2, see [Overview of UI component
## Additional information
More information can get at articles:
+
- [Learn how to add Product Video](https://docs.magento.com/user-guide/catalog/product-video.html)
- [Learn how to configure Product Video](https://developer.adobe.com/commerce/frontend-core/guide/themes/product-video/)
diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminUploadSameVimeoVideoForMultipleProductsTest.xml b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminUploadSameVimeoVideoForMultipleProductsTest.xml
index 5b346040db818..d3ce3159187c5 100644
--- a/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminUploadSameVimeoVideoForMultipleProductsTest.xml
+++ b/app/code/Magento/ProductVideo/Test/Mftf/Test/AdminUploadSameVimeoVideoForMultipleProductsTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Test/StorefrontProductVideoAutoplayOnGalleryFullscreenModeTest.xml b/app/code/Magento/ProductVideo/Test/Mftf/Test/StorefrontProductVideoAutoplayOnGalleryFullscreenModeTest.xml
new file mode 100644
index 0000000000000..3086ee1979f1f
--- /dev/null
+++ b/app/code/Magento/ProductVideo/Test/Mftf/Test/StorefrontProductVideoAutoplayOnGalleryFullscreenModeTest.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $grabAllowAttribute
+ autoplay
+
+
+
+
diff --git a/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js b/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js
index 670d91febe9f7..a8a168c03aa95 100644
--- a/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js
+++ b/app/code/Magento/ProductVideo/view/frontend/web/js/fotorama-add-video-events.js
@@ -558,7 +558,7 @@ define([
}
if (this.isFullscreen && this.fotoramaItem.data('fotorama').activeFrame.i === number) {
- this.fotoramaItem.data('fotorama').activeFrame.$stageFrame[0].trigger('click');
+ this.fotoramaItem.data('fotorama').activeFrame.$stageFrame.trigger('click');
}
},
@@ -700,7 +700,7 @@ define([
if (activeIndexIsBase && number === 1 && $(window).width() > this.MobileMaxWidth) {
setTimeout($.proxy(function () {
fotorama.requestFullScreen();
- this.fotoramaItem.data('fotorama').activeFrame.$stageFrame[0].trigger('click');
+ this.fotoramaItem.data('fotorama').activeFrame.$stageFrame.trigger('click');
this.Base = false;
}, this), 50);
}
diff --git a/app/code/Magento/Quote/Model/BillingAddressManagement.php b/app/code/Magento/Quote/Model/BillingAddressManagement.php
index 6f8a44dff464c..9ed4f5ecd516b 100644
--- a/app/code/Magento/Quote/Model/BillingAddressManagement.php
+++ b/app/code/Magento/Quote/Model/BillingAddressManagement.php
@@ -3,14 +3,15 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
namespace Magento\Quote\Model;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Exception\InputException;
-use Magento\Quote\Model\Quote\Address\BillingAddressPersister;
-use Psr\Log\LoggerInterface as Logger;
use Magento\Quote\Api\BillingAddressManagementInterface;
-use Magento\Framework\App\ObjectManager;
+use Magento\Quote\Api\Data\AddressInterface;
+use Psr\Log\LoggerInterface as Logger;
/**
* Quote billing address write service object.
@@ -25,14 +26,14 @@ class BillingAddressManagement implements BillingAddressManagementInterface
protected $addressValidator;
/**
- * Logger.
+ * Logger object.
*
* @var Logger
*/
protected $logger;
/**
- * Quote repository.
+ * Quote repository object.
*
* @var \Magento\Quote\Api\CartRepositoryInterface
*/
@@ -72,10 +73,14 @@ public function __construct(
* @inheritdoc
* @SuppressWarnings(PHPMD.NPathComplexity)
*/
- public function assign($cartId, \Magento\Quote\Api\Data\AddressInterface $address, $useForShipping = false)
+ public function assign($cartId, AddressInterface $address, $useForShipping = false)
{
/** @var \Magento\Quote\Model\Quote $quote */
$quote = $this->quoteRepository->getActive($cartId);
+
+ // validate the address
+ $this->addressValidator->validateWithExistingAddress($quote, $address);
+
$address->setCustomerId($quote->getCustomerId());
$quote->removeAddress($quote->getBillingAddress()->getId());
$quote->setBillingAddress($address);
@@ -104,6 +109,7 @@ public function get($cartId)
*
* @return \Magento\Quote\Model\ShippingAddressAssignment
* @deprecated 101.0.0
+ * @see \Magento\Quote\Model\ShippingAddressAssignment
*/
private function getShippingAddressAssignment()
{
diff --git a/app/code/Magento/Quote/Model/Cart/ProductReader.php b/app/code/Magento/Quote/Model/Cart/ProductReader.php
index 6a333e8b9b795..1dd127977d686 100644
--- a/app/code/Magento/Quote/Model/Cart/ProductReader.php
+++ b/app/code/Magento/Quote/Model/Cart/ProductReader.php
@@ -62,6 +62,7 @@ public function loadProducts(array $skus, int $storeId): void
$this->productCollection->addFieldToFilter(ProductInterface::SKU, ['in' => $skus]);
$this->productCollection->joinAttribute('status', 'catalog_product/status', 'entity_id', null, 'inner');
$this->productCollection->joinAttribute('visibility', 'catalog_product/visibility', 'entity_id', null, 'inner');
+ $this->productCollection->addOptionsToResult();
$this->productCollection->load();
foreach ($this->productCollection->getItems() as $productItem) {
$this->productsBySku[$productItem->getData(ProductInterface::SKU)] = $productItem;
diff --git a/app/code/Magento/Quote/Model/Quote.php b/app/code/Magento/Quote/Model/Quote.php
index 572d87d5f4bec..7270734e3ff4e 100644
--- a/app/code/Magento/Quote/Model/Quote.php
+++ b/app/code/Magento/Quote/Model/Quote.php
@@ -985,6 +985,8 @@ public function assignCustomerWithAddressChange(
/**
* Define customer object
*
+ * Important: This method also copies customer data to quote and removes quote addresses
+ *
* @param \Magento\Customer\Api\Data\CustomerInterface $customer
* @return $this
*/
diff --git a/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php b/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php
index 6fdb70350ed72..9f28f52adee97 100644
--- a/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php
+++ b/app/code/Magento/Quote/Model/Quote/Address/BillingAddressPersister.php
@@ -5,12 +5,13 @@
*/
namespace Magento\Quote\Model\Quote\Address;
+use Magento\Customer\Api\AddressRepositoryInterface;
use Magento\Framework\Exception\InputException;
-use Magento\Quote\Api\Data\CartInterface;
-use Magento\Quote\Api\Data\AddressInterface;
+use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Quote\Api\Data\AddressInterface;
+use Magento\Quote\Api\Data\CartInterface;
use Magento\Quote\Model\QuoteAddressValidator;
-use Magento\Customer\Api\AddressRepositoryInterface;
/**
* Saves billing address for quotes.
@@ -47,7 +48,7 @@ public function __construct(
* @param bool $useForShipping
* @return void
* @throws NoSuchEntityException
- * @throws InputException
+ * @throws InputException|LocalizedException
*/
public function save(CartInterface $quote, AddressInterface $address, $useForShipping = false)
{
@@ -55,8 +56,6 @@ public function save(CartInterface $quote, AddressInterface $address, $useForShi
$this->addressValidator->validateForCart($quote, $address);
$customerAddressId = $address->getCustomerAddressId();
$shippingAddress = null;
- $addressData = [];
-
if ($useForShipping) {
$shippingAddress = $address;
}
@@ -64,13 +63,13 @@ public function save(CartInterface $quote, AddressInterface $address, $useForShi
if ($customerAddressId) {
try {
$addressData = $this->addressRepository->getById($customerAddressId);
+ $address = $quote->getBillingAddress()->importCustomerAddressData($addressData);
+ if ($useForShipping) {
+ $shippingAddress = $quote->getShippingAddress()->importCustomerAddressData($addressData);
+ $shippingAddress->setSaveInAddressBook($saveInAddressBook);
+ }
} catch (NoSuchEntityException $e) {
- // do nothing if customer is not found by id
- }
- $address = $quote->getBillingAddress()->importCustomerAddressData($addressData);
- if ($useForShipping) {
- $shippingAddress = $quote->getShippingAddress()->importCustomerAddressData($addressData);
- $shippingAddress->setSaveInAddressBook($saveInAddressBook);
+ $address->setCustomerAddressId(null);
}
} elseif ($quote->getCustomerId()) {
$address->setEmail($quote->getCustomerEmail());
diff --git a/app/code/Magento/Quote/Model/Quote/Address/Rate.php b/app/code/Magento/Quote/Model/Quote/Address/Rate.php
index 3f96be4bd25a4..339add647c90c 100644
--- a/app/code/Magento/Quote/Model/Quote/Address/Rate.php
+++ b/app/code/Magento/Quote/Model/Quote/Address/Rate.php
@@ -43,6 +43,13 @@ class Rate extends AbstractModel
protected $_address;
/**
+ * @var carrier_sort_order
+ */
+ public $carrier_sort_order;
+
+ /**
+ * Check the Quote rate
+ *
* @return void
*/
protected function _construct()
@@ -51,6 +58,8 @@ protected function _construct()
}
/**
+ * Set Address id with address before save
+ *
* @return $this
*/
public function beforeSave()
@@ -63,6 +72,8 @@ public function beforeSave()
}
/**
+ * Set address
+ *
* @param \Magento\Quote\Model\Quote\Address $address
* @return $this
*/
@@ -73,6 +84,8 @@ public function setAddress(\Magento\Quote\Model\Quote\Address $address)
}
/**
+ * Get Method for address
+ *
* @return \Magento\Quote\Model\Quote\Address
*/
public function getAddress()
@@ -81,6 +94,8 @@ public function getAddress()
}
/**
+ * Import shipping rate
+ *
* @param \Magento\Quote\Model\Quote\Address\RateResult\AbstractResult $rate
* @return $this
*/
diff --git a/app/code/Magento/Quote/Model/Quote/Address/ShippingAddressPersister.php b/app/code/Magento/Quote/Model/Quote/Address/ShippingAddressPersister.php
new file mode 100644
index 0000000000000..3536e092d132f
--- /dev/null
+++ b/app/code/Magento/Quote/Model/Quote/Address/ShippingAddressPersister.php
@@ -0,0 +1,71 @@
+addressValidator = $addressValidator;
+ $this->addressRepository = $addressRepository;
+ }
+
+ /**
+ * Save address for shipping.
+ *
+ * @param CartInterface $quote
+ * @param AddressInterface $address
+ * @return void
+ * @throws InputException
+ * @throws LocalizedException
+ * @throws NoSuchEntityException
+ */
+ public function save(CartInterface $quote, AddressInterface $address): void
+ {
+ $this->addressValidator->validateForCart($quote, $address);
+ $customerAddressId = $address->getCustomerAddressId();
+
+ $saveInAddressBook = $address->getSaveInAddressBook() ? 1 : 0;
+ if ($customerAddressId) {
+ try {
+ $addressData = $this->addressRepository->getById($customerAddressId);
+ $address = $quote->getShippingAddress()->importCustomerAddressData($addressData);
+ } catch (NoSuchEntityException $e) {
+ $address->setCustomerAddressId(null);
+ }
+ } elseif ($quote->getCustomerId()) {
+ $address->setEmail($quote->getCustomerEmail());
+ }
+ $address->setSaveInAddressBook($saveInAddressBook);
+ $quote->setShippingAddress($address);
+ }
+}
diff --git a/app/code/Magento/Quote/Model/QuoteAddressValidator.php b/app/code/Magento/Quote/Model/QuoteAddressValidator.php
index f0bc12f7b3a36..f8e7141b3bb00 100644
--- a/app/code/Magento/Quote/Model/QuoteAddressValidator.php
+++ b/app/code/Magento/Quote/Model/QuoteAddressValidator.php
@@ -3,8 +3,15 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
+declare(strict_types=1);
+
namespace Magento\Quote\Model;
+use Magento\Customer\Api\AddressRepositoryInterface;
+use Magento\Customer\Api\CustomerRepositoryInterface;
+use Magento\Customer\Model\Session;
+use Magento\Framework\Exception\InputException;
+use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Quote\Api\Data\AddressInterface;
use Magento\Quote\Api\Data\CartInterface;
@@ -17,35 +24,33 @@
class QuoteAddressValidator
{
/**
- * Address factory.
- *
- * @var \Magento\Customer\Api\AddressRepositoryInterface
+ * @var AddressRepositoryInterface
*/
- protected $addressRepository;
+ protected AddressRepositoryInterface $addressRepository;
/**
- * Customer repository.
- *
- * @var \Magento\Customer\Api\CustomerRepositoryInterface
+ * @var CustomerRepositoryInterface
*/
- protected $customerRepository;
+ protected CustomerRepositoryInterface $customerRepository;
/**
+ * @var Session
* @deprecated 101.1.1 This class is not a part of HTML presentation layer and should not use sessions.
+ * @see Session
*/
- protected $customerSession;
+ protected Session $customerSession;
/**
* Constructs a quote shipping address validator service object.
*
- * @param \Magento\Customer\Api\AddressRepositoryInterface $addressRepository
- * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository Customer repository.
- * @param \Magento\Customer\Model\Session $customerSession
+ * @param AddressRepositoryInterface $addressRepository
+ * @param CustomerRepositoryInterface $customerRepository Customer repository.
+ * @param Session $customerSession
*/
public function __construct(
- \Magento\Customer\Api\AddressRepositoryInterface $addressRepository,
- \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository,
- \Magento\Customer\Model\Session $customerSession
+ AddressRepositoryInterface $addressRepository,
+ CustomerRepositoryInterface $customerRepository,
+ Session $customerSession
) {
$this->addressRepository = $addressRepository;
$this->customerRepository = $customerRepository;
@@ -56,10 +61,10 @@ public function __construct(
* Validate address.
*
* @param AddressInterface $address
- * @param int|null $customerId Cart belongs to
+ * @param int|null $customerId
* @return void
- * @throws \Magento\Framework\Exception\InputException The specified address belongs to another customer.
- * @throws \Magento\Framework\Exception\NoSuchEntityException The specified customer ID or address ID is not valid.
+ * @throws LocalizedException The specified customer ID or address ID is not valid.
+ * @throws NoSuchEntityException The specified customer ID or address ID is not valid.
*/
private function doValidate(AddressInterface $address, ?int $customerId): void
{
@@ -67,7 +72,7 @@ private function doValidate(AddressInterface $address, ?int $customerId): void
if ($customerId) {
$customer = $this->customerRepository->getById($customerId);
if (!$customer->getId()) {
- throw new \Magento\Framework\Exception\NoSuchEntityException(
+ throw new NoSuchEntityException(
__('Invalid customer id %1', $customerId)
);
}
@@ -76,7 +81,7 @@ private function doValidate(AddressInterface $address, ?int $customerId): void
if ($address->getCustomerAddressId()) {
//Existing address cannot belong to a guest
if (!$customerId) {
- throw new \Magento\Framework\Exception\NoSuchEntityException(
+ throw new NoSuchEntityException(
__('Invalid customer address id %1', $address->getCustomerAddressId())
);
}
@@ -84,7 +89,7 @@ private function doValidate(AddressInterface $address, ?int $customerId): void
try {
$this->addressRepository->getById($address->getCustomerAddressId());
} catch (NoSuchEntityException $e) {
- throw new \Magento\Framework\Exception\NoSuchEntityException(
+ throw new NoSuchEntityException(
__('Invalid address id %1', $address->getId())
);
}
@@ -94,7 +99,7 @@ private function doValidate(AddressInterface $address, ?int $customerId): void
return $address->getId();
}, $this->customerRepository->getById($customerId)->getAddresses());
if (!in_array($address->getCustomerAddressId(), $applicableAddressIds)) {
- throw new \Magento\Framework\Exception\NoSuchEntityException(
+ throw new NoSuchEntityException(
__('Invalid customer address id %1', $address->getCustomerAddressId())
);
}
@@ -104,29 +109,74 @@ private function doValidate(AddressInterface $address, ?int $customerId): void
/**
* Validates the fields in a specified address data object.
*
- * @param \Magento\Quote\Api\Data\AddressInterface $addressData The address data object.
+ * @param AddressInterface $addressData The address data object.
* @return bool
- * @throws \Magento\Framework\Exception\InputException The specified address belongs to another customer.
- * @throws \Magento\Framework\Exception\NoSuchEntityException The specified customer ID or address ID is not valid.
+ * @throws InputException The specified address belongs to another customer.
+ * @throws NoSuchEntityException|LocalizedException The specified customer ID or address ID is not valid.
*/
- public function validate(AddressInterface $addressData)
+ public function validate(AddressInterface $addressData): bool
{
$this->doValidate($addressData, $addressData->getCustomerId());
return true;
}
+ /**
+ * Validate Quest Address for guest user
+ *
+ * @param AddressInterface $address
+ * @param CartInterface $cart
+ * @return void
+ * @throws NoSuchEntityException
+ */
+ private function doValidateForGuestQuoteAddress(AddressInterface $address, CartInterface $cart): void
+ {
+ //validate guest cart address
+ if ($address->getId() !== null) {
+ $old = $cart->getAddressById($address->getId());
+ if ($old === false) {
+ throw new NoSuchEntityException(
+ __('Invalid quote address id %1', $address->getId())
+ );
+ }
+ }
+ }
+
/**
* Validate address to be used for cart.
*
* @param CartInterface $cart
* @param AddressInterface $address
* @return void
- * @throws \Magento\Framework\Exception\InputException The specified address belongs to another customer.
- * @throws \Magento\Framework\Exception\NoSuchEntityException The specified customer ID or address ID is not valid.
+ * @throws InputException The specified address belongs to another customer.
+ * @throws NoSuchEntityException|LocalizedException The specified customer ID or address ID is not valid.
*/
public function validateForCart(CartInterface $cart, AddressInterface $address): void
{
- $this->doValidate($address, $cart->getCustomerIsGuest() ? null : $cart->getCustomer()->getId());
+ if ($cart->getCustomerIsGuest()) {
+ $this->doValidateForGuestQuoteAddress($address, $cart);
+ }
+ $this->doValidate($address, $cart->getCustomerIsGuest() ? null : (int) $cart->getCustomer()->getId());
+ }
+
+ /**
+ * Validate address id to be used for cart.
+ *
+ * @param CartInterface $cart
+ * @param AddressInterface $address
+ * @return void
+ * @throws NoSuchEntityException The specified customer ID or address ID is not valid.
+ */
+ public function validateWithExistingAddress(CartInterface $cart, AddressInterface $address): void
+ {
+ // check if address belongs to quote.
+ if ($address->getId() !== null) {
+ $old = $cart->getAddressesCollection()->getItemById($address->getId());
+ if ($old === null) {
+ throw new NoSuchEntityException(
+ __('Invalid quote address id %1', $address->getId())
+ );
+ }
+ }
}
}
diff --git a/app/code/Magento/Quote/Model/QuoteManagement.php b/app/code/Magento/Quote/Model/QuoteManagement.php
index dc0858f183809..3391ea6a29124 100644
--- a/app/code/Magento/Quote/Model/QuoteManagement.php
+++ b/app/code/Magento/Quote/Model/QuoteManagement.php
@@ -26,6 +26,7 @@
use Magento\Framework\HTTP\PhpEnvironment\RemoteAddress;
use Magento\Framework\Lock\LockManagerInterface;
use Magento\Framework\Model\AbstractExtensibleModel;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Framework\Validator\Exception as ValidatorException;
use Magento\Payment\Model\Method\AbstractMethod;
use Magento\Quote\Api\CartManagementInterface;
@@ -50,7 +51,7 @@
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @SuppressWarnings(PHPMD.TooManyFields)
*/
-class QuoteManagement implements CartManagementInterface
+class QuoteManagement implements CartManagementInterface, ResetAfterRequestInterface
{
private const LOCK_PREFIX = 'PLACE_ORDER_';
@@ -774,4 +775,12 @@ private function rollbackAddresses(
throw new \Exception($message, 0, $e);
}
}
+
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ $this->addressesToSync = [];
+ }
}
diff --git a/app/code/Magento/Quote/Model/QuoteRepository.php b/app/code/Magento/Quote/Model/QuoteRepository.php
index b1bef834197aa..776479a4773f8 100644
--- a/app/code/Magento/Quote/Model/QuoteRepository.php
+++ b/app/code/Magento/Quote/Model/QuoteRepository.php
@@ -14,12 +14,13 @@
use Magento\Framework\App\ObjectManager;
use Magento\Framework\Exception\InputException;
use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Quote\Api\CartRepositoryInterface;
use Magento\Quote\Api\Data\CartInterface;
use Magento\Quote\Api\Data\CartInterfaceFactory;
use Magento\Quote\Api\Data\CartSearchResultsInterfaceFactory;
-use Magento\Quote\Model\QuoteRepository\SaveHandler;
use Magento\Quote\Model\QuoteRepository\LoadHandler;
+use Magento\Quote\Model\QuoteRepository\SaveHandler;
use Magento\Quote\Model\ResourceModel\Quote\Collection as QuoteCollection;
use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory as QuoteCollectionFactory;
use Magento\Store\Model\StoreManagerInterface;
@@ -29,7 +30,7 @@
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
-class QuoteRepository implements CartRepositoryInterface
+class QuoteRepository implements CartRepositoryInterface, ResetAfterRequestInterface
{
/**
* @var Quote[]
@@ -44,6 +45,7 @@ class QuoteRepository implements CartRepositoryInterface
/**
* @var QuoteFactory
* @deprecated 101.1.2
+ * @see no longer used
*/
protected $quoteFactory;
@@ -55,6 +57,7 @@ class QuoteRepository implements CartRepositoryInterface
/**
* @var QuoteCollection
* @deprecated 101.0.0
+ * @see $quoteCollectionFactory
*/
protected $quoteCollection;
@@ -98,7 +101,7 @@ class QuoteRepository implements CartRepositoryInterface
*
* @param QuoteFactory $quoteFactory
* @param StoreManagerInterface $storeManager
- * @param QuoteCollection $quoteCollection
+ * @param QuoteCollection $quoteCollection Deprecated. Use $quoteCollectionFactory
* @param CartSearchResultsInterfaceFactory $searchResultsDataFactory
* @param JoinProcessorInterface $extensionAttributesJoinProcessor
* @param CollectionProcessorInterface|null $collectionProcessor
@@ -127,6 +130,15 @@ public function __construct(
$this->cartFactory = $cartFactory ?: ObjectManager::getInstance()->get(CartInterfaceFactory::class);
}
+ /**
+ * @inheritdoc
+ */
+ public function _resetState(): void
+ {
+ $this->quotesById = [];
+ $this->quotesByCustomerId = [];
+ }
+
/**
* @inheritdoc
*/
@@ -198,7 +210,6 @@ public function save(CartInterface $quote)
}
}
}
-
$this->getSaveHandler()->save($quote);
unset($this->quotesById[$quote->getId()]);
unset($this->quotesByCustomerId[$quote->getCustomerId()]);
@@ -268,6 +279,7 @@ public function getList(SearchCriteriaInterface $searchCriteria)
* @param QuoteCollection $collection The quote collection.
* @return void
* @deprecated 101.0.0
+ * @see no longer used
* @throws InputException The specified filter group or quote collection does not exist.
*/
protected function addFilterGroupToCollection(FilterGroup $filterGroup, QuoteCollection $collection)
@@ -288,7 +300,6 @@ protected function addFilterGroupToCollection(FilterGroup $filterGroup, QuoteCol
* Get new SaveHandler dependency for application code.
*
* @return SaveHandler
- * @deprecated 100.1.0
*/
private function getSaveHandler()
{
@@ -302,7 +313,6 @@ private function getSaveHandler()
* Get load handler instance.
*
* @return LoadHandler
- * @deprecated 100.1.0
*/
private function getLoadHandler()
{
diff --git a/app/code/Magento/Quote/Model/QuoteRepository/SaveHandler.php b/app/code/Magento/Quote/Model/QuoteRepository/SaveHandler.php
index 12a71648690d4..12d155de5b017 100644
--- a/app/code/Magento/Quote/Model/QuoteRepository/SaveHandler.php
+++ b/app/code/Magento/Quote/Model/QuoteRepository/SaveHandler.php
@@ -7,35 +7,47 @@
namespace Magento\Quote\Model\QuoteRepository;
-use Magento\Quote\Api\Data\CartInterface;
+use Magento\Backend\Model\Session\Quote as QuoteSession;
use Magento\Customer\Api\AddressRepositoryInterface;
use Magento\Framework\App\ObjectManager;
-use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\InputException;
+use Magento\Framework\Exception\LocalizedException;
+use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Quote\Api\Data\AddressInterface;
use Magento\Quote\Api\Data\AddressInterfaceFactory;
+use Magento\Quote\Api\Data\CartInterface;
+use Magento\Quote\Model\Quote\Address\BillingAddressPersister;
+use Magento\Quote\Model\Quote\Address\ShippingAddressPersister;
+use Magento\Quote\Model\Quote\Item\CartItemPersister;
+use Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister;
+use Magento\Quote\Model\ResourceModel\Quote;
/**
* Handler for saving quote.
+ *
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
*/
class SaveHandler
{
/**
- * @var \Magento\Quote\Model\Quote\Item\CartItemPersister
+ * @var CartItemPersister
*/
private $cartItemPersister;
/**
- * @var \Magento\Quote\Model\Quote\Address\BillingAddressPersister
+ * @var BillingAddressPersister
*/
private $billingAddressPersister;
/**
- * @var \Magento\Quote\Model\ResourceModel\Quote
+ * @var Quote
*/
private $quoteResourceModel;
/**
- * @var \Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister
+ * @var ShippingAssignmentPersister
*/
private $shippingAssignmentPersister;
@@ -50,20 +62,34 @@ class SaveHandler
private $quoteAddressFactory;
/**
- * @param \Magento\Quote\Model\ResourceModel\Quote $quoteResource
- * @param \Magento\Quote\Model\Quote\Item\CartItemPersister $cartItemPersister
- * @param \Magento\Quote\Model\Quote\Address\BillingAddressPersister $billingAddressPersister
- * @param \Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister $shippingAssignmentPersister
- * @param AddressRepositoryInterface $addressRepository
+ * @var ShippingAddressPersister
+ */
+ private $shippingAddressPersister;
+
+ /**
+ * @var QuoteSession
+ */
+ private $quoteSession;
+
+ /**
+ * @param Quote $quoteResource
+ * @param CartItemPersister $cartItemPersister
+ * @param BillingAddressPersister $billingAddressPersister
+ * @param ShippingAssignmentPersister $shippingAssignmentPersister
+ * @param AddressRepositoryInterface|null $addressRepository
* @param AddressInterfaceFactory|null $addressFactory
+ * @param ShippingAddressPersister|null $shippingAddressPersister
+ * @param QuoteSession|null $quoteSession
*/
public function __construct(
- \Magento\Quote\Model\ResourceModel\Quote $quoteResource,
- \Magento\Quote\Model\Quote\Item\CartItemPersister $cartItemPersister,
- \Magento\Quote\Model\Quote\Address\BillingAddressPersister $billingAddressPersister,
- \Magento\Quote\Model\Quote\ShippingAssignment\ShippingAssignmentPersister $shippingAssignmentPersister,
+ Quote $quoteResource,
+ CartItemPersister $cartItemPersister,
+ BillingAddressPersister $billingAddressPersister,
+ ShippingAssignmentPersister $shippingAssignmentPersister,
AddressRepositoryInterface $addressRepository = null,
- AddressInterfaceFactory $addressFactory = null
+ AddressInterfaceFactory $addressFactory = null,
+ ShippingAddressPersister $shippingAddressPersister = null,
+ QuoteSession $quoteSession = null
) {
$this->quoteResourceModel = $quoteResource;
$this->cartItemPersister = $cartItemPersister;
@@ -71,8 +97,11 @@ public function __construct(
$this->shippingAssignmentPersister = $shippingAssignmentPersister;
$this->addressRepository = $addressRepository
?: ObjectManager::getInstance()->get(AddressRepositoryInterface::class);
- $this->quoteAddressFactory = $addressFactory ?:ObjectManager::getInstance()
+ $this->quoteAddressFactory = $addressFactory ?: ObjectManager::getInstance()
->get(AddressInterfaceFactory::class);
+ $this->shippingAddressPersister = $shippingAddressPersister
+ ?: ObjectManager::getInstance()->get(ShippingAddressPersister::class);
+ $this->quoteSession = $quoteSession ?: ObjectManager::getInstance()->get(QuoteSession::class);
}
/**
@@ -81,18 +110,16 @@ public function __construct(
* @param CartInterface $quote
* @return CartInterface
* @throws InputException
- * @throws \Magento\Framework\Exception\CouldNotSaveException
- * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws CouldNotSaveException
+ * @throws LocalizedException
*/
public function save(CartInterface $quote)
{
- /** @var \Magento\Quote\Model\Quote $quote */
// Quote Item processing
$items = $quote->getItems();
if ($items) {
foreach ($items as $item) {
- /** @var \Magento\Quote\Model\Quote\Item $item */
if (!$item->isDeleted()) {
$quote->setLastAddedItem($this->cartItemPersister->save($quote, $item));
} elseif (count($items) === 1) {
@@ -104,33 +131,50 @@ public function save(CartInterface $quote)
// Billing Address processing
$billingAddress = $quote->getBillingAddress();
-
if ($billingAddress) {
- if ($billingAddress->getCustomerAddressId()) {
- try {
- $this->addressRepository->getById($billingAddress->getCustomerAddressId());
- } catch (NoSuchEntityException $e) {
- $billingAddress->setCustomerAddressId(null);
- }
- }
-
+ $this->processAddress($billingAddress);
$this->billingAddressPersister->save($quote, $billingAddress);
}
+ // Shipping Address processing
+ if ($this->quoteSession->getData(('reordered'))) {
+ $shippingAddress = $this->processAddress($quote->getShippingAddress());
+ $this->shippingAddressPersister->save($quote, $shippingAddress);
+ }
+
$this->processShippingAssignment($quote);
$this->quoteResourceModel->save($quote->collectTotals());
return $quote;
}
+ /**
+ * Process address for customer address Id
+ *
+ * @param AddressInterface $address
+ * @return AddressInterface
+ * @throws LocalizedException
+ */
+ private function processAddress(AddressInterface $address): AddressInterface
+ {
+ if ($address->getCustomerAddressId()) {
+ try {
+ $this->addressRepository->getById($address->getCustomerAddressId());
+ } catch (NoSuchEntityException $e) {
+ $address->setCustomerAddressId(null);
+ }
+ }
+ return $address;
+ }
+
/**
* Process shipping assignment
*
- * @param \Magento\Quote\Model\Quote $quote
+ * @param CartInterface $quote
* @return void
* @throws InputException
*/
- private function processShippingAssignment($quote)
+ private function processShippingAssignment(CartInterface $quote)
{
// Shipping Assignments processing
$extensionAttributes = $quote->getExtensionAttributes();
diff --git a/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php b/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php
index b59737bff988b..6e27625283bec 100644
--- a/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php
+++ b/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php
@@ -127,6 +127,16 @@ protected function _construct()
$this->_init(QuoteItem::class, ResourceQuoteItem::class);
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ parent::_resetState();
+ $this->_productIds = [];
+ $this->_quote = null;
+ }
+
/**
* Retrieve store Id (From Quote)
*
diff --git a/app/code/Magento/Quote/README.md b/app/code/Magento/Quote/README.md
index 4c3cf42359f0c..439b902ca9946 100644
--- a/app/code/Magento/Quote/README.md
+++ b/app/code/Magento/Quote/README.md
@@ -7,6 +7,7 @@ This module provides customer cart management functionality.
The Magento_Quote module is one of the base Magento 2 modules. You cannot disable or uninstall this module.
The Magento_Quote module creates the following table in the database:
+
- `quote`
- `quote_address`
- `quote_item`
@@ -27,6 +28,7 @@ Extension developers can interact with the Magento_Quote module. For more inform
### Events
The module dispatches the following events:
+
- `sales_quote_address_collection_load_after` event in the `\Magento\Quote\Model\ResourceModel\Quote\Address\Collection::_afterLoad` method. Parameters:
- `quote_address_collection` is a `$this` object (`Magento\Quote\Model\ResourceModel\Quote\Address\Collection` class)
@@ -108,7 +110,7 @@ The module dispatches the following events:
- `sales_quote_item_collection_products_after_load` event in the `\Magento\Quote\Model\QuoteManagement::_assignProducts` method. Parameters:
- `collection` is a product collection object (`\Magento\Catalog\Model\ResourceModel\Product\Collection` class)
-For information about an event in Magento 2, see [Events and observers](https://developer.adobe.com/commerce/php/development/components/events-and-observers/#events).
+For information about an event in Magento 2, see [Events and observers](https://developer.adobe.com/commerce/php/development/components/events-and-observers/#events).
### Public APIs
@@ -169,7 +171,7 @@ For information about an event in Magento 2, see [Events and observers](https://
- `\Magento\Quote\Api\ChangeQuoteControlInterface`
- checks if user is allowed to change the quote
-
+
#### Guest
- `\Magento\Quote\Api\GuestBillingAddressManagementInterface`
@@ -180,7 +182,7 @@ For information about an event in Magento 2, see [Events and observers](https://
- gets lists items that are assigned to a specified quote
- add/update the specified cart guest item
- removes the specified item from the specified quote
-
+
- `\Magento\Quote\Api\GuestCouponManagementInterface`
- gets coupon for a specified quote by quote ID
- adds a coupon by code to a specified quote
@@ -205,7 +207,7 @@ For information about an event in Magento 2, see [Events and observers](https://
- `\Magento\Quote\Api\GuestCartRepositoryInterface`
- gets quote by quote ID for guest user
-
+
- `\Magento\Quote\Api\GuestCartTotalManagementInterface`
- sets shipping/billing methods and additional data for a quote and collect totals for guest
@@ -237,7 +239,7 @@ For information about an event in Magento 2, see [Events and observers](https://
- returns information for the quote for a specified customer
- assigns a specified customer to a specified shopping quote
- places an order for a specified quote
-
+
- `\Magento\Quote\Api\CartRepositoryInterface`
- gets quote by quote ID
- gets list carts that match specified search criteria
@@ -252,7 +254,7 @@ For information about an event in Magento 2, see [Events and observers](https://
- `\Magento\Quote\Api\CartTotalRepositoryInterface`
- gets quote totals by quote ID
-
+
- `\Magento\Quote\Api\CouponManagementInterface`
- gets coupon for a specified quote by quote ID
- adds a coupon by code to a specified quote
@@ -278,20 +280,19 @@ For information about an event in Magento 2, see [Events and observers](https://
- `\Magento\Quote\Model\ShippingMethodManagementInterface`
- sets the carrier and shipping methods codes for a specified quote
- gets the selected shipping method for a specified quote
-
+
#### Model
-
+
- `\Magento\Quote\Model\Quote\Address\FreeShippingInterface`
- checks if is a free shipping
- `\Magento\Quote\Model\Quote\Address\RateCollectorInterface`
- retrieves all methods for supplied shipping data
-
+
- `\Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface`
- converts masked quote ID to the quote ID (entity ID)
- `\Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface`
- converts quote ID to the masked quote ID
-
-For information about a public API in Magento 2, see [Public interfaces & APIs](https://developer.adobe.com/commerce/php/development/components/api-concepts/).
+For information about a public API in Magento 2, see [Public interfaces & APIs](https://developer.adobe.com/commerce/php/development/components/api-concepts/).
diff --git a/app/code/Magento/Quote/Test/Fixture/QuoteIdMask.php b/app/code/Magento/Quote/Test/Fixture/QuoteIdMask.php
new file mode 100644
index 0000000000000..ab6bf0a3ff895
--- /dev/null
+++ b/app/code/Magento/Quote/Test/Fixture/QuoteIdMask.php
@@ -0,0 +1,60 @@
+quoteIdMaskFactory = $quoteIdMaskFactory;
+ $this->quoteIdMaskResourceModel = $quoteIdMaskResourceModel;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function apply(array $data = []): ?DataObject
+ {
+ if (empty($data[self::FIELD_CART_ID])) {
+ throw new InvalidArgumentException(__('"%field" is required', ['field' => self::FIELD_CART_ID]));
+ }
+
+ $quoteIdMask = $this->quoteIdMaskFactory->create();
+ $quoteIdMask->setQuoteId($data[self::FIELD_CART_ID]);
+ $this->quoteIdMaskResourceModel->save($quoteIdMask);
+
+ return $quoteIdMask;
+ }
+}
diff --git a/app/code/Magento/Quote/etc/db_schema.xml b/app/code/Magento/Quote/etc/db_schema.xml
index ff183e3150894..98c55175318c9 100644
--- a/app/code/Magento/Quote/etc/db_schema.xml
+++ b/app/code/Magento/Quote/etc/db_schema.xml
@@ -97,8 +97,9 @@
-
+
+
diff --git a/app/code/Magento/Quote/etc/db_schema_whitelist.json b/app/code/Magento/Quote/etc/db_schema_whitelist.json
index 5667a9a5b4600..9e1f8ce164b61 100644
--- a/app/code/Magento/Quote/etc/db_schema_whitelist.json
+++ b/app/code/Magento/Quote/etc/db_schema_whitelist.json
@@ -53,7 +53,8 @@
},
"index": {
"QUOTE_CUSTOMER_ID_STORE_ID_IS_ACTIVE": true,
- "QUOTE_STORE_ID": true
+ "QUOTE_STORE_ID": true,
+ "QUOTE_STORE_ID_UPDATED_AT": true
},
"constraint": {
"PRIMARY": true,
@@ -121,7 +122,9 @@
"vat_is_valid": true,
"vat_request_id": true,
"vat_request_date": true,
- "vat_request_success": true
+ "vat_request_success": true,
+ "validated_country_code": true,
+ "validated_vat_number": true
},
"index": {
"QUOTE_ADDRESS_QUOTE_ID": true
diff --git a/app/code/Magento/Quote/i18n/en_US.csv b/app/code/Magento/Quote/i18n/en_US.csv
index 54b7edbd7fd15..483b29a9fdbce 100644
--- a/app/code/Magento/Quote/i18n/en_US.csv
+++ b/app/code/Magento/Quote/i18n/en_US.csv
@@ -69,6 +69,7 @@ Carts,Carts
"Validated Country Code","Validated Country Code"
"Validated Vat Number","Validated Vat Number"
"Invalid Quote Item id %1","Invalid Quote Item id %1"
+"Invalid quote address id %1","Invalid quote address id %1"
"Number above 0 is required for the limit","Number above 0 is required for the limit"
"Please select a valid rate limit period in seconds: %1.","Please select a valid rate limit period in seconds: %1."
"Identity type not found","Identity type not found"
diff --git a/app/code/Magento/QuoteAnalytics/README.md b/app/code/Magento/QuoteAnalytics/README.md
index c7abc18ffccbe..e9e220549ab44 100644
--- a/app/code/Magento/QuoteAnalytics/README.md
+++ b/app/code/Magento/QuoteAnalytics/README.md
@@ -5,6 +5,7 @@ This module configures data definitions for a data collection related to the Quo
## Installation
Before installing this module, note that the Magento_QuoteAnalytics is dependent on the following modules:
+
- `Magento_Quote`
- `Magento_Analytics`
@@ -15,5 +16,6 @@ For information about a module installation in Magento 2, see [Enable or disable
## Additional data
More information can get at articles:
+
- [Advanced Reporting](https://developer.adobe.com/commerce/php/development/advanced-reporting/)
- [Data collection for advanced reporting](https://developer.adobe.com/commerce/php/development/advanced-reporting/data-collection/)
diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequest.php b/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequest.php
index e15b7324ce24b..7fdce5245b475 100644
--- a/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequest.php
+++ b/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequest.php
@@ -20,13 +20,21 @@ class CreateBuyRequest
*/
private $dataObjectFactory;
+ /**
+ * @var CreateBuyRequestDataProviderInterface[]
+ */
+ private $providers;
+
/**
* @param DataObjectFactory $dataObjectFactory
+ * @param array $providers
*/
public function __construct(
- DataObjectFactory $dataObjectFactory
+ DataObjectFactory $dataObjectFactory,
+ array $providers = []
) {
$this->dataObjectFactory = $dataObjectFactory;
+ $this->providers = $providers;
}
/**
@@ -39,21 +47,30 @@ public function __construct(
public function execute(float $qty, array $customizableOptionsData): DataObject
{
$customizableOptions = [];
+ $enteredOptions = [];
foreach ($customizableOptionsData as $customizableOption) {
if (isset($customizableOption['value_string'])) {
- $customizableOptions[$customizableOption['id']] = $this->convertCustomOptionValue(
- $customizableOption['value_string']
- );
+ if (!is_numeric($customizableOption['id'])) {
+ $enteredOptions[$customizableOption['id']] = $customizableOption['value_string'];
+ } else {
+ $customizableOptions[$customizableOption['id']] = $this->convertCustomOptionValue(
+ $customizableOption['value_string']
+ );
+ }
}
}
- $dataArray = [
- 'data' => [
+ $requestData = [
+ [
'qty' => $qty,
- 'options' => $customizableOptions,
- ],
+ 'options' => $customizableOptions
+ ]
];
- return $this->dataObjectFactory->create($dataArray);
+ foreach ($this->providers as $provider) {
+ $requestData[] = $provider->execute($enteredOptions);
+ }
+
+ return $this->dataObjectFactory->create(['data' => array_merge([], ...$requestData)]);
}
/**
diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequestDataProviderInterface.php b/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequestDataProviderInterface.php
new file mode 100644
index 0000000000000..af52c2869e907
--- /dev/null
+++ b/app/code/Magento/QuoteGraphQl/Model/Cart/CreateBuyRequestDataProviderInterface.php
@@ -0,0 +1,19 @@
+dataObjectConverter = $dataObjectConverter;
$this->uidEncoder = $uidEncoder;
+ $this->getAttributeValue = $getAttributeValue;
}
/**
@@ -67,7 +80,17 @@ public function execute(QuoteAddress $address): array
'uid' => $this->uidEncoder->encode((string)$address->getAddressId()) ,
'street' => $address->getStreet(),
'items_weight' => $address->getWeight(),
- 'customer_notes' => $address->getCustomerNotes()
+ 'customer_notes' => $address->getCustomerNotes(),
+ 'custom_attributes' => array_map(
+ function (AttributeInterface $attribute) {
+ return $this->getAttributeValue->execute(
+ 'customer_address',
+ $attribute->getAttributeCode(),
+ $attribute->getValue()
+ );
+ },
+ $address->getCustomAttributes() ?? []
+ )
]
);
@@ -76,7 +99,7 @@ public function execute(QuoteAddress $address): array
}
foreach ($address->getAllItems() as $addressItem) {
- if ($addressItem instanceof \Magento\Quote\Model\Quote\Item) {
+ if ($addressItem instanceof Item) {
$itemId = $addressItem->getItemId();
} else {
$itemId = $addressItem->getQuoteItemId();
diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/TotalsCollector.php b/app/code/Magento/QuoteGraphQl/Model/Cart/TotalsCollector.php
index 06fc3ad2e6657..e57deafe9c536 100644
--- a/app/code/Magento/QuoteGraphQl/Model/Cart/TotalsCollector.php
+++ b/app/code/Magento/QuoteGraphQl/Model/Cart/TotalsCollector.php
@@ -8,6 +8,7 @@
namespace Magento\QuoteGraphQl\Model\Cart;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Quote\Model\Quote;
use Magento\Quote\Model\Quote\Address;
use Magento\Quote\Model\Quote\Address\Total;
@@ -16,7 +17,7 @@
/**
* Helper class to eliminate redundant expensive total calculations
*/
-class TotalsCollector
+class TotalsCollector implements ResetAfterRequestInterface
{
/**
* @var QuoteTotalsCollector
@@ -34,6 +35,8 @@ class TotalsCollector
private $addressTotals;
/**
+ * TotalsCollector constructor
+ *
* @param QuoteTotalsCollector $quoteTotalsCollector
*/
public function __construct(QuoteTotalsCollector $quoteTotalsCollector)
@@ -43,6 +46,14 @@ public function __construct(QuoteTotalsCollector $quoteTotalsCollector)
$this->addressTotals = [];
}
+ /**
+ * @inheritdoc
+ */
+ public function _resetState(): void
+ {
+ $this->clearTotals();
+ }
+
/**
* Clear stored totals to force them to be recalculated the next time they're requested
*
@@ -101,7 +112,6 @@ public function collectAddressTotals(Quote $quote, Address $address, bool $force
$this->addressTotals[$quoteId][$addressId] =
$this->quoteTotalsCollector->collectAddressTotals($quote, $address);
}
-
return $this->addressTotals[$quoteId][$addressId];
}
}
diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemsUidArgsProcessor.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemsUidArgsProcessor.php
index 85e744c026c43..b0d68aa634399 100644
--- a/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemsUidArgsProcessor.php
+++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/CartItemsUidArgsProcessor.php
@@ -10,6 +10,7 @@
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\Framework\GraphQl\Query\Resolver\ArgumentsProcessorInterface;
use Magento\Framework\GraphQl\Query\Uid;
+use Magento\Framework\App\ObjectManager;
/**
* Category UID processor class for category uid and category id arguments
@@ -23,18 +24,26 @@ class CartItemsUidArgsProcessor implements ArgumentsProcessorInterface
/** @var Uid */
private $uidEncoder;
+ /**
+ * @var CustomizableOptionUidArgsProcessor
+ */
+ private $optionUidArgsProcessor;
+
/**
* @param Uid $uidEncoder
+ * @param CustomizableOptionUidArgsProcessor|null $optionUidArgsProcessor
*/
- public function __construct(Uid $uidEncoder)
+ public function __construct(Uid $uidEncoder, ?CustomizableOptionUidArgsProcessor $optionUidArgsProcessor = null)
{
$this->uidEncoder = $uidEncoder;
+ $this->optionUidArgsProcessor =
+ $optionUidArgsProcessor ?: ObjectManager::getInstance()->get(CustomizableOptionUidArgsProcessor::class);
}
/**
* Process the updateCartItems arguments for cart uids
*
- * @param string $fieldName,
+ * @param string $fieldName
* @param array $args
* @return array
* @throws GraphQlInputException
@@ -58,6 +67,10 @@ public function process(
$args[$filterKey]['cart_items'][$key][self::ID] = $this->uidEncoder->decode((string)$uidFilter);
unset($args[$filterKey]['cart_items'][$key][self::UID]);
}
+ if (!empty($cartItem['customizable_options'])) {
+ $args[$filterKey]['cart_items'][$key]['customizable_options'] =
+ $this->optionUidArgsProcessor->process($fieldName, $cartItem['customizable_options']);
+ }
}
}
return $args;
diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/CustomizableOptionUidArgsProcessor.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/CustomizableOptionUidArgsProcessor.php
new file mode 100644
index 0000000000000..278239bba54fa
--- /dev/null
+++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/CustomizableOptionUidArgsProcessor.php
@@ -0,0 +1,64 @@
+uidEncoder = $uidEncoder;
+ }
+
+ /**
+ * Process the customizable options for updateCartItems arguments for uids
+ *
+ * @param string $fieldName
+ * @param array $customizableOptions
+ * @return array
+ * @throws GraphQlInputException
+ */
+ public function process(string $fieldName, array $customizableOptions): array
+ {
+ foreach ($customizableOptions as $key => $option) {
+ $idFilter = $option[self::ID] ?? [];
+ $uidFilter = $option[self::UID] ?? [];
+
+ if (!empty($idFilter)
+ && !empty($uidFilter)
+ && $fieldName === 'updateCartItems') {
+ throw new GraphQlInputException(
+ __(
+ '`%1` and `%2` can\'t be used for CustomizableOptionInput object at the same time.',
+ [self::ID, self::UID]
+ )
+ );
+ } elseif (!empty($uidFilter)) {
+ $customizableOptions[$key][self::ID] = $this->uidEncoder->decode((string)$uidFilter);
+ unset($customizableOptions[$key][self::UID]);
+ }
+ }
+ return $customizableOptions;
+ }
+}
diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php
index dfbc20bf7abd4..4722b3db537a1 100644
--- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php
+++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php
@@ -11,6 +11,7 @@
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
+use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Quote\Model\Cart\Totals;
use Magento\Quote\Model\Quote\Item;
use Magento\QuoteGraphQl\Model\Cart\TotalsCollector;
@@ -18,7 +19,7 @@
/**
* @inheritdoc
*/
-class CartItemPrices implements ResolverInterface
+class CartItemPrices implements ResolverInterface, ResetAfterRequestInterface
{
/**
* @var TotalsCollector
@@ -26,11 +27,13 @@ class CartItemPrices implements ResolverInterface
private $totalsCollector;
/**
- * @var Totals
+ * @var Totals|null
*/
private $totals;
/**
+ * CartItemPrices constructor
+ *
* @param TotalsCollector $totalsCollector
*/
public function __construct(
@@ -39,6 +42,14 @@ public function __construct(
$this->totalsCollector = $totalsCollector;
}
+ /**
+ * @inheritdoc
+ */
+ public function _resetState(): void
+ {
+ $this->totals = null;
+ }
+
/**
* @inheritdoc
*/
@@ -49,14 +60,12 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
}
/** @var Item $cartItem */
$cartItem = $value['model'];
-
if (!$this->totals) {
// The totals calculation is based on quote address.
// But the totals should be calculated even if no address is set
$this->totals = $this->totalsCollector->collectQuoteTotals($cartItem->getQuote());
}
$currencyCode = $cartItem->getQuote()->getQuoteCurrencyCode();
-
return [
'model' => $cartItem,
'price' => [
@@ -88,13 +97,13 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
*
* @param Item $cartItem
* @param string $currencyCode
- * @return array
+ * @return array|null
*/
private function getDiscountValues($cartItem, $currencyCode)
{
$itemDiscounts = $cartItem->getExtensionAttributes()->getDiscounts();
if ($itemDiscounts) {
- $discountValues=[];
+ $discountValues = [];
foreach ($itemDiscounts as $value) {
$discount = [];
$amount = [];
diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/Discounts.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/Discounts.php
index d0c69b8b54497..1f7e0f914d7c2 100644
--- a/app/code/Magento/QuoteGraphQl/Model/Resolver/Discounts.php
+++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/Discounts.php
@@ -18,6 +18,8 @@
*/
class Discounts implements ResolverInterface
{
+ public const TYPE_SHIPPING = "SHIPPING";
+ public const TYPE_ITEM = "ITEM";
/**
* @inheritdoc
*/
@@ -41,21 +43,22 @@ private function getDiscountValues(Quote $quote)
{
$discountValues=[];
$address = $quote->getShippingAddress();
- $totals = $address->getTotals();
- if ($totals && is_array($totals)) {
- foreach ($totals as $total) {
- if (stripos($total->getCode(), 'total') === false && $total->getValue() < 0.00) {
- $discount = [];
- $amount = [];
- $discount['label'] = $total->getTitle() ?: __('Discount');
- $amount['value'] = $total->getValue() * -1;
- $amount['currency'] = $quote->getQuoteCurrencyCode();
- $discount['amount'] = $amount;
- $discountValues[] = $discount;
- }
+ $totalDiscounts = $address->getExtensionAttributes()->getDiscounts();
+
+ if ($totalDiscounts && is_array($totalDiscounts)) {
+ foreach ($totalDiscounts as $value) {
+ $discount = [];
+ $amount = [];
+ $discount['label'] = $value->getRuleLabel() ?: __('Discount');
+ /* @var \Magento\SalesRule\Api\Data\DiscountDataInterface $discountData */
+ $discountData = $value->getDiscountData();
+ $discount['applied_to'] = $discountData->getAppliedTo();
+ $amount['value'] = $discountData->getAmount();
+ $amount['currency'] = $quote->getQuoteCurrencyCode();
+ $discount['amount'] = $amount;
+ $discountValues[] = $discount;
}
- return $discountValues;
}
- return null;
+ return $discountValues ?: null;
}
}
diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php
index abd5ceca881f4..307087391b89d 100644
--- a/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php
+++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/RemoveItemFromCart.php
@@ -87,7 +87,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
$storeId = (int)$context->getExtensionAttributes()->getStore()->getId();
/** Check if the current user is allowed to perform actions with the cart */
- $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId);
+ $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId);
try {
$this->cartItemRepository->deleteById($cartId, $itemId);
@@ -97,7 +97,6 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
throw new GraphQlInputException(__($e->getMessage()), $e);
}
- $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId);
return [
'cart' => [
'model' => $cart,
diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php
index b20bbe0e00660..01e9a95dd5c76 100644
--- a/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php
+++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/ShippingAddress/SelectedShippingMethod.php
@@ -50,11 +50,12 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
return null;
}
- list($carrierCode, $methodCode) = explode('_', $address->getShippingMethod(), 2);
-
/** @var Rate $rate */
+ $carrierCode = $methodCode = null;
foreach ($rates as $rate) {
if ($rate->getCode() === $address->getShippingMethod()) {
+ $carrierCode = $rate->getCarrier();
+ $methodCode = $rate->getMethod();
break;
}
}
diff --git a/app/code/Magento/QuoteGraphQl/README.md b/app/code/Magento/QuoteGraphQl/README.md
index 396f886fc04be..7eebc7c5e5291 100644
--- a/app/code/Magento/QuoteGraphQl/README.md
+++ b/app/code/Magento/QuoteGraphQl/README.md
@@ -6,6 +6,7 @@ to generate quote (cart) information endpoints. Also provides endpoints for modi
## Installation
Before installing this module, note that the Magento_QuoteGraphQl is dependent on the following modules:
+
- `Magento_CatalogGraphQl`
This module does not introduce any database schema modifications or new data.
@@ -20,7 +21,7 @@ Extension developers can interact with the Magento_QuoteDownloadableLinks module
## Additional information
-You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql).
+You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/).
### GraphQl Query
diff --git a/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/ShippingAddress/SelectedShippingMethodTest.php b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/ShippingAddress/SelectedShippingMethodTest.php
new file mode 100644
index 0000000000000..68f52c4a348d9
--- /dev/null
+++ b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/ShippingAddress/SelectedShippingMethodTest.php
@@ -0,0 +1,180 @@
+shippingMethodConverterMock = $this->createMock(ShippingMethodConverter::class);
+ $this->contextMock = $this->createMock(Context::class);
+ $this->fieldMock = $this->getMockBuilder(Field::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->resolveInfoMock = $this->getMockBuilder(ResolveInfo::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class)
+ ->getMockForAbstractClass();
+ $this->addressMock = $this->getMockBuilder(Address::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['getShippingMethod','getAllShippingRates','getQuote',])
+ ->AddMethods(['getShippingAmount','getMethod',])
+ ->getMock();
+ $this->rateMock = $this->getMockBuilder(Rate::class)
+ ->disableOriginalConstructor()
+ ->AddMethods(['getCode','getCarrier','getMethod'])
+ ->getMock();
+ $this->quoteMock = $this->getMockBuilder(Quote::class)
+ ->disableOriginalConstructor()
+ ->addMethods([
+ 'getQuoteCurrencyCode',
+ 'getMethodTitle',
+ 'getCarrierTitle',
+ 'getPriceExclTax',
+ 'getPriceInclTax'
+ ])
+ ->getMock();
+ $this->selectedShippingMethod = new SelectedShippingMethod(
+ $this->shippingMethodConverterMock
+ );
+ }
+
+ public function testResolveWithoutModelInValueParameter(): void
+ {
+ $this->expectException(LocalizedException::class);
+ $this->expectExceptionMessage('"model" value should be specified');
+ $this->selectedShippingMethod->resolve(
+ $this->fieldMock,
+ $this->contextMock,
+ $this->resolveInfoMock,
+ $this->valueMock
+ );
+ }
+
+ public function testResolve(): void
+ {
+ $this->valueMock = ['model' => $this->addressMock];
+ $this->quoteMock
+ ->method('getQuoteCurrencyCode')
+ ->willReturn('USD');
+ $this->quoteMock
+ ->method('getMethodTitle')
+ ->willReturn('method_title');
+ $this->quoteMock
+ ->method('getCarrierTitle')
+ ->willReturn('carrier_title');
+ $this->quoteMock
+ ->expects($this->once())
+ ->method('getPriceExclTax')
+ ->willReturn('PriceExclTax');
+ $this->quoteMock
+ ->expects($this->once())
+ ->method('getPriceInclTax')
+ ->willReturn('PriceInclTax');
+ $this->rateMock
+ ->expects($this->once())
+ ->method('getCode')
+ ->willReturn('shipping_method');
+ $this->rateMock
+ ->expects($this->once())
+ ->method('getCarrier')
+ ->willReturn('shipping_carrier');
+ $this->rateMock
+ ->expects($this->once())
+ ->method('getMethod')
+ ->willReturn('shipping_carrier');
+ $this->addressMock
+ ->method('getAllShippingRates')
+ ->willReturn([$this->rateMock]);
+ $this->addressMock
+ ->method('getShippingMethod')
+ ->willReturn('shipping_method');
+ $this->addressMock
+ ->method('getShippingAmount')
+ ->willReturn('shipping_amount');
+ $this->addressMock
+ ->expects($this->once())
+ ->method('getQuote')
+ ->willReturn($this->quoteMock);
+ $this->shippingMethodConverterMock->method('modelToDataObject')
+ ->willReturn($this->quoteMock);
+ $this->selectedShippingMethod->resolve(
+ $this->fieldMock,
+ $this->contextMock,
+ $this->resolveInfoMock,
+ $this->valueMock
+ );
+ }
+}
diff --git a/app/code/Magento/QuoteGraphQl/composer.json b/app/code/Magento/QuoteGraphQl/composer.json
index 24cb1382634c2..62c37801cbcbb 100644
--- a/app/code/Magento/QuoteGraphQl/composer.json
+++ b/app/code/Magento/QuoteGraphQl/composer.json
@@ -15,7 +15,8 @@
"magento/module-directory": "*",
"magento/module-graph-ql": "*",
"magento/module-gift-message": "*",
- "magento/module-catalog-inventory": "*"
+ "magento/module-catalog-inventory": "*",
+ "magento/module-eav-graph-ql": "*"
},
"suggest": {
"magento/module-graph-ql-cache": "*",
diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls
index 27433a30f3c92..1b65bdcf4564c 100644
--- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls
@@ -62,7 +62,8 @@ input CartItemInput @doc(description: "Defines an item to be added to the cart."
}
input CustomizableOptionInput @doc(description: "Defines a customizable option.") {
- id: Int @doc(description: "The customizable option ID of the product.")
+ uid: ID @doc(description: "The unique ID for a `CartItemInterface` object.")
+ id: Int @deprecated(reason: "Use `uid` instead.") @doc(description: "The customizable option ID of the product.")
value_string: String! @doc(description: "The string value of the option.")
}
@@ -125,6 +126,10 @@ input CartAddressInput @doc(description: "Defines the billing or shipping addres
telephone: String @doc(description: "The telephone number for the billing or shipping address.")
vat_id: String @doc(description: "The VAT company number for billing or shipping address.")
save_in_address_book: Boolean @doc(description: "Determines whether to save the address in the customer's address book. The default value is true.")
+ fax: String @doc(description: "The customer's fax number.")
+ middlename: String @doc(description: "The middle name of the person associated with the billing/shipping address.")
+ prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.")
+ suffix: String @doc(description: "A value such as Sr., Jr., or III.")
}
input SetShippingMethodsOnCartInput @doc(description: "Applies one or shipping methods to the cart.") {
@@ -232,6 +237,10 @@ interface CartAddressInterface @typeResolver(class: "\\Magento\\QuoteGraphQl\\Mo
country: CartAddressCountry! @doc(description: "An object containing the country label and code.")
telephone: String @doc(description: "The telephone number for the billing or shipping address.")
vat_id: String @doc(description: "The VAT company number for billing or shipping address.")
+ fax: String @doc(description: "The customer's fax number.")
+ middlename: String @doc(description: "The middle name of the person associated with the billing/shipping address.")
+ prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.")
+ suffix: String @doc(description: "A value such as Sr., Jr., or III.")
}
type ShippingCartAddress implements CartAddressInterface @doc(description: "Contains shipping addresses and methods.") {
@@ -358,11 +367,17 @@ enum CartItemErrorType {
ITEM_INCREMENTS
}
-type Discount @doc(description:"Defines an individual discount. A discount can be applied to the cart as a whole or to an item.") {
+type Discount @doc(description:"Defines an individual discount. A discount can be applied to the cart as a whole or to an item, shipping.") {
amount: Money! @doc(description:"The amount of the discount.")
+ applied_to: CartDiscountType! @doc(description:"The type of the entity the discount is applied to.")
label: String! @doc(description:"A description of the discount.")
}
+enum CartDiscountType {
+ ITEM
+ SHIPPING
+}
+
type CartItemPrices @doc(description: "Contains details about the price of the item, including taxes and discounts.") {
price: Money! @doc(description: "The price of the item before any discounts were applied. The price that might include tax, depending on the configured display settings for cart.")
price_including_tax: Money! @doc(description: "The price of the item before any discounts were applied. The price that might include tax, depending on the configured display settings for cart.")
diff --git a/app/code/Magento/RelatedProductGraphQl/Model/DataProvider/RelatedProductDataProvider.php b/app/code/Magento/RelatedProductGraphQl/Model/DataProvider/RelatedProductDataProvider.php
index e5084d4c9f9b6..1dec3387c87f6 100644
--- a/app/code/Magento/RelatedProductGraphQl/Model/DataProvider/RelatedProductDataProvider.php
+++ b/app/code/Magento/RelatedProductGraphQl/Model/DataProvider/RelatedProductDataProvider.php
@@ -118,6 +118,9 @@ public function getRelations(array $products, int $linkType): array
$collection = $link->getLinkCollection();
$collection->addFieldToFilter('product_id', ['in' => array_keys($productsByActualIds)]);
$collection->addLinkTypeIdFilter();
+ $collection->joinAttributes();
+ $collection->addOrder('product_id');
+ $collection->addOrder('position', 'asc');
//Prepare map
$map = [];
diff --git a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php
index fac7b23d408e3..f35af6f4885d2 100644
--- a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php
+++ b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php
@@ -94,9 +94,7 @@ private function findRelations(array $products, array $loadAttributes, int $link
$this->searchCriteriaBuilder->addFilter('entity_id', $relatedIds, 'in');
$relatedSearchResult = $this->productDataProvider->getList(
$this->searchCriteriaBuilder->create(),
- $loadAttributes,
- false,
- true
+ $loadAttributes
);
//Filling related products map.
/** @var \Magento\Catalog\Api\Data\ProductInterface[] $relatedProducts */
diff --git a/app/code/Magento/RelatedProductGraphQl/README.md b/app/code/Magento/RelatedProductGraphQl/README.md
index 7aa93403a6949..d25286e686aed 100644
--- a/app/code/Magento/RelatedProductGraphQl/README.md
+++ b/app/code/Magento/RelatedProductGraphQl/README.md
@@ -16,4 +16,4 @@ Extension developers can interact with the Magento_QuoteDownloadableLinks module
## Additional information
-You can get more information about [GraphQl In Magento 2](https://devdocs.magento.com/guides/v2.4/graphql).
+You can get more information about [GraphQl In Magento 2](https://developer.adobe.com/commerce/webapi/graphql/).
diff --git a/app/code/Magento/ReleaseNotification/README.md b/app/code/Magento/ReleaseNotification/README.md
index 060aeb24f473a..c6e55ab091bc3 100644
--- a/app/code/Magento/ReleaseNotification/README.md
+++ b/app/code/Magento/ReleaseNotification/README.md
@@ -19,6 +19,7 @@ Extension developers can interact with the Magento_ReleaseNotification module. F
### UI components
You can extend release notification updates using the configuration files located in the `view/adminhtml/ui_component` directory:
+
- `release_notification`
For information about a UI component in Magento 2, see [Overview of UI components](https://developer.adobe.com/commerce/frontend-core/ui-components/).
@@ -27,19 +28,19 @@ For information about a UI component in Magento 2, see [Overview of UI component
### Purpose and Content
-* Provides a method of notifying administrators of changes, features, and functionality being introduced in a Magento release.
-* Displays a modal containing a high level overview of the features included in the installed or upgraded release of Magento upon the initial login of each administrator into the Admin Panel for a given Magento version.
-* The modal is enabled with pagination functionality to allow for easy navigation between each modal page.
-* Each modal page includes detailed information about a highlighted feature of the Magento release or other notification.
-* Release Notification modal content is determined and provided by Magento Marketing.
+- Provides a method of notifying administrators of changes, features, and functionality being introduced in a Magento release.
+- Displays a modal containing a high level overview of the features included in the installed or upgraded release of Magento upon the initial login of each administrator into the Admin Panel for a given Magento version.
+- The modal is enabled with pagination functionality to allow for easy navigation between each modal page.
+- Each modal page includes detailed information about a highlighted feature of the Magento release or other notification.
+- Release Notification modal content is determined and provided by Magento Marketing.
### Content Retrieval
Release notification content is maintained by Magento for each Magento version, edition, and locale. To retrieve the content, a response is returned from a request with the following parameters:
-* **version** = The Magento version that the client has installed (ex. 2.4.0).
-* **edition** = The Magento edition that the client has installed (ex. Community).
-* **locale** = The chosen locale of the admin user (ex. en_US).
+- **version** = The Magento version that the client has installed (ex. 2.4.0).
+- **edition** = The Magento edition that the client has installed (ex. Community).
+- **locale** = The chosen locale of the admin user (ex. en_US).
The module will make three attempts to retrieve content for the parameters in the order listed:
@@ -51,21 +52,21 @@ If there is no content to be retrieved after these requests, the release notific
### Content Guidelines
-The modal system in the ReleaseNotification module can have up to four modal pages. The admin user can navigate between pages using the "< Prev" and "Next >" buttons at the bottom of the modal. The last modal page will have a "Done" button that will close the modal and record that the admin user has seen the notification.
+The modal system in the ReleaseNotification module can have up to four modal pages. The admin user can navigate between pages using the "< Prev" and "Next >" buttons at the bottom of the modal. The last modal page will have a "Done" button that will close the modal and record that the admin user has seen the notification.
Each modal page can have the following optional content:
-* Main Content
- * Title
- * URL to the image to be displayed alongside the title
- * Text body
- * Bullet point list
-* Sub Headings (highlighted overviews of the content to be detailed on subsequent modal pages) - one to three Sub Headings may be displayed
- * Sub heading title
- * URL to the image to be display before the sub heading title
- * Sub heading content
-* Footer
- * Footer content text
+- Main Content
+ - Title
+ - URL to the image to be displayed alongside the title
+ - Text body
+ - Bullet point list
+- Sub Headings (highlighted overviews of the content to be detailed on subsequent modal pages) - one to three Sub Headings may be displayed
+ - Sub heading title
+ - URL to the image to be display before the sub heading title
+ - Sub heading content
+- Footer
+ - Footer content text
The Sub Heading section is ideally used on the first modal page as a way to describe one to three highlighted features that will be presented in greater detail on the following modal pages. It is recommended to use the Main Content -> Text Body and Bullet Point lists as the paragraph and list content displayed on a highlighted feature's detail modal page.
diff --git a/app/code/Magento/ReleaseNotification/i18n/en_US.csv b/app/code/Magento/ReleaseNotification/i18n/en_US.csv
index 178482dc7a980..50a4587b4398c 100644
--- a/app/code/Magento/ReleaseNotification/i18n/en_US.csv
+++ b/app/code/Magento/ReleaseNotification/i18n/en_US.csv
@@ -5,11 +5,11 @@
"We’ll try to show it again the next time you sign in to Magento Admin.
To learn more about new features, see our latest Release Notes in
- DevDocs' Release Information .
]]>","We’ll try to show it again the next time you sign in to Magento Admin.
To learn more about new features, see our latest Release Notes in
- DevDocs' Release Information .
]]>"
diff --git a/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml b/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml
index 9c6d152bed27b..16b7b94da858b 100644
--- a/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml
+++ b/app/code/Magento/ReleaseNotification/view/adminhtml/ui_component/release_notification.xml
@@ -67,7 +67,7 @@
- We’ll try to show it again the next time you sign in to Magento Admin.
To learn more about new features, see our latest Release Notes in
- DevDocs' Release Information .
]]>
@@ -127,7 +127,7 @@
- We’ll try to show it again the next time you sign in to Magento Admin.
To learn more about new features, see our latest Release Notes in
- DevDocs' Release Information .
]]>
@@ -208,7 +208,7 @@
- We’ll try to show it again the next time you sign in to Magento Admin.
To learn more about new features, see our latest Release Notes in
- DevDocs' Release Information .
]]>
@@ -289,7 +289,7 @@
- We’ll try to show it again the next time you sign in to Magento Admin.
To learn more about new features, see our latest Release Notes in
- DevDocs' Release Information .
]]>
diff --git a/app/code/Magento/RemoteStorage/Driver/DriverPool.php b/app/code/Magento/RemoteStorage/Driver/DriverPool.php
index e1fda91923e4c..f67eee4ddb0c5 100644
--- a/app/code/Magento/RemoteStorage/Driver/DriverPool.php
+++ b/app/code/Magento/RemoteStorage/Driver/DriverPool.php
@@ -87,4 +87,15 @@ public function getDriver($code = self::REMOTE): DriverInterface
return parent::getDriver($code);
}
+
+ /**
+ * Disable show internals with var_dump
+ *
+ * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo
+ * @return array
+ */
+ public function __debugInfo()
+ {
+ return [];
+ }
}
diff --git a/app/code/Magento/RemoteStorage/Filesystem.php b/app/code/Magento/RemoteStorage/Filesystem.php
index 4593c26281554..ae0dc791c275e 100644
--- a/app/code/Magento/RemoteStorage/Filesystem.php
+++ b/app/code/Magento/RemoteStorage/Filesystem.php
@@ -129,4 +129,15 @@ public function getDirectoryCodes(): array
{
return $this->directoryCodes;
}
+
+ /**
+ * Disable show internals with var_dump
+ *
+ * @see https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo
+ * @return array
+ */
+ public function __debugInfo()
+ {
+ return [];
+ }
}
diff --git a/app/code/Magento/Reports/README.md b/app/code/Magento/Reports/README.md
index 1fac1a15782cb..3d90323a3ee5d 100644
--- a/app/code/Magento/Reports/README.md
+++ b/app/code/Magento/Reports/README.md
@@ -1,4 +1,5 @@
Magento_Reports module provides ability to collect various reports such as:
+
- products reports (bestsellers, low stock, most viewed, products ordered),
- sales reports (orders, tax, invoiced, shipping, refunds, coupons, and PayPal settlement reports),
- customer reports (new accounts, customer by order totals, customers by number of orders),
diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminCanceledOrdersInOrderSalesReportTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminCanceledOrdersInOrderSalesReportTest.xml
index 600291dffade4..74296fbe66b38 100644
--- a/app/code/Magento/Reports/Test/Mftf/Test/AdminCanceledOrdersInOrderSalesReportTest.xml
+++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminCanceledOrdersInOrderSalesReportTest.xml
@@ -18,6 +18,7 @@
+
@@ -75,7 +76,7 @@
-
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsNavigateMenuTest.xml
index 88c94a27a83fb..f550bf6d58b1f 100644
--- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsNavigateMenuTest.xml
+++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsAbandonedCartsNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsBestsellersNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsBestsellersNavigateMenuTest.xml
index efe988acbcf7d..d4ae4752e93a4 100644
--- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsBestsellersNavigateMenuTest.xml
+++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsBestsellersNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsCouponsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsCouponsNavigateMenuTest.xml
index 14db012e76888..c753ff49a0ffb 100644
--- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsCouponsNavigateMenuTest.xml
+++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsCouponsNavigateMenuTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsDownloadsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsDownloadsNavigateMenuTest.xml
index d6a3ae8bcd201..261ff3c9e5377 100644
--- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsDownloadsNavigateMenuTest.xml
+++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsDownloadsNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsInvoicedNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsInvoicedNavigateMenuTest.xml
index e9ed4caa7ef03..c99c6faf7c1e6 100644
--- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsInvoicedNavigateMenuTest.xml
+++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsInvoicedNavigateMenuTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockDisableProductTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockDisableProductTest.xml
index 7756a43c68ace..cad8185fe204e 100644
--- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockDisableProductTest.xml
+++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockDisableProductTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockNavigateMenuTest.xml
index 8d8ceb69ba7ba..d2de9cefd4f55 100644
--- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockNavigateMenuTest.xml
+++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsLowStockNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsNewNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsNewNavigateMenuTest.xml
index 30d9392071a9c..5d4e5c494f7a4 100644
--- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsNewNavigateMenuTest.xml
+++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsNewNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderCountNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderCountNavigateMenuTest.xml
index cb17169c4cf8c..30980009d847b 100644
--- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderCountNavigateMenuTest.xml
+++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderCountNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderTotalNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderTotalNavigateMenuTest.xml
index c3d0f3b51f69e..313770e7411dc 100644
--- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderTotalNavigateMenuTest.xml
+++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderTotalNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedGroupedBySkuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedGroupedBySkuTest.xml
index fb44eca668e68..8cad119c65578 100644
--- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedGroupedBySkuTest.xml
+++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedGroupedBySkuTest.xml
@@ -52,7 +52,7 @@
-
+
@@ -63,7 +63,7 @@
-
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedNavigateMenuTest.xml
index d42a41a45d6f8..51c3148bcb487 100644
--- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedNavigateMenuTest.xml
+++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrderedNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrdersNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrdersNavigateMenuTest.xml
index 388be27113074..6595baa826ebc 100644
--- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrdersNavigateMenuTest.xml
+++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsOrdersNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsProductsInCartNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsProductsInCartNavigateMenuTest.xml
index ec8b60ac743fd..601c2015ea7e7 100644
--- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsProductsInCartNavigateMenuTest.xml
+++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsProductsInCartNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsRefreshStatisticsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsRefreshStatisticsNavigateMenuTest.xml
index 9a32e20594dfa..3d0a247a0b180 100644
--- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsRefreshStatisticsNavigateMenuTest.xml
+++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsRefreshStatisticsNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsTaxNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsTaxNavigateMenuTest.xml
index f8091d4f63101..65fb97d60fa77 100644
--- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsTaxNavigateMenuTest.xml
+++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsTaxNavigateMenuTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsViewsNavigateMenuTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsViewsNavigateMenuTest.xml
index d68eb332d81a3..45ba0e76f67a5 100644
--- a/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsViewsNavigateMenuTest.xml
+++ b/app/code/Magento/Reports/Test/Mftf/Test/AdminReportsViewsNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Order/CollectionTest.php b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Order/CollectionTest.php
index 50398d42a7019..f2d6433ec24bb 100644
--- a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Order/CollectionTest.php
+++ b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Order/CollectionTest.php
@@ -278,20 +278,17 @@ public function testPrepareSummary($useAggregatedData, $mainTable, $isFilter, $g
* @param int $range
* @param string $customStart
* @param string $customEnd
- * @param string $expectedInterval
+ * @param array $expectedInterval
*
* @return void
* @dataProvider firstPartDateRangeDataProvider
*/
public function testGetDateRangeFirstPart($range, $customStart, $customEnd, $expectedInterval): void
{
- $timeZoneToReturn = date_default_timezone_get();
- date_default_timezone_set('UTC');
$result = $this->collection->getDateRange($range, $customStart, $customEnd);
$interval = $result['to']->diff($result['from']);
- date_default_timezone_set($timeZoneToReturn);
$intervalResult = $interval->format('%y %m %d %h:%i:%s');
- $this->assertEquals($expectedInterval, $intervalResult);
+ $this->assertContains($intervalResult, $expectedInterval);
}
/**
@@ -464,9 +461,9 @@ public function useAggregatedDataDataProvider(): array
public function firstPartDateRangeDataProvider(): array
{
return [
- ['', '', '', '0 0 0 23:59:59'],
- ['24h', '', '', '0 0 1 0:0:0'],
- ['7d', '', '', '0 0 6 23:59:59']
+ ['', '', '', ['0 0 0 23:59:59', '0 0 1 0:59:59', '0 0 0 22:59:59']],
+ ['24h', '', '', ['0 0 1 0:0:0', '0 0 1 1:0:0', '0 0 0 23:0:0']],
+ ['7d', '', '', ['0 0 6 23:59:59', '0 0 7 0:59:59', '0 0 6 22:59:59']]
];
}
diff --git a/app/code/Magento/RequireJs/README.md b/app/code/Magento/RequireJs/README.md
index 8ed9f88095606..55573d9c5fafd 100644
--- a/app/code/Magento/RequireJs/README.md
+++ b/app/code/Magento/RequireJs/README.md
@@ -1,12 +1,15 @@
# Overview
+
## Purpose of module
The Magento\RequireJs module introduces support for RequireJs JavaScript library and provides infrastructure for other modules to have them declared related configuration for RequireJs library.
# Deployment
+
## System requirements
The Magento\RequireJs module does not have any specific system requirements.
## Install
+
The Magento\RequireJs module is installed automatically (using the native Magento Setup). No additional actions required.
diff --git a/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php b/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php
index 1fb7e7df2461f..900cdc1f330d4 100644
--- a/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php
+++ b/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php
@@ -21,22 +21,16 @@
class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection
{
/**
- * Entities alias
- *
* @var array
*/
protected $_entitiesAlias = [];
/**
- * Review store table
- *
* @var string
*/
protected $_reviewStoreTable;
/**
- * Add store data flag
- *
* @var bool
*/
protected $_addStoreDataFlag = false;
@@ -159,6 +153,17 @@ protected function _construct()
$this->_initTables();
}
+ /**
+ * @inheritDoc
+ */
+ public function _resetState(): void
+ {
+ parent::_resetState();
+ $this->_entitiesAlias = [];
+ $this->_addStoreDataFlag = false;
+ $this->_storesIds = [];
+ }
+
/**
* Initialize select
*
diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingPendingReviewsNavigateMenuActiveTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingPendingReviewsNavigateMenuActiveTest.xml
index 57e7d44dab10d..4339d423159f1 100644
--- a/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingPendingReviewsNavigateMenuActiveTest.xml
+++ b/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingPendingReviewsNavigateMenuActiveTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingReviewsNavigateMenuTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingReviewsNavigateMenuTest.xml
index 32f11b08616cb..218899f1f6889 100644
--- a/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingReviewsNavigateMenuTest.xml
+++ b/app/code/Magento/Review/Test/Mftf/Test/AdminMarketingReviewsNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminRatingsAddNewRatingAttributeTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminRatingsAddNewRatingAttributeTest.xml
index 18b45155fdc67..bca91c2ed88fd 100644
--- a/app/code/Magento/Review/Test/Mftf/Test/AdminRatingsAddNewRatingAttributeTest.xml
+++ b/app/code/Magento/Review/Test/Mftf/Test/AdminRatingsAddNewRatingAttributeTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByCustomersNavigateMenuTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByCustomersNavigateMenuTest.xml
index 51b4ff58e88f1..475349e6d19a5 100644
--- a/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByCustomersNavigateMenuTest.xml
+++ b/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByCustomersNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByProductsNavigateMenuTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByProductsNavigateMenuTest.xml
index e577289ed3679..c4ee11c9b8d21 100644
--- a/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByProductsNavigateMenuTest.xml
+++ b/app/code/Magento/Review/Test/Mftf/Test/AdminReportsByProductsNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminStoresRatingNavigateMenuTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminStoresRatingNavigateMenuTest.xml
index 49e574c09fe78..decb69b6a7027 100644
--- a/app/code/Magento/Review/Test/Mftf/Test/AdminStoresRatingNavigateMenuTest.xml
+++ b/app/code/Magento/Review/Test/Mftf/Test/AdminStoresRatingNavigateMenuTest.xml
@@ -19,6 +19,7 @@
+
diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminValidateLastReviewDateForReviewsByProductsReportTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminValidateLastReviewDateForReviewsByProductsReportTest.xml
index 1209bd4d351e0..4b49d3d0079d2 100644
--- a/app/code/Magento/Review/Test/Mftf/Test/AdminValidateLastReviewDateForReviewsByProductsReportTest.xml
+++ b/app/code/Magento/Review/Test/Mftf/Test/AdminValidateLastReviewDateForReviewsByProductsReportTest.xml
@@ -18,6 +18,7 @@
+
diff --git a/app/code/Magento/Review/Test/Mftf/Test/AdminVerifyNewRatingFormSingleStoreModeNoTest.xml b/app/code/Magento/Review/Test/Mftf/Test/AdminVerifyNewRatingFormSingleStoreModeNoTest.xml
index e9a08a3e196f5..5c52335c81796 100644
--- a/app/code/Magento/Review/Test/Mftf/Test/AdminVerifyNewRatingFormSingleStoreModeNoTest.xml
+++ b/app/code/Magento/Review/Test/Mftf/Test/AdminVerifyNewRatingFormSingleStoreModeNoTest.xml
@@ -17,6 +17,7 @@
+
diff --git a/app/code/Magento/Review/Test/Mftf/Test/StorefrontNoJavascriptErrorOnAddYourReviewClickTest.xml b/app/code/Magento/Review/Test/Mftf/Test/StorefrontNoJavascriptErrorOnAddYourReviewClickTest.xml
index b577c415fd242..03b26005459af 100644
--- a/app/code/Magento/Review/Test/Mftf/Test/StorefrontNoJavascriptErrorOnAddYourReviewClickTest.xml
+++ b/app/code/Magento/Review/Test/Mftf/Test/StorefrontNoJavascriptErrorOnAddYourReviewClickTest.xml
@@ -16,6 +16,7 @@
+
diff --git a/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifyProductReviewInCustomerAccountTest.xml b/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifyProductReviewInCustomerAccountTest.xml
index c581fd2757ad3..ce8b68a0e4af9 100644
--- a/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifyProductReviewInCustomerAccountTest.xml
+++ b/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifyProductReviewInCustomerAccountTest.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml b/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml
index b714bac3a7ab3..aadcef81da880 100644
--- a/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml
+++ b/app/code/Magento/Review/view/frontend/layout/catalog_product_view.xml
@@ -26,6 +26,7 @@
Magento\Framework\View\Element\ButtonLockManager
+
diff --git a/app/code/Magento/Review/view/frontend/templates/form.phtml b/app/code/Magento/Review/view/frontend/templates/form.phtml
index 1a01bfd387cde..17dbde65bf7e6 100644
--- a/app/code/Magento/Review/view/frontend/templates/form.phtml
+++ b/app/code/Magento/Review/view/frontend/templates/form.phtml
@@ -72,6 +72,9 @@
+
+ = $block->getChildHtml('form_additional_review_info') ?>
+